# Delta Agent Harness Delta is a lean runtime for building production agents that do durable knowledge work. It gives one model call a workspace, tools, memory, delegation, human review hooks, tracked-usage budgets, durable execution, and production visibility without turning the harness into a framework. > This is the canonical Delta guide. It is self-contained and written for both humans and language models. Start with the quickstart, then use the later sections as the exact operating reference. Choose a path: - [Create and test a local agent](#start-here) - [Build an agentic assistant or AI feature](#two-ways-to-build) - [Give a coding agent the right Delta context](#use-delta-with-coding-agents) - [Integrate through the CLI or HTTP API](#run-and-call-the-agent) - [Understand tools, MCP, memory, and review](#built-in-capabilities) - [Deploy and operate in production](#deploy-delta) - [Look up every setting](#configuration-reference) This guide describes the source checkout that contains it. A running daemon reports its harness version at `/healthz`. Delta reads `DELTA_BUILD` at runtime. The Docker build argument persists it into the image environment; a direct binary deployment must export it when starting the daemon. A Delta agent is three things: 1. **Engine:** the compiled `delta` binary. It owns the model loop, tool execution, persistence, budgets, events, and HTTP API. 2. **Bundle:** five small files that define one agent. They hold its identity, policy, dynamic context, product vocabulary, and local configuration. 3. **State:** the workspace plus a SQLite database. They hold files, conversations, checkpoints, memory, events, and self-file revisions. Delta is designed for work that crosses files, tools, systems, and people. It is not limited to a source repository. When a task genuinely needs advanced coding, Delta can delegate that part to an installed Codex, Claude Code, or other command-line coding agent. ## Start here ### Prerequisites - Bun 1.3 or newer - A model credential, such as an OpenRouter, Anthropic, or OpenAI API key - Optional: an Exa key for built-in web search - Optional: an installed coding CLI for the `code` tool - Optional: the host `grep` command for the `grep` tool - Optional: the host `unzip` command for lightweight DOCX and XLSX extraction Delta installs three ways: `curl -fsSL https://deltaharness.dev/install.sh | sh`, `bunx @carrara-labs/delta-harness`, or a prebuilt binary from GitHub Releases. To build from source, clone the repository. Commands in this guide use `./dist/delta ...` from a source checkout; if you installed the binary, substitute `delta ...`. All commands run from the repository root unless stated otherwise. ### Build the binary ```sh bun install --frozen-lockfile bun run build ``` The build creates `dist/delta`, a Bun-independent executable for the current operating system and architecture. Build on the target platform, or use the Docker build for Linux. Optional built-ins can still call external `grep`, `unzip`, and coding CLI executables. During repository development, run the test suite with: ```sh bun test ``` ### Create an agent ```sh ./dist/delta init ./my-agent ``` `delta init` attempts to create these five scaffold files with exclusive writes: ```text my-agent/ ├── delta.env ├── vocab.json ├── DELTA.md ├── POLICY.md └── PROMPT_CONTEXT.md ``` It never overwrites an existing path. In a partially scaffolded directory it creates only the missing files. A permissions, disk, or path failure can leave a partial scaffold and returns a nonzero exit status; rerun after fixing the error to fill the remaining files. Set a model credential and a stable agent ID in `my-agent/delta.env`: ```dotenv OPENROUTER_API_KEY=your-key DELTA_MODEL_PRIMARY=anthropic/claude-sonnet-5 DELTA_AGENT_ID=my-research-agent ``` The agent ID must remain stable for the life of the agent. Local memory is scoped by it. `delta.env` is a local-development convenience inside the agent workspace, and file tools can read workspace files. Use a restricted development key, never commit this file, and keep production secrets in the process environment or a secret manager. Add at least these paths to the repository ignore rules: ```gitignore my-agent/delta.env my-agent/.delta/ ``` Give the agent a concrete identity in `my-agent/DELTA.md`: ```md # Persona You are Meridian, a research operator for the product team. # Mission Turn ambiguous market questions into sourced, decision-ready briefs. # Success The reader can make a decision without repeating the research. # Learned Keep recommendations explicit and separate evidence from inference. ``` `POLICY.md` can remain as generated for the first run. Its comment-only starter causes Delta to use the embedded review policy when a reviewed-write MCP tool is present. ### Launch the agent and Cockpit ```sh ./dist/delta dev ./my-agent --port 8080 ``` With the generated configuration, the launcher: - reads `my-agent/delta.env` - uses `my-agent` as the workspace, unless `delta.env` sets `DELTA_WORKSPACE` - stores local state in `my-agent/.delta/delta.db`, unless `delta.env` sets `DELTA_DB` - defaults the daemon bind to `127.0.0.1` - defaults local Cockpit editing and normalized model-call capture to on - opens `http://localhost:8080/dev` `delta.env` overlays the launcher's inherited environment. `DELTA_BIND`, `DELTA_INSPECT_WRITE`, and `DELTA_CAPTURE_CALLS` from that merged environment override their development defaults. `--port`, or the automatically selected free port, always replaces `PORT`. For workspace and database paths, only values in `delta.env` override the launcher defaults; inherited `DELTA_WORKSPACE` and `DELTA_DB` values are ignored. Relative overrides resolve from the bundle directory. Do not set a public `DELTA_BIND` for local development unless `DELTA_CONTROL_TOKEN` and a deliberate inspection policy are also configured. Otherwise the run API can become reachable beyond the local machine. The bundled browser UI does not attach those tokens, so a token-protected local Cockpit also needs an authorization-injecting proxy. Omit `--port 8080` to let Delta select a free port. Keep the browser closed when needed: ```sh ./dist/delta dev ./my-agent --port 8080 --no-open ``` The terminal prints the selected port. `delta dev` runs the ordinary production daemon as a child, so the execution behavior is the same as a directly launched Delta. ### Send the first task In another terminal: ```sh ./dist/delta send --port 8080 "Using only DELTA.md and POLICY.md, create brief.md with the agent mission, success criteria, risks, and next steps." ``` The default `delta send` path creates an asynchronous task, then prints the run's dev event stream as JSON Lines until it finishes. It needs the control credential to create the task and the inspection credential to read that stream when those gates are enabled. For only the final text: ```sh ./dist/delta send --port 8080 --json "Read brief.md and summarize it in five bullets." ``` The `--json` name is historical. This mode makes a synchronous request and prints only `output_text`; it needs only the control credential. `delta send` and `delta watch` read credentials from their own process environment and do not load the bundle's `delta.env`. At this point the agent is real: it can reason across multiple model turns, use files and tools, persist every checkpoint, continue a conversation, and be inspected in the Cockpit. ### Develop and acceptance-test the agent Treat the five-file bundle as code and keep one fixed acceptance task for the agent: 1. Start Delta on a pinned port and send the task without `previous_response_id`, which creates a fresh session. 2. In Cockpit, inspect the run timeline, model calls, selected tools, tool arguments, results, usage, and final answer. 3. Verify the expected workspace artifact, budget behavior, policy behavior, and a deliberate failure case such as a missing input file. 4. Edit the smallest relevant bundle file. `DELTA.md` takes effect on the next run; policy, vocabulary, all prompt context, MCP, and provider changes require a daemon restart. 5. Run the identical task in another fresh session and compare the artifact, tool choices, cost, and call trace. 6. Before release, repeat the task through the authenticated production API and inspect the production run with the separate inspection credential. `bun test` verifies the Delta engine. This repeatable task verifies the particular agent bundle and its connected systems. ## Use Delta with coding agents The documentation exposes three layers so Claude Code, Cursor, Codex, and similar tools can receive only the context they need: 1. **Immediate handoff:** use **Copy agent context** from the documentation header menu, then paste it into the coding agent with your task. The copied brief explains Delta's integration shapes, current source-checkout workflow, bundle contract, safety rules, and authoritative URLs. 2. **Focused retrieval:** point an agent to [`/llms.txt`](/llms.txt) for a concise index, [`/guide.md`](/guide.md) for the canonical guide, or [`/llms-full.txt`](/llms-full.txt) when a tool explicitly needs the complete corpus. 3. **Persistent project context:** place a short, relevant Delta section in the target repository's root `AGENTS.md`, or install the official Delta Skill once it ships. A hosted `AGENTS.md` is a source to copy from; coding agents only load it automatically after it is present in the repository they are working in. The header's **Copy .md** action copies the complete current guide. Use that for a full page handoff. Prefer the smaller agent context for routine implementation work so the coding agent can retrieve only the relevant guide sections instead of carrying the entire manual in every turn. Delta ships as the npm package `@carrara-labs/delta-harness`, installable via `install.sh`, `bunx`, or a prebuilt release binary; the brief points agents at these alongside the source-checkout commands in this guide. Still planned: a portable `SKILL.md` and a small installer that can add or update a bounded Delta section in the target repository's `AGENTS.md`. MCP is only necessary later if agents need live registry search or operational actions; static product knowledge does not require it. ## Mental model ### What one run does Every request follows the same durable loop: ```text request -> durable queue row -> load identity, policy, context, memory, and relevant capabilities -> model call -> zero or more parallel tool calls -> checkpoint messages, usage, tool intents, and active tools -> repeat until final text, cancellation, budget exhaustion, or failure -> on a handled outcome, durable response and run.finished event -> optional background reflection ``` A run may last for seconds or hours. Delta has no overall wall-clock limit. Its loop stops on successful-main-model-call, recorded fresh-token, and recorded model-cost budgets. Individual model and ordinary tool calls have separate timeouts, with the exceptions documented under built-in capabilities. ### The prompt spine Delta rebuilds a small, ordered system spine for every model turn: 1. Delta's base behavior and operating norms 2. boot-stable rendered context 3. `DELTA.md`, read once at the start of the current execution attempt 4. `POLICY.md`, rendered as the last instruction layer 5. an index of currently resident tool names and descriptions The full JSON tool schemas are sent separately in the provider request's `tools` field. Conversation history follows the system spine. Per-turn context, request instructions, and retrieved capabilities are appended as user-role blocks. This matters because request-scoped text can specialize a task, but it does not become system policy. The stable prefix is deliberately cache-friendly. Volatile values such as time and request metadata stay outside it. `{{model}}` is the configured primary on the first turn and the model that served the previous successful turn after that, so it cannot predict a fallback that will serve the call currently being assembled. ### Sessions and runs One API response ID identifies one run. Passing that ID as `previous_response_id` appends another run to the same session and preserves the full current conversation. It is a session lookup, not a branch pointer: passing an older response ID does not fork or rewind the conversation, so pass the latest response ID. Runs in one session execute serially. The shipped daemon executes up to `DELTA_MAX_CONCURRENCY` different sessions concurrently (default 8, clamped 1–256; see [Run and call the agent](#run-and-call-the-agent)). Do not invent or reuse an unknown response ID. Delta returns `400` instead of silently starting a new session. ## Two ways to build Delta is one runtime, but products put it to work in two shapes. Both are the same agent with the same primitives — threads for context, tools for reach, and review-driven reflection for learning. They differ only in who opens the thread and where the human gives feedback. - **An agentic assistant.** A person chats with the agent. Each topic or task is its own thread; the agent uses tools and permissioned app access to do real work, and gets better from how its proposals are received. - **An agentic feature.** A product turns a one-shot LLM call — "generate a job description," "draft the outreach," "summarize the deal" — into an agent that iterates to a reviewable deliverable and improves across generations. The feature is a pre-seeded thread: the same agent, opened by the product with task context instead of by a person with a chat message. The rest of this guide is the reference for every primitive named here. This section shows how they compose into each shape. ### A thread is the unit of context A thread is a Delta session (see [Sessions and runs](#sessions-and-runs)). Open one with a first `POST /v1/responses`; continue or switch by passing that thread's latest response ID as `previous_response_id`. Everything that makes a long task survivable — compaction, `recall`, the working plan — is scoped to the thread, so two threads never bleed into each other. A person managing several threads, or a product running one thread per job, gets clean isolation without extra work. Your product owns the list of threads and their titles; Delta owns each thread's execution and memory. ### Learning spans threads; context does not The isolation above is deliberate, and it is only half the design. The learning rail is the other half, and it runs the opposite way: a lesson distilled in one thread becomes available to every later thread of the same kind. Tag related work with a caller-declared `task_type` (see [Request fields](#request-fields)) — `jd-generation`, `weekly-report` — and reflection tiers what it learns to that use case. Generation fifty opens a fresh, isolated context but recalls what generations one through forty-nine taught the agent about the job. Isolation where you want a clean slate; memory where you want compounding. ### Use case 1: an agentic assistant Give a person a chat surface backed by one agent. - **Threads for context.** One thread per topic or task; the user switches between them and each keeps its own working context. - **Tools and permissioned app access.** Connect the agent to your systems over [MCP](#connect-mcp-tools) — retrieve contacts, read a CRM, draft in a document store — under whatever scopes your gateway enforces. Delta calls the tools; your product owns identity and permission. - **Self-improvement from review.** When the agent proposes work and a human accepts, edits, or rejects it, that verdict becomes a learning signal (see [Learn from human review](#learn-from-human-review)). The agent's `DELTA.md` and scoped memory improve without a prompt change. ### Use case 2: an agentic feature Turn a static feature into an agent that does the whole job and learns from every correction. The mechanism is the same review loop, opened by your product instead of by a person. Job-description generation, end to end: 1. **Open a thread per job.** `POST /v1/responses` with no `previous_response_id`, seeded with the source material and tagged with the feature's `task_type`: ```json { "input": "Draft a JD and specs for the Staff PM role. Hiring-manager transcript: ... Recruiter transcript: ...", "metadata": { "task_type": "jd-generation", "user_id": "acme" } } ``` 2. **Let it work and iterate.** The agent pulls context, drafts, and proposes; continue the thread with `previous_response_id` as the hiring manager reacts. This job's transcripts and drafts stay in this thread. 3. **Propose, do not just answer.** Delta's write rail bundles the deliverable into one proposal a human reviews (see [`POLICY.md`](#policymd-fixed-operator-rules) and [Learn from human review](#learn-from-human-review)), stamped back to the run. 4. **Feed the verdict back.** When the reviewer accepts with edits, your control plane sends the disposition into the same thread as a new run: ```json { "input": "Review outcome: accepted with edits. Proposed: ... Accepted: ... Reviewer note: lead with scope, cut adjectives.", "metadata": { "review_kind": "submission_disposition", "submission_id": "sub_123", "reflect": true } } ``` 5. **It learns from the diff, and the next job starts smarter.** Reflection grounds the lesson in the gap between what was proposed and what was accepted, tiers it to `jd-generation`, and the next job's thread recalls it at open. ### What Delta owns and what you own Delta owns the loop, threads, context management, reflection, and the memory tiers. Your product — or a control plane — owns opening and titling threads, the review surface, and, critically, authenticating the disposition and constructing the real proposed-versus-accepted digest. Delta trusts that digest; it does not verify the reviewer, so the trusted plane must be the one asserting that a human reviewed the work. The learning loop ships wired to a submission-and-review model: an agent that proposes through the review inbox and is reviewed there gets the propose-review-learn cycle today. A feature on a different product surface reuses every harness primitive but supplies its own equivalent of that disposition digest and sender. ## Configure the five-file bundle ### `delta.env`: local launch configuration `delta dev` parses this file as trimmed `KEY=value` pairs. Blank lines and full lines beginning with `#` are ignored. One matching pair of outer single or double quotes is removed. It is not a shell parser: `export`, variable expansion, escape processing, and inline-comment removal are not supported, and malformed lines are silently skipped. The direct daemon does not automatically load `delta.env`. In production, pass the same settings as real environment variables. The container entrypoint can seed the four non-secret bundle files, but never `delta.env`. Keep credentials out of version control. A useful local file starts small: ```dotenv OPENROUTER_API_KEY=your-key DELTA_MODEL_PRIMARY=anthropic/claude-sonnet-5 DELTA_AGENT_ID=my-research-agent # Optional built-in web search EXA_API_KEY=your-exa-key # Optional post-run learning DELTA_REFLECT=1 ``` ### `DELTA.md`: identity and writable self-memory `DELTA.md` is the agent's compact, durable self-file. The generated structure is: ```md # Persona Who the agent is and who it serves. # Mission The standing goal it exists to advance. # Success The concrete outcome that defines good work. # Learned Durable lessons worth carrying into every future run. ``` The file is human-editable and agent-editable. The model can replace it through the `remember` tool. A content-changing overwrite is atomic and size-capped. When prior content exists, Delta records it in SQLite before retaining the newest 20 previous revisions. The first write and a same-content write do not create a revision. The Cockpit can compare and restore recorded revisions. The filesystem rename happens just before the database snapshot, so a process crash in that narrow interval can leave the new file in place without a revision for the immediately prior content. Important behavior: - Delta reads one snapshot of `DELTA.md` for each execution attempt. An uninterrupted run keeps that snapshot. A run resumed after a daemon restart loads the current file again. - A change made during uninterrupted execution becomes visible to the next fresh or restarted execution attempt, not the current in-memory attempt. - The default budget is about 800 tokens, implemented as a 3,200-byte write ceiling. - A file above the prompt budget but no larger than 1 MB is head-and-tail elided as a recovery behavior. A file above 1 MB loads as empty, and an oversized self-write is rejected by its byte cap. - The agent must write the complete replacement file, not only a new lesson. Keep this file lean. Every future model call includes it. ### `POLICY.md`: fixed operator rules `POLICY.md` is the fixed, highest-priority prompt guidance for a daemon process. It is loaded once when the daemon starts, placed after writable identity as the last instruction layer before the tool index, and cannot be changed by the normal root-file tools or `remember`. Use it for rules such as: ```md # Policy - Never send an external message without human approval. - Cite the source for every material factual claim. - Stop and ask when an action could spend more than $500. ``` Important behavior: - A missing, unreadable, heading-only, or comment-only file uses Delta's embedded reviewed-write policy when a compatible write rail exists. - A real custom policy replaces that embedded policy. - The default policy budget is about 800 tokens. - Delta refuses to boot when a custom policy exceeds the budget. Fixed rules are never silently truncated. - An edit on disk requires a daemon restart. Human review is policy plus tools, not a built-in approval server. Delta's embedded policy recognizes an MCP tool whose name ends with the configured reviewed-write suffix, such as `propose_change`. It tells the agent to bundle work into one human-reviewable proposal. The actual queue, approval UI, and side effect belong to the connected product. Prompt guidance is not an authorization boundary. Delta does not evaluate `POLICY.md` before each tool call. MCP services, the trusted gateway, and any approval product must enforce permissions and block unapproved side effects even when the model ignores or misinterprets the prompt. ### `PROMPT_CONTEXT.md`: stable and live context This optional file has two sections: ```md ## Stable Engine {{engine.version}} | agent {{agent.id}} | profile {{profile}} ## Turn Model {{model}} | date {{now.date}} | timezone {{now.tz}} Requester city: {{request.city}} ``` `## Stable` is rendered once at boot and joins the cached prefix. It can use: - `{{engine.version}}` - `{{agent.id}}` - `{{profile}}` `## Turn` is rendered for every model turn. It can use: - `{{model}}` - `{{now.iso}}` - `{{now.date}}` - `{{now.tz}}` - `{{request.}}` from `metadata.context` Both section templates are loaded at daemon boot. The turn block is re-rendered from that boot-loaded template, so editing either section requires restart. Example request: ```json { "input": "Prepare the regional launch note.", "metadata": { "context": { "city": "Paris", "country": "FR" } } } ``` Request context is treated as bounded data. Delta accepts at most 24 safe keys, reduces each value to one line of at most 200 characters, and leaves unknown placeholders visible. The source file is ignored if it exceeds 100 KB. Rendered stable and turn blocks are capped at 2,000 and 4,000 characters respectively. ### `vocab.json`: product bindings The vocabulary file lets the same engine fit a product without compiling product names into the runtime. It controls: - which MCP verb suffixes are pinned in the lean core - which tool suffix is the reviewed-write rail - the nouns used by the embedded review policy - which request metadata keys identify a hydration subject - how a learned artifact maps into the product's write-tool arguments - the default local memory namespace The generated neutral vocabulary is enough for a plain agent. A product-specific example is: ```json { "coreVerbs": ["get_dashboard", "list_open_work", "propose_change"], "writeVerbSuffix": "propose_change", "writeNoun": "workspace", "runRefKey": "run_ref", "learningTargetKind": "note", "taskNoun": "task", "itemNoun": "review item", "subjectKeys": ["account"], "writeShape": { "summary": "{{summary}}", "details": "{{brief}}", "provenance": { "key": "{{run_ref_key}}", "value": "{{run_id}}" }, "items": [ { "kind": "{{target_kind}}", "source_kind": "{{kind}}", "content": "{{content}}", "confidence": "{{confidence}}" } ] } } ``` MCP tools are named `__`. A core verb matches the suffix after `__`. For example, `crm__get_dashboard` matches `get_dashboard`. `writeVerbSuffix` is resolved with the first insertion-order tool whose full name ends with that text. It must identify exactly one connected tool. If two servers expose `propose_change`, use a more specific suffix such as `crm__propose_change`; otherwise policy rendering and learned-write promotion can bind to the wrong server. `writeShape` is the exact argument object sent to the tool whose name ends in `writeVerbSuffix` when Delta promotes a fact, preference, or pitfall. It replaces the built-in envelope completely; it is not merged with it. Delta recursively resolves these placeholders: | Placeholder | Value | |---|---| | `{{run_id}}` | Producing run ID. | | `{{kind}}` | Learned artifact kind. | | `{{content}}` | Distilled learning text. | | `{{confidence}}` | Numeric confidence when available. | | `{{target_kind}}` | `learningTargetKind` from the vocabulary. | | `{{summary}}` | Generated one-line reflection summary. | | `{{brief}}` | Generated reflection details and provenance. | | `{{run_ref_key}}` | The configured `runRefKey` name. | A string leaf that is exactly one placeholder keeps the value's type. If that value is unavailable, Delta removes the object key or array item. Placeholders inside a longer string are converted to text, and unavailable values become an empty string. Without `writeShape`, Delta sends this logical envelope: ```json { "summary": "Reflection () from run ", "details": "Post-task or post-review reflection details", "": "", "items": [ { "kind": "", "content": "", "confidence": 0.8 } ] } ``` The `confidence` key is omitted when no numeric value exists. The custom shape must be a JSON object at its root. `DELTA_VOCAB` can supply the same JSON as an environment variable and takes precedence over `vocab.json`. Malformed vocabulary falls back to neutral defaults. If the wording of `writeNoun` changes, the derived memory namespace also changes. Set `DELTA_MEMORY_NAMESPACE` explicitly when changing only the wording. ## Choose a model provider Delta has no model SDK dependency. It supports three wire formats: - OpenAI-compatible Chat Completions, used by OpenRouter and many other gateways - native Anthropic Messages - OpenAI Responses ### OpenRouter OpenRouter is the default endpoint and requires the least configuration: ```dotenv OPENROUTER_API_KEY=your-key DELTA_MODEL_PRIMARY=anthropic/claude-sonnet-5 ``` `MODEL_API_KEY` can be used instead of `OPENROUTER_API_KEY`. Set `DELTA_MODEL_FALLBACKS` to a comma-separated model list to try additional models on the same provider: ```dotenv DELTA_MODEL_FALLBACKS=openai/gpt-5.5,google/gemini-3.5-flash ``` Primary credential precedence is `MODEL_API_KEY`, then `OPENROUTER_API_KEY`. A defined but empty `MODEL_API_KEY` suppresses the fallback key. The fallback key name is not endpoint-aware, so set `MODEL_API_KEY` explicitly whenever `MODEL_BASE_URL` is not OpenRouter. Delta does not validate a usable credential at boot or in `/healthz`; the first model call exposes a bad route or key. ### Anthropic directly Use the base URL without `/messages` because Delta appends that path: ```dotenv MODEL_BASE_URL=https://api.anthropic.com/v1 MODEL_API=anthropic MODEL_API_KEY=your-anthropic-key DELTA_MODEL_PRIMARY=claude-sonnet-5 ``` Delta sends native Messages requests and uses Anthropic prompt-cache breakpoints on the system prefix and recent user or tool blocks. ### OpenAI directly For the Responses API: ```dotenv MODEL_BASE_URL=https://api.openai.com/v1 MODEL_API=responses MODEL_API_KEY=your-openai-key DELTA_MODEL_PRIMARY=gpt-5 ``` For an OpenAI-compatible Chat Completions endpoint, omit `MODEL_API`. Delta appends `/chat/completions` to `MODEL_BASE_URL`. Model IDs pass through unchanged. `DELTA_MODEL_FALLBACKS` shares the primary base URL, wire, headers, and credential, so every ID in that list must be valid on that endpoint. ### Provider and model failover There are two levels of failover: 1. `DELTA_MODEL_FALLBACKS` tries more model IDs on the primary endpoint. 2. `DELTA_PROVIDERS` adds endpoints after the primary provider. Example: ```dotenv BACKUP_OPENAI_KEY=your-key DELTA_PROVIDERS=[{"label":"openai","baseUrl":"https://api.openai.com/v1","api":"responses","models":["gpt-5"],"apiKeyEnv":"BACKUP_OPENAI_KEY"}] ``` Inside one provider, a retriable failure gets two retries by default, for up to three wire attempts per model, then advances to the next model. A non-retriable client `4xx` skips retries but still advances to the next model on that provider. Credential-wide `401`, `403`, broker `409`, and shared-subscription `429` errors skip the remaining models. After the provider's model list is exhausted, network errors, `401`, `403`, `409`, `429`, and `5xx` errors can advance to the next provider; a normal client `4xx` and an exhausted HTTP `408` do not. Failed attempts do not increment the run's step count and usually have no usage to charge. The timeout applies per wire attempt, so retries and fallbacks can make one model turn last longer than `DELTA_MODEL_TIMEOUT_MS`. Once answer text has started streaming, Delta does not retry or switch providers and concatenate a second answer. Each `DELTA_PROVIDERS` entry supports `baseUrl`, `models`, `api`, `label`, `apiKey`, `apiKeyEnv`, `brokerMintUrl`, and `brokerAuthEnv`. `baseUrl` is required. `models` may be one string or an array and otherwise reuses the primary model list. Only `anthropic` and `responses` are recognized as `api`; every other value selects compatible Chat Completions. An inline `apiKey`, including an empty string, wins over `apiKeyEnv`. Fallback entries cannot configure static headers and do not inherit `MODEL_HEADERS`. Invalid JSON is logged and the whole fallback list is ignored. Entries must be JSON objects; a `null` entry currently aborts configuration loading. `MODEL_HEADERS` accepts a JSON object of extra static headers for the primary provider only. Authentication, account, protocol, cookie, and hop-by-hop headers are reserved and rejected at boot. ### Codex subscription access Delta has two distinct Codex paths: 1. **Delegated coding:** the built-in `code` tool runs an installed Codex CLI. The CLI uses its own credentials from the process user's home directory. 2. **Primary model access:** Delta can call an OpenAI Responses-compatible Codex backend with a short-lived bearer minted by a trusted broker. The primary-model subscription path is an infrastructure integration, not a standalone login flow. A complete deployment commonly uses: ```dotenv MODEL_API=responses MODEL_BASE_URL=https://chatgpt.com/backend-api/codex DELTA_MODEL_PRIMARY= DELTA_BROKER_TOKEN_URL=https://control.example/api/broker/openai-token DELTA_BROKER_AUTH=machine-gateway-token MODEL_HEADERS={"originator":"codex_cli_rs"} ``` The harness requires the Responses wire, an endpoint-native model ID, an allowed backend host, and a usable broker response. `DELTA_BROKER_AUTH` and `MODEL_HEADERS` are optional to Delta but may be required by the deployed broker or backend. Delta sends a `GET` to the broker with the optional bearer and a 15-second timeout. A usable JSON response contains non-empty `accessToken`, non-empty `accountId`, and an ISO `expiresAt` more than five minutes in the future. Delta caches the token and coalesces concurrent mints. A `401` or `403` invalidates it and permits one re-mint retry; `409` immediately advances to another provider; a `429` temporarily cools the shared credential and advances to a keyed provider. Subscription tokens are sent only to exact HTTPS hosts in `DELTA_BROKER_ALLOWED_HOSTS`, whose default is `chatgpt.com`. Add the real backend host explicitly when required. The broker URL must be HTTPS, except for loopback development. Configure a keyed provider in `DELTA_PROVIDERS` as a production fallback for broker exhaustion, authentication failure, or rate limiting. Subscription-route `cost_usd` is API-rate-equivalent consumption from Delta's price table, not the incremental subscription bill. ### Utility model and reasoning Compaction, reflection, and `eval_n` judging use `DELTA_UTILITY_MODEL`, which defaults to `anthropic/claude-haiku-4.5`. Set it to an empty value to use the main provider cascade for those calls. The utility model is tried on each compatible provider. Native Anthropic translates dotted Claude version segments to dashes; Responses providers are skipped for a Claude utility model. Any utility-lane failure falls back to the main cascade, and reasoning effort is not applied to utility calls. Set a daemon default for extended reasoning: ```dotenv DELTA_REASONING_EFFORT=high ``` Or override it for one run with `metadata.reasoning_effort`. Delta lowercases and trims the value. OpenRouter and Responses receive `reasoning.effort`; a directly compatible Chat Completions endpoint receives `reasoning_effort`. Native Anthropic picks the wire by model. Claude 4.6+ and every Claude 5 model use `thinking:{type:"adaptive"}` with `output_config.effort` — they reject the older `thinking:{type:"enabled"}` budget wire; on this path the OpenAI-only `none` and `minimal` map to `low` (an always-on reasoning model cannot disable thinking), and Delta raises the request's `max_tokens` by an effort-based headroom so adaptive thinking has room. Claude 4.5 and earlier keep the legacy budget wire, mapping `none`, `minimal`, `low`, `medium`, `high`, and `xhigh` to disabled, 1,024, 4,096, 8,192, 16,384, and 32,768 thinking tokens, with an unknown value using the 16,384-token mapping. Other providers validate their own accepted values and can return a clean `4xx`. ## Run and call the agent ### CLI reference | Command | Behavior | |---|---| | `delta init ` | Create the five-file bundle without overwriting files. | | `delta dev [--port N] [--no-open]` | Load `delta.env`, run a loopback daemon, capture calls, and open the Cockpit. | | `delta send [--port N] ""` | Create an async task and print its dev event stream as JSON Lines. | | `delta send [--port N] --json ""` | Make a sync request and print final `output_text`. | | `delta watch [--port N] [--run ] [--since ]` | Replay and follow the Cockpit event stream without starting work. | | `delta run ` | Run one task with an in-memory database and persistent workspace. Primarily used for delegated subagents. | | `delta bundle apply` | Re-seed the FIXED operator files (`POLICY.md`, `vocab.json`, `PROMPT_CONTEXT.md`) from their base64 env vars, validating each first; never touches `DELTA.md`. Also run automatically on container boot. | `delta run` reads configuration from the process environment and bundle files from `DELTA_WORKSPACE`. It does not load `delta.env`, connect configured MCP servers, start HTTP or Cockpit, or provide crash recovery. Its conversation database is in memory, but file writes and `DELTA.md` changes persist in the workspace. ### Synchronous HTTP request ```sh curl -sS http://127.0.0.1:8080/v1/responses \ -H 'content-type: application/json' \ -d '{ "input": "Create a decision brief from the files in the workspace.", "metadata": { "user_id": "user_123", "task_id": "task_456", "task_type": "decision-brief" } }' ``` When `DELTA_CONTROL_TOKEN` is set, add the machine bearer to every run, task, queue, and upload request: ```sh -H "authorization: Bearer $DELTA_CONTROL_TOKEN" ``` The terminal response has this shape: ```json { "id": "resp_...", "object": "response", "model": "anthropic/claude-sonnet-5", "status": "completed", "output_text": "...", "output": [ { "type": "message", "role": "assistant", "content": [{ "type": "output_text", "text": "..." }] } ], "previous_response_id": null, "usage": { "input_tokens": 0, "output_tokens": 0, "total_tokens": 0, "cost_usd": 0 } } ``` ### Continue a conversation Use the prior response's `id`: ```sh curl -sS http://127.0.0.1:8080/v1/responses \ -H 'content-type: application/json' \ -d '{ "input": "Now turn that brief into an executive email.", "previous_response_id": "resp_previous" }' ``` The new run joins the same session. Its active conversation, compacted summaries, and session-scoped user identity remain available. A prior response ID is only a session lookup, not a history snapshot or branch point. Continuing from an older response still appends to the current session head and sees later turns. Multiple continuations into one session serialize rather than fork. ### Stream final text ```sh curl -N http://127.0.0.1:8080/v1/responses \ -H 'content-type: application/json' \ -H 'accept: text/event-stream' \ -d '{"input":"Write the brief.","stream":true}' ``` The stream emits `response.output_text.delta` frames, followed by one `response.completed` frame with the full response. Tool-calling turns can emit intermediate assistant text before the terminal answer, so concatenating every delta is not guaranteed to equal the final `output_text`; treat `response.completed` as authoritative. Reasoning deltas are available to the internal dev stream, but are never mixed into final answer text. ### Start and monitor a long task Create the task: ```sh curl -sS http://127.0.0.1:8080/v1/tasks \ -H 'content-type: application/json' \ -d '{"input":"Review every report in the workspace and synthesize the findings."}' ``` Delta acknowledges only after the queue row is durable: ```json {"id":"resp_...","object":"task","status":"queued"} ``` Inspect status: ```sh curl -sS http://127.0.0.1:8080/v1/tasks/resp_... ``` The task view carries lifecycle metadata for a polling host: `created_at` (accepted and enqueued), `started_at` (dequeued and began executing), and `finished_at` (terminal), which separate queue wait from execution time. A failed or cancelled run also exposes a first-class `error` field with the run's last error — the provider or fatal error that ended it — so a plain poller learns why a run failed before its first token, without shelling in. Per-tool errors remain in the message stream. Follow live task events: ```sh curl -N http://127.0.0.1:8080/v1/tasks/resp_.../events ``` This task event endpoint is live-only. If the task is already terminal, it immediately emits the final `done` frame. Use `/v1/dev/stream?since=` when replayable inspection is required. The shipped queue runs up to `DELTA_MAX_CONCURRENCY` cross-session runs at once (**default 8**, clamped 1–256), with no admission or backlog cap. Each run is IO-bound async work on one event loop — this is "how many provider calls in flight", not threads — and sessions stay serial (one run per session). The mechanism itself is proven correct well past this; the practical ceiling is your provider's concurrent-request tolerance (a subscription key wants a **low** value; a high-limit API key can run 25+), then per-run context memory (~4–15 MB per active run). Raise it for a high-throughput agent; lower it to 1–2 to be gentle on a rate-limited subscription. Put hard capacity limits and backpressure in the gateway. For long work, prefer `/v1/tasks` over keeping a synchronous request open. Delta disables Bun's server idle timeout, but proxies and load balancers still need suitable request, read, and SSE timeouts. Task and response streams do not bound queued frames for slow consumers, so clients must consume promptly and gateways should cap concurrent or abandoned streams. Cancel a queued or running task: ```sh curl -sS -X DELETE http://127.0.0.1:8080/v1/tasks/resp_... ``` Cancellation is cooperative. Signal-aware model and tool work receives an abort. A detached, signal-ignoring tool may still finish, so verify external state before retrying its side effect. Task status has five values: | Status | Meaning | |---|---| | `queued` | Durable and waiting for a free run slot. Sessions stay serial; different sessions run concurrently up to `DELTA_MAX_CONCURRENCY` (default 8). | | `running` | The worker owns the run. | | `done` | Completed successfully. | | `failed` | Reached a terminal provider, tool-loop, or budget failure. | | `cancelled` | Cancelled before start or stopped cooperatively while running. | `GET /v1/tasks/:id` includes `result` only for `done`, `failed`, or `cancelled`. Handled outcomes store a complete Responses-compatible object with `status`, `output_text`, `output`, model, previous response ID, and usage. Failed and cancelled output begins with a Delta status marker rather than pretending to be a successful answer. An unexpected exception outside Delta's handled run path can currently mark a task `failed` with an empty `{}` result and no normal `run.finished` event. A task or response SSE client waiting for that event can continue receiving heartbeats, so clients need their own terminal-status polling and timeout. Operators must alert on structured errors and malformed terminal results. **Progress events.** `GET /v1/tasks/:id/events` follows the live run as Server-Sent Events; add `?coarse=1` to drop the per-token `output_text.delta` / `reasoning.delta` frames and keep only the structural heartbeat (run/turn/tool lifecycle, `model.call`, `checkpoint`, `error`). For a host that can't hold an SSE connection, `GET /v1/tasks/:id/events?since=` returns a bounded JSON page `{ "events": [...], "cursor": N, "done": bool }` — pass the returned `cursor` as the next `since` and stop when `done` is true (`limit` defaults to 200, max 500). `model.call` events carry `cache_hit_pct`, the share of that turn's input served from the prompt cache, so a host can watch cache warmth live. Both surfaces require the run's owner (see identity, below). Handled non-streaming API errors use this shape: ```json {"error":{"message":"human-readable reason"}} ``` The request root must be a JSON object. A valid JSON scalar such as `null` currently escapes the normal validation path and can produce a generic `500` response instead of this envelope, so a public gateway should reject non-object bodies before forwarding them. | Status | Common meaning | |---|---| | `400` | Invalid JSON, empty input, unknown prior response, or invalid route parameters. | | `401` | Missing or incorrect control or inspection bearer. | | `403` | Inspection is non-loopback without an inspect token, editing is disabled, or a file operation is forbidden. | | `404` | Unknown task or route, unavailable inspection state, or the inspection surface is disabled. | | `413` | An uploaded file, batch, or Cockpit file body exceeds its cap. | ### Request fields | Field | Meaning | |---|---| | `input` | Required non-empty task text. | | `instructions` | Optional task-specific instructions appended as an ephemeral user-role block on each model turn. | | `previous_response_id` | Continue the session that contains this response. | | `stream` | Stream a synchronous response over SSE. | | `metadata` | Caller-supplied identity, budgets, context, auth, learning, and correlation fields described below. A trusted gateway must validate and normalize it. | | `store` | Accepted for Responses API compatibility. It does not disable Delta's required durable persistence. | Useful metadata: | Metadata | Meaning | |---|---| | `user_id` | Bind the session user, scope private memory, and correlate events. Prefer this snake-case form for consistency; the `userId` alias is also recognized and durably binds the session owner at creation. | | `agent_id` or `agentId` | Override the per-run event identity. The daemon's stable memory identity still comes from `DELTA_AGENT_ID`; a gateway should normally replace this value. | | `task_id` or `taskId` | Correlate the run with external work. | | `entity_id`, `entityId`, or `entity` | Correlate and optionally scope product hydration. | | `task_type` or `taskType` | Select a caller-declared reusable memory tier, such as `weekly-report`. | | `profile` | Narrow the daemon's placement profile for this request. It cannot escalate permissions or budgets. | | `reasoning_effort` or `reasoningEffort` | Override the main model's reasoning effort for this run. | | `reflect` | Set `true` to run post-task reflection even when the daemon default is off. | | `authToken` | Replace static authorization for this run's HTTP MCP calls. Treat it as a sensitive, short-lived user credential. | | `context` | Supply bounded values for `{{request.*}}` prompt context. | | `review_kind` | Set exactly `submission_disposition` for a human-review correction turn. **Stripped from the body by default** (0.2.4); honored only when `DELTA_TRUST_REVIEW_METADATA=1`. | | `submission_id` | Caller correlation only. Delta does not look up, authenticate, or validate a submission with it. | | `widen_authorized` | Allow reviewed user-derived learning to widen beyond private user memory when explicitly `true`. **Stripped from the body by default** (0.2.4); honored only when `DELTA_TRUST_REVIEW_METADATA=1`. | The trust model: the control token authenticates the *gateway*; the gateway asserts which of its users a request is for via the `x-delta-user` header, which is the sole tenancy authority — Delta never takes ownership from a request-body field. As of 0.2.4 Delta **enforces ownership of task IDs**: `GET`/`DELETE /v1/tasks/:id` and `…/events` are served only to the run's owner, and a cross-tenant ID and an unknown ID return the same `404` so existence isn't disclosed. The `x-delta-user` principal is canonicalized into the stored run at ingress, so recall, reflection, and event identity all key on that owner — a body `user_id` can no longer point them at another tenant — and the idempotency dedupe is scoped to the owner. Session ownership is still enforced too: continuing with `previous_response_id` is rejected across owners (a `404`, indistinguishable from an unknown ID). A run with no asserted owner stays open, which is the single-tenant path. Review-widening authorization (`review_kind`, `widen_authorized`) is **stripped from every request body by default** — a shared control token is not proof a human reviewer set them — so a caller can't self-authorize widening a user-scoped memory; `DELTA_TRUST_REVIEW_METADATA=1` opts a single-tenant operator back in. For a daemon that serves **multiple users behind one control token**, set `DELTA_STRICT_TENANT=1`: every run must then be owned (creating a task without `x-delta-user` is `401`), unowned runs are inaccessible, a null-owned session can't be continued by another principal, and `/v1/queue` requires a principal and numbers positions only among the caller's own entries. A trusted gateway should still authenticate the caller and inject one canonical `x-delta-user`; end users must never reach Delta directly. ### Queue visibility ```sh curl -sS http://127.0.0.1:8080/v1/queue \ -H 'x-delta-user: user_123' ``` The named caller sees its own queued and running rows with IDs and session IDs. Other users' rows remain opaque and show only status, position, and age. The header is an identity hint expected from a trusted control plane, not authentication by itself. ### Suspend safety ```sh curl -sS http://127.0.0.1:8080/v1/busy \ -H 'authorization: Bearer ' ``` Returns `{"busy":true|false,"running":N,"queued":N}`. `busy` is the durable queued-or-running truth read from the run table, so a host managing suspend and resume never suspends a Machine with work still owed, including a queued run that has not started. It sits behind the `/v1/` control-token gate and is deliberately not folded into the open, data-free `/healthz`. A host polls it before suspend and after every terminal task; the [hosting contract](https://github.com/Carrara-Labs/delta-harness/blob/main/docs/hosting.md) covers the full lifecycle. ### Upload files ```sh curl -sS http://127.0.0.1:8080/v1/files \ -F 'file=@./brief.docx' \ -F 'file=@./chart.png' ``` Delta validates the full batch before writing any file. Limits are 50 files per request, 25 MB per file, and 100 MB total. The later filesystem writes are not transactional, so a storage failure on file N can leave earlier files from that batch in place. Files land under `inbox/YYYY-MM-DD/`, and the first upload creates `FILES.md` with organization conventions. Pass the returned paths to the agent in the next request. Images are attached to a vision-capable model only after Delta itself has verified their provenance through upload, `read_file`, or an MCP image response. Up to four recent images smaller than about 3.4 MB each are attached. Provenance is process-local, so after a daemon restart re-read a workspace image, re-upload it, or retrieve it again through MCP before expecting an inline attachment. Old markers remain file references and can be re-read. ## Built-in capabilities The `work` profile exposes the built-in tools below. Tool failures are returned to the model as values, so one failed dependency does not crash the daemon. | Tool | What it does | Important limits | |---|---|---| | `web_search` | Search through Exa. | Requires `EXA_API_KEY`; 5 results by default, 10 maximum. | | `web_fetch` | Fetch an HTTP or HTTPS page. | Blocks private, loopback, and metadata addresses, including up to five redirects, unless explicitly relaxed. It buffers the full response before applying the returned-text cap, so use egress and upstream size controls for hostile endpoints. | | `read_file` | Read supported workspace files. | Text: 20 MB maximum and pages of at most 2,000 lines and 50,000 characters. Also handles images, notebooks, DOCX text, and XLSX shared-string cells. PDFs, PPTX, other binary formats, formulas, numeric spreadsheet cells, and document layout need another tool; DOCX and XLSX extraction requires host `unzip`. | | `list_dir` | Recursively list files under a workspace directory. | Directories are not returned. The full list is built before the central result cap. | | `grep` | Search file contents with a regular expression. | Maximum 100 hits; 10 second process timeout; skips `.delta`. | | `write_file` | Create or replace a workspace file. | Confined to the workspace. No per-file or workspace quota is enforced. Only protected names at the workspace root are blocked. | | `move_file` | Move or rename a workspace file. | Destination overwrite must be explicit. | | `delete_file` | Move a file or directory to recoverable trash. | Entries older than seven days are swept at daemon startup, not periodically while it runs. | | `remember` | Replace `DELTA.md`. | Full-file replacement, size-capped, revisioned, visible on the next run. | | `recall` | Search this conversation's earlier turns, including ones compacted out of the live window. | Returns the matching snippet plus the on-disk path of any spilled result; scoped to the current session; lexical search over the recent window, not a full-text index. | | `todo` | Read or replace the run's working plan. | Re-injected every turn and survives compaction; bounded, so offload large notes to a workspace file. Send the full item list to write; omit to read. | | `search_tools` | Find and activate non-resident allowed tools for the current run. | Activates up to five name or description matches per call; lexical matching only. | | `code` | Delegate a task to a configured coding CLI. | No harness timeout or Delta filesystem sandbox. The child has every path and capability available to the daemon's OS user, subject to any sandbox implemented by the selected CLI. | | `research` | Run 1 to 3 parallel in-process sub-agents that each explore in their own bounded context and return a short summary plus a findings-file path. | One nesting level; each child holds the parent's tools minus the delegation and scheduling tools; children share the workspace. | | `spawn_subagent` | Run one isolated side task through `delta run`. | One nesting level; fresh in-memory conversation and narrow environment. | | `eval_n` | Run 2 to 5 independent variants and judge the best. | Best for reasoning or drafting that does not mutate shared files. | | `schedule_self` | Ask a control plane to wake the agent later. | Registered only when control-plane URL and token are configured. | | `list_schedules` | List the agent's schedules through the control plane. | Same requirement. | | `cancel_schedule` | Cancel a schedule through the control plane. | Same requirement. | `DELTA_TOOL_TIMEOUT_MS` supplies the outer timeout for ordinary queued-run tools and defaults to 120 seconds. In normal queued execution, that outer signal replaces the search and fetch helpers' fallback 30-second signal; setting it to `0` therefore leaves those tools without a harness ceiling. MCP calls have an internal 60-second ceiling, and scheduling calls use 15 seconds. `code`, `spawn_subagent`, and `eval_n` deliberately use no outer timeout. Asynchronous work that ignores abort can continue detached, and synchronous blocking code cannot be preempted. ### Workspace safety Normal file tools resolve every model-supplied path inside the workspace and reject traversal and escaping symlinks. Their protected-name rule applies only at the workspace root. They cannot write these root paths: - `POLICY.md` - `vocab.json` - `PROMPT_CONTEXT.md` - `DELTA.md`, except through `remember` - root `.env`, `.env.*`, or `delta.env` - root `.delta` and everything under it The same names below a subdirectory are not protected, so `notes/POLICY.md` is writable. These are mutation guards, not read controls or a process sandbox. `read_file` can read workspace configuration, and the `code` tool runs a separate process with the daemon OS user's access. Never place secrets anywhere in the agent workspace. Use a dedicated OS user, VM, or purpose-built sandbox for stronger isolation. ### Large tool results All tool results pass through one central cap, 20,000 characters by default. If a result is larger, Delta keeps its beginning and end in the conversation and saves the complete result under: ```text .delta/spill/..txt ``` The inline message tells the model where to re-read the full output. Change the inline cap with `DELTA_TOOL_RESULT_MAX_BYTES`. ### Coding CLI The default command is: ```text codex exec --sandbox workspace-write --skip-git-repo-check ``` Override it as a whitespace-separated command: ```dotenv DELTA_CODE_CLI=claude --print ``` The command runs with the workspace as its current directory and has no harness timeout. Delta splits `DELTA_CODE_CLI` on spaces without shell or quote parsing, so quoted arguments and executable paths containing spaces are unsupported. The default container does not install a coding CLI. Add one to the image and provision its credentials if the `code` tool is required. Delta's default asks Codex for its own `workspace-write` sandbox; a custom CLI receives no equivalent sandbox from Delta. ### Subagents and `eval_n` `spawn_subagent` starts the same Delta binary in one-shot `run` mode with: - a new in-memory database and conversation - the same workspace - the same bundle files, loaded again under child defaults - the remaining parent fresh-token and cost budget - at most one level of nesting - a default-deny child environment - a narrow allowlist of provider and run variables plus one static primary credential The child forwards the safe process environment plus the primary base URL, wire type, legacy model name, model fallbacks, utility model, profile, model and stream timeouts, result cap, remaining budget values, and one static primary key. It does not inherit the parent's MCP configuration, Exa key, telemetry, control-plane token, subscription broker token, provider cascade objects or secrets, model headers, custom prices, reasoning, vision settings, coding CLI, stable agent ID, learning and hydration settings, or custom self and policy caps. A broker-only parent therefore cannot currently power a spawned subagent unless a static primary model key is also available. The same bundle does not guarantee the same resolved behavior: the child can reset to the default coding CLI, default identity, and 800-token file caps. The child allowlist forwards both `DELTA_MODEL_PRIMARY` and the legacy `DELTA_MODEL` alias, so a subagent runs on the parent's configured model. The child also does not receive `MODEL_HEADERS`, `DELTA_MODEL_PRICES`, or reasoning settings, so a custom-header route, custom metering, or parent reasoning configuration is not reproduced in the child. Subagents share the workspace. A subagent can also receive the `remember` tool under the `work` profile, so it can replace the shared `DELTA.md`; its revision database is temporary. Use delegation only for tasks whose file and self-memory effects are intentional. `eval_n` runs variants concurrently against the same workspace. Use it for drafting, analysis, or other non-mutating work. Do not use it for concurrent edits to the same files. Each child prints a `DELTA_USAGE` marker. Valid parsed usage is added to the parent and constrained by the remaining fresh-token and dollar budget. Missing or malformed child usage fails open as zero. External spend by a coding CLI or subscription route is not discovered through this marker. The current `eval_n` judge call is not charged to the parent usage total. All tool calls returned by one model turn execute concurrently, including mutations, and up to `DELTA_MAX_CONCURRENCY` different sessions can run concurrently (default 8). The `DELTA.md` self-file is guarded by optimistic concurrency — a write that raced a diverged base is refused and the run re-reads, re-merges, and retries — but there is no compare-and-swap layer for other files or external resources. Same-session serialization does not serialize sibling tool calls within one turn. Design mutating tools to be idempotent and conflict-aware, and avoid parallel tasks that can touch the same state. ### Scheduling Delta does not keep a local alarm clock because a production VM may be suspended. When both settings are present, it exposes scheduling tools backed by the control plane: ```dotenv DELTA_CONTROL_URL=https://control.example DELTA_CONTROL_TOKEN=machine-token ``` Schedules support: - one-time ISO timestamps - intervals of at least 60 seconds - five-field cron expressions with an optional IANA timezone The control plane owns storage, time, and wake-up behavior. Delta only requests and manages schedules through its authenticated API. ## Connect MCP tools Delta implements MCP Streamable HTTP and newline-framed stdio without an SDK dependency. It initializes every configured server at daemon boot, discovers its tools, and registers them as `__`. ### HTTP server `DELTA_MCP_SERVERS` should be a JSON array. Give each entry an explicit `transport`; if you omit it, Delta infers it from the entry shape (`url` implies `http`, `command` implies `stdio`) and logs that it did so: ```dotenv DELTA_MCP_SERVERS=[{"name":"crm","transport":"http","url":"https://crm.example/mcp","headers":{"authorization":"Bearer service-token"}}] ``` The expanded JSON is: ```json [ { "name": "crm", "transport": "http", "url": "https://crm.example/mcp", "headers": { "authorization": "Bearer service-token" } } ] ``` An MCP tool named `get_account` becomes `crm__get_account`. Names that cannot fit the model API's tool-name rules are dropped. Malformed JSON is treated as no configured servers. Delta validates each entry's shape at boot and skips any malformed entry (with a warning), but it does not verify a server is reachable. Inspect boot logs plus `/v1/dev/config` rather than assuming a non-empty variable produced tools. ### Stdio server ```dotenv DELTA_MCP_SERVERS=[{"name":"local","transport":"stdio","command":["bun","run","server.ts"],"env":{"MODE":"production"}}] ``` The stdio child inherits the daemon environment plus the explicit `env` object. Treat a stdio MCP process as trusted code inside the same machine boundary. ### Authentication Static HTTP headers apply to discovery and tool calls. For a user-scoped run, pass a short-lived token: ```json { "input": "Update my account brief.", "metadata": { "user_id": "user_123", "authToken": "short-lived-user-token" } } ``` For that run, Delta replaces the HTTP MCP `Authorization` header with `Bearer `. One run has one shared `authToken`, and Delta applies it to every HTTP MCP server called by that run. Use tokens with the correct audience across all connected servers, or place differently scoped connectors on separate daemons. Each MCP service must enforce the user's permissions. Delta also supports one rotating MCP credential attached to a named HTTP server: ```dotenv DELTA_MCP_REFRESH_URL=https://auth.example/token DELTA_MCP_REFRESH_FILE=/data/mcp-refresh-token DELTA_MCP_REFRESH_SERVER=crm DELTA_MCP_REFRESH_CLIENT_ID=delta-agent DELTA_MCP_REFRESH_TOKEN=first-boot-refresh-token ``` The required fields are URL, file, and server. The seed token and client ID are optional. On an MCP `401`, Delta rotates once and retries unless a per-run user token supplied the authorization. The refresh file is authoritative persistent state. Delta seeds it only when absent, does not create its parent directory, and does not repair an existing empty or corrupt file. Pre-create a private writable parent outside the agent workspace, use one refresh-token file per daemon or machine, and include its current value in the recovery design. Never restore a stale backup of a refresh token that may already have been exchanged. Refresh coalescing is process-local. Do not also set a static `Authorization` header on the selected rotating server. Static headers are applied after the rotating credential and can overwrite it, including on the `401` retry. A partial or unmatched refresh configuration only warns and may leave the server unauthenticated, so assert the expected connection at startup. ### Failure and reload behavior One unavailable MCP server does not prevent the daemon from starting. Its tools are absent and the failure is logged. MCP configuration is boot-time in the shipped daemon, so restart after changing `DELTA_MCP_SERVERS`. Delta implements a deliberately small MCP subset around protocol version `2025-06-18`. It calls `tools/list` once at boot, does not paginate or consume list-changed notifications, and requires a restart for rediscovery. HTTP accepts plain JSON or one complete JSON object on a single SSE `data:` line; multiline SSE events are unsupported. Stdio is newline-delimited JSON, not `Content-Length` framing. Tool results retain text and image content only; structured content, resources, audio, and embedded resources are not passed to the model. Every MCP result is framed as untrusted data before it reaches the model. The server still owns the side effects and authorization of its tools. Crash idempotence is inferred from the MCP tool name. Names containing an underscore- or dot-delimited segment such as `get`, `list`, `search`, `read`, `version`, `versions`, or `file` are treated as read-only and may be re-executed after interruption. All other MCP tools are treated as non-idempotent. Name mutations so they do not contain those read segments. In particular, a mutating name such as `get_or_create` is unsafe because Delta will classify it as idempotent. ### Progressive tool loading A large connector surface should not put hundreds of schemas in every prompt. The `work` profile: 1. pins all built-in tools 2. pins MCP tools whose suffix appears in `vocab.coreVerbs` 3. leaves other allowed tools searchable through `search_tools` 4. activates up to five matches at a time 5. persists those activations with the run Delta applies a 60-name threshold to the initially requested pinned set. If that set is larger, it warns and falls back to the built-ins plus `vocab.coreVerbs`. The fallback is not capped a second time, so an oversized `coreVerbs` list can still exceed 60. Keep the declared core small; this threshold is a guardrail, not a strict invariant or a restriction on the total MCP registry. ## Memory and self-improvement Delta has three separate learning surfaces. They solve different problems and should not be conflated. ### 1. The living self-file `DELTA.md` carries a small lesson into every future request. Use it for a compact operating preference or identity change that is important enough to pay for on every model call. The `remember` tool is explicit, reversible, and visible in the workspace. It is not the same as automatic post-run reflection. Concurrent updates are atomic but last-writer-wins, so two runs can overwrite each other's lesson without corrupting the file. ### 2. Governed local memory Optional reflection runs after a successful response and distills at most one reusable artifact: - fact - preference - pitfall - procedure Enable it for every successful run: ```dotenv DELTA_REFLECT=1 ``` Or enable it for one request: ```json { "input": "Prepare the weekly revenue review.", "metadata": { "reflect": true, "task_type": "weekly-revenue-review" } } ``` Reflection runs best-effort in the background after the response has been released. It has no durable work queue or retry, and shutdown or a crash can drop it. A failure never changes the completed user response. A per-run `reflect: false` does not disable a daemon that has `DELTA_REFLECT=1`. Reflection usage is added later to the run row, but it is not added to the already stored terminal response payload or the earlier `run.finished` event and does not emit a normal `model.call` usage event. Cockpit can therefore later show more run usage than the API result or exported terminal telemetry. Local memory is structured by four independent axes: | Axis | Values | |---|---| | Audience | `user`, `task_type`, `agent`, `org` | | Artifact | `fact`, `preference`, `pitfall`, `procedure` | | Trust | `trusted`, `untrusted` | | Source | `self`, `review` | It is also keyed by product namespace, agent ID, user ID when private, and caller-declared task type when shared by use case. The local recall query currently returns agent, current-user, and matching-task-type rows. `org` rows can participate in shared promotion state but are not recalled by the local rail. Governance rules: - self-rated confidence below `0.6` is rejected - a human-review correction receives at least `0.8` confidence - content is capped at 500 characters - duplicate content confirms one row and records the distinct producing run - one identity is normally capped at 200 evictable rows; rows protected by staged or claimed promotion can temporarily take it over that limit - memory unused for 90 days stops being recalled - recall scores lexical relevance, aliases, confidence, recency, and prior use, but applies no minimum relevance threshold, so a zero-overlap row can still rank into the bounded result - recall is inserted only at the first run of a session Intended privacy routing, after a trusted gateway has supplied canonical metadata, is: - a run tied to a user writes user-scoped memory - a model cannot widen that private memory to a shared audience - widening user-derived learning requires a human review turn with explicit `widen_authorized` - user memory is never promoted to an external shared store - a userless run with `task_type` can create and recall memory shared only for that caller-declared use case These rules are not an authentication layer. Caller-controlled identity and review fields can change scopes unless a gateway normalizes them as described under request fields. Agent recall also includes legacy rows whose `agent_id` is empty. Separate databases or migrate and delete legacy rows before relying on agent-level isolation. The local memory rail is deterministic SQLite retrieval, not vector search. It gives the harness a useful no-backend fallback while leaving semantic enterprise knowledge to an optional product store. ### 3. External context and reusable skills Task-start hydration can call product MCP reads before the first model turn of a new session: ```dotenv DELTA_HYDRATE_TOOLS=crm__get_dashboard,crm__list_recent_notes DELTA_HYDRATE_SEARCH_TOOL=crm__search_knowledge ``` Subject-scoped reads run only when request metadata contains a key declared by `vocab.subjectKeys`. If the vocabulary contains `"account"`, Delta checks `account`, `account_id`, and `accountId`. Task-keyed search requires `metadata.authToken`, even when no subject is present. With a trusted gateway, this avoids falling back to a shared daemon credential for user knowledge. Hydration is best-effort, waits at most 20 seconds, and injects at most about 16,000 characters across its blocks. The wait race does not abort the underlying calls, which can continue detached. Hydration calls configured tools directly, without the normal tool journal, idempotence classifier, or central result cap. Configure read-only, least-privilege tools only. Each configured hydration read receives the resolved subject fields plus `limit: 20`. The search tool receives both `q` and `query`, the same subject fields, and `limit: 8`; the task text is truncated to 500 characters and must contain at least two non-space characters. Delta sends that whole object unchanged. A strict MCP schema must declare every supplied property or tolerate extras. Separately, the capability adapter searches once per execution attempt for a reusable procedure, so it can repeat after crash recovery. Its 20-second wait likewise does not abort a late underlying call. Delta loads the first reference returned by the adapter and exposes other matches as references. A custom adapter may rank by task relevance; the built-in skill-registry adapter currently uses its returned index order. The block is ephemeral, bounded, and framed as untrusted directory data. The built-in adapter recognizes skill-registry-style MCP tools with this concrete contract: - it is considered bound only when a matching skill create, update, or propose tool exists; `skill_search` and `skill_get` alone do not activate retrieval - directory search uses a tool whose name ends in `skill_search`, normally `__skill_search`, and calls it with `{ "limit": 25 }` - loading a procedure uses a tool whose name ends in `skill_get` and calls it with `{ "name": "" }` - creation or update discovers a tool whose name matches `skill` followed by `create`, `update`, or `propose` - a create sends `name`, `body`, and a bounded `description`; an update sends `name`, `body`, `base_version`, and `baseVersion`; reflected promotions also send both `change_summary` and `changeSummary` - an update retries once after a reported version conflict by loading and rebuilding from the fresh version - the current built-in search does not send the task query and keeps the returned index order; custom capability adapters can rank by relevance - only one matching capability-tool family is bound, selected by insertion order - `skill_search` extracts JSON-like objects with a string `name` and optional string `description` - `skill_get` accepts only a string `body` and integer `version` of at least 1; other response shapes are treated as absent Strict skill-registry tool schemas must accept the paired snake-case and camel-case fields above. The engine boundary remains generic: - a capability store searches, reads, and proposes versioned procedures - a curated store accepts proposed facts, preferences, and pitfalls Facts, preferences, and pitfalls go to the MCP tool selected by `vocab.writeVerbSuffix`; `vocab.writeShape` maps them into that product's exact arguments. Procedures use the capability adapter instead. These external stores are optional. A plain Delta works with local memory only. ### Promotion to shared knowledge Non-user learning enters a durable SQLite outbox before any external proposal: - procedures target the capability adapter - facts, preferences, and pitfalls target the curated adapter - a review-grounded correction may promote immediately - the same lowercased, whitespace-normalized reflection content must recur in two distinct runs by default; a semantic paraphrase counts as different content - only trusted memory can promote - the local outbox carries an idempotency key, but the bundled skill-registry and curated MCP adapters do not transmit it to their remote tools; a crash after remote success can therefore duplicate a proposal - a failed proposal can be retried when a later successful reflection drains the outbox and becomes terminal after five attempts; there is no independent retry timer Change the recurrence threshold with `DELTA_PROMOTE_MIN_RUNS`. Agent, user, and task-type rows remain locally useful when no external adapter is bound. An `org` row is not part of local recall. Promotion rows are scoped by the normalized content, full identity and artifact dimensions, namespace, and selected adapter. Changing the memory namespace, write noun, suffix, or adapter can strand staged rows. Drain or deliberately migrate the outbox before changing those bindings. ### Learn from human review Delta is ready to participate in a review loop, but it does not ship a universal approval service. The connected product must provide the proposal tool, review state, user interface, and eventual side effect. The recommended loop is: ```text agent work -> one proposal through __ -> human accepts, rejects, or edits it in the product -> control plane sends the disposition back as a new Delta run -> review reflection learns from proposed versus accepted work ``` The review run must be explicit: ```json { "input": "Review outcome: item 1 accepted with edits. Proposed: ... Accepted: ... Reviewer note: ...", "metadata": { "review_kind": "submission_disposition", "submission_id": "submission_123", "reflect": true, "widen_authorized": false } } ``` Only the exact `review_kind` value `submission_disposition` switches reflection to the correction-focused rubric. Delta does not validate reviewer identity, fetch the submission, or structurally compare proposed and accepted work. It asks the reflection model to ground learning in the supplied digest. The trusted control plane must authenticate the disposition and construct the real proposed-versus-accepted content rather than forwarding an end-user claim that review happened. ## Durable execution and budgets ### Profiles Two placement profiles ship: | Profile | Allowed tools | Initial schemas | Steps | Fresh tokens | Cost | |---|---|---|---:|---:|---:| | `work` | All registered tools | Lean core | 100 | 2,000,000 | $5.00 | | `chat` | `web_search`, `web_fetch`, `read_file`, `list_dir`, `recall`, `todo` | All six | 10 | 100,000 | $0.25 | Set the daemon ceiling: ```dotenv DELTA_PROFILE=work ``` A request can choose `metadata.profile: "chat"` on a `work` daemon. It cannot choose `work` on a `chat` daemon. Unknown or more-permissive request values stay at the placement ceiling. This protection assumes the daemon ceiling itself is valid: an unknown `DELTA_PROFILE` currently falls open to `work`. Validate the environment before launch and allow only `work` or `chat`. Lower the token and cost ceilings: ```dotenv DELTA_MAX_TOKENS=250000 DELTA_MAX_COST_USD=1 ``` There is no environment setting for maximum steps. Profiles are defined in the engine. ### What the budget measures One step is one successfully completed main-loop model call. Failed provider attempts, compaction, reflection, hydration, capability retrieval, and the `eval_n` judge are not steps. The fresh-token guard uses recorded main and child usage: ```text fresh input tokens + output tokens ``` Prompt-cache reads do not repeatedly consume the fresh-token budget. Dollar usage comes from the provider when available or from Delta's price table. A compatible provider that omits or misreports usage weakens both token and dollar enforcement, so verify metering with production routes before relying on caps. For a model that is not priced, add dollars per million tokens: ```dotenv DELTA_MODEL_PRICES={"my-model":{"in":2,"out":10,"cacheRead":0.2}} ``` Without a known price, Delta warns and records zero model cost for that path. A dollar cap cannot protect an unpriced model. Table lookup follows the served model ID and prefix matching; verify the selected row. A price override without `cacheWrite` values cache writes at 1.25 times input. Current parsing also accepts negative prices, so validate overrides externally because a negative value can weaken the cost guard. Budgets are hard loop guards against usage Delta records, checked between model calls. The final call, its parallel tools, or background reflection can move recorded usage past the nominal threshold before the next guard. They do not cap failed provider attempts without usage, a child with a missing or malformed usage marker, provider under-reporting, coding CLI or subscription-account spend, MCP or search API fees, cloud cost, or the current `eval_n` judge. Subscription calls record API-rate-equivalent cost rather than the subscription's actual marginal bill. Treat these values as one-call-granularity model-loop guards, not prepaid authorization or a complete spend limit. Environment overrides use JavaScript number conversion. An exported empty `DELTA_MAX_TOKENS` or `DELTA_MAX_COST_USD` becomes zero rather than unset. Validate all budget variables before starting the daemon. ### Compaction Delta compacts when the previous model request exceeded `DELTA_COMPACT_AT_TOKENS`, default 120,000, and there is more than a minimal tail of active messages to shed. It preserves a recent tail sized by a token budget (default 24,000 tokens) snapped to a turn boundary, so an assistant tool call always travels with its result, rather than a fixed message count. It asks the utility model for a summary under 350 words from at most 60,000 characters of older history. A failed compaction is a no-op. The compaction call is charged to run usage but not the step count. If a provider reports a context-window overflow earlier, Delta forces one compaction and retries the same model turn once. Forced overflow handling may also elide an oversized tool message. If compaction cannot reduce the context, the run fails cleanly. ### Checkpoints and crash recovery SQLite uses WAL mode, foreign keys, transactional schema migrations, and a five-second busy timeout. Durable state includes: - sessions and runs - active and compacted messages - model step count and usage - active tool names, whose schemas are reconstructed from the boot registry - tool intents and results - events - local memory and promotion state - writer lease - exact model-call captures when enabled - `DELTA.md` revisions The queue writes a run before acknowledging it. Each assistant message and all tool intents it creates commit atomically. Each tool result and its conversation message also commit atomically. On restart: - rows left as `running` resume before queued rows drain - an unfinished model call can run again because no response was checkpointed; the provider may already have billed the interrupted attempt, so recovery can duplicate model charges - a completed tool journal result is replayed without re-executing the tool - an interrupted idempotent tool may execute again - an interrupted non-idempotent tool is not blindly re-fired - the model receives an `[interrupted]` result and must verify whether the side effect happened - step count, usage, compaction, and activated tools survive A resumed execution is recovery, not deterministic replay. It reloads current boot configuration, `POLICY.md`, prompt context, vocabulary, MCP registry, provider routes, and a fresh `DELTA.md` snapshot. Keep bundle and infrastructure changes coordinated with in-flight recovery. Events and all run-state transitions are not one atomic log. A crash at a boundary can leave a durable state change without its expected event or can produce a repeated lifecycle event. Treat the event stream as operational observability, not an exactly-once audit ledger, and reconcile against task and run state. ### Single writer Delta uses a renewable writer lease to reject a different machine holder. The lease defaults to 30 seconds and never goes below 5 seconds. On a renewal miss the heartbeat renews-or-reacquires: it reclaims the lease when it is free or already self-held — so a Fly suspend/resume across a wall-clock jump no longer tears the daemon down without releasing — and the process exits only when a genuinely different live holder owns it. The holder ID defaults to `FLY_MACHINE_ID`, then the host name. A process with the same holder can immediately reacquire the lease so a crash restart does not wait for expiry. That also means the lease does not distinguish two live processes on the same machine. Do not set one manual `DELTA_LEASE_HOLDER` across different machines, because that defeats cross-machine exclusion too. Binding the same port catches an ordinary duplicate start before work resumes. It does not protect two processes using different ports. In particular, two `delta dev` launches choose different free ports and can open the same bundle database under the same host identity. Never run two daemons for one database or bundle. Enforce one replica in the process manager or control plane rather than treating the current lease as a complete same-machine mutex. The lease is coarse, best-effort exclusion, not per-write fencing. Clock skew, an event-loop pause longer than the TTL, or identical holder identities can permit overlapping work. Infrastructure must enforce one live owner for each database and volume. ### Local retention Defaults: | Data | Age | Row cap | |---|---:|---:| | Events | 7 days | 50,000 | | Tool journal | 7 days | 50,000 | The sweep runs once at boot and then hourly. Set `DELTA_RETENTION_SWEEP_MS=0` to disable only the periodic sweep. When telemetry export is active, the ordinary event sweep is skipped entirely and exporter overflow owns event deletion, while the sweep still prunes the journal. Set journal retention longer than the maximum run duration: deleting an old completed intent or result from an active multi-day run can remove restart replay protection. This is not whole-database retention. Sessions, runs, messages, memory, promotions, captured calls, `.delta/spill`, and `.delta/media` are not age-pruned. Workspace trash is swept only at startup. SQLite reuses deleted pages but Delta does not run `VACUUM`. Plan database and file maintenance according to workload. Shutdown is interruption-oriented, not drain-aware. On a termination signal Delta clears timers, force-stops the server, and closes the database without waiting for queued work, background reflection, or telemetry flush. Before planned maintenance, gate new traffic and use external queue and activity checks to wait for an acceptable state, or rely on documented crash recovery. The four-worker queue cap does not bound background reflection, because completed runs can start multiple untracked reflections concurrently. ## Long-running tasks and context management Delta is built to run for hours across dozens of tool calls without the active context window filling up and without dropping earlier findings. The mechanism is a single principle: **restorable context — nothing load-bearing is ever unrecoverable.** Every compression the engine performs leaves a pointer back to the full thing on disk or in the local database, so a fact found at step 3 is still available at step 50. ### The active window stays bounded Before every model call, the engine estimates the fully assembled request — system spine, tool schemas, ephemeral blocks, message history, and reserved output — using serialized byte size rather than a naive character count, and treating attached images as a fixed token reserve rather than their base64 length. If the estimate exceeds `DELTA_COMPACT_AT_TOKENS`, it compacts **before** sending, so a resumed or continued session cannot overflow on its first call. It also compacts when the previous call's real gross prompt size crossed the threshold, since cached tokens still occupy the model's window. Compaction is archive-safe. Older turns are summarized into a structured note and their message rows are marked inactive — never deleted or overwritten. The recent tail is kept verbatim, sized by a token budget rather than a fixed message count, and snapped to a turn boundary so an assistant tool call always travels with its result. The original request is pinned verbatim, read fresh from the run record, and the summary is wrapped in a trusted-request / untrusted-history envelope with an explicit end marker so a weak model cannot read the historical note as fresh instructions. The context window is one dial: ```dotenv DELTA_COMPACT_AT_TOKENS=120000 ``` Tight (60,000–90,000) is cheaper and lower latency and compacts more often. The default 120,000 is safe on any model with a 200,000-token window. Large (160,000+) keeps more continuity and performance and is safe to run because compaction is restorable — raise it only up to the model's real window minus output headroom. ### The summary preserves facts across many generations When a prior summary is already present, the engine merges forward — it instructs the summarizer to preserve every prior fact and only add new ones, rather than lossily re-summarizing a summary each generation. After each summary it **audits** the result: it harvests load-bearing identifiers (numbers, years, and on-disk paths) from the recent turns and from the carried-forward summary, verifies the new summary reproduced them, and retries once with the misses named if more than a quarter were dropped. In practice a task that fires a dozen compactions keeps its numbers intact through every generation. ### Recovering what scrolled out Two tools let an agent reach past the live window: - `recall` searches this conversation's earlier turns, including ones compacted out of the active window, by keyword. It returns the matching finding — the whole message when it is reasonably sized, not a fragment — along with the on-disk path of any large result that was spilled. Searches are scoped to the current session; a caller cannot search another session's transcript. - Tool results larger than the inline cap are spilled to a file under `.delta/spill`, and compaction records every spill path in an artifact ledger inside the summary, so a large result is always one `read_file` away. An agent can also keep its own working plan with the `todo` tool. The plan is re-injected every turn as an ephemeral block that survives compaction, because it is rebuilt from a per-session store rather than living in the message history. It is the cheapest way to hold a running list of findings across a long run, and the tool tells the agent when the plan outgrows its budget so it can offload longer notes to a workspace file instead of silently losing them. ### Delegating heavy exploration For work that would bloat the primary window, the `research` tool runs one to three sub-agents in parallel, in-process. Each explores in its own bounded context, writes its full findings to a file the parent owns, and returns only a short summary plus that path — so the primary conversation absorbs the signal, not the exploration. A research child holds the parent's exact rights: its callable tools are the parent's full registry minus a small withheld set (the delegation tools `research`, `spawn_subagent`, and `eval_n`, plus the run-scheduling tools), so nesting stays exactly one level deep. A child can read, write, run code, `remember`, and call MCP reads and writes — whatever the parent can. It is forged from the same system spine as the parent, so it inherits the same norms and policy along with those rights, and it starts on the parent's pinned tool set with `search_tools` for the rest. ### Boundaries The token estimate is a conservative heuristic, not exact provider tokenization, so the post-provider overflow path remains a backstop that sheds aggressively and retries once. The summary audit tracks numbers, years, and paths; it does not track proper names, which the summarizer preserves but the engine does not verify. `recall` is a lexical search over the most recent window of the session, not a full-text index over an unbounded transcript. The re-injected plan is bounded and meant for a running list, not a large document — large findings belong in a workspace file. These are the seams to watch on genuinely long, high-volume runs. ## Debug and inspect with Cockpit The Cockpit is an active JavaScript application embedded in every compiled binary at `/dev`. Its asset contains no embedded runtime data or credentials; it reads the separate `/v1/dev/*` inspection API after loading. `delta dev` is the fastest local path because it defaults editing and normalized successful-call capture to on unless `delta.env` explicitly overrides those settings. ### Five tabs | Tab | What it shows | |---|---| | Thread | Sessions, user and assistant messages, live reasoning, model turns, tool calls, hydration, recall, checkpoints, usage, and final status. It also has a task composer and file upload. | | Runs | Recent runs with status, served model, input and output tokens, cost, and input preview. Selecting a run opens its thread. | | Files | Workspace and operator bundle trees, text editing when enabled, and previews for supported files. Text preview is capped at 1 MiB and raw reads at 10 MiB. PNG, JPEG, GIF, WebP, and PDF can render inline; other raw types download. | | Data | Fixed, redacted projections of internal SQLite tables for sessions, memory, occurrences, promotions, events, metadata, and the writer lease. | | Setup | Resolved safe configuration, model, profile, namespace, credential-presence booleans, MCP servers, tools, vocabulary, and operator files. | The header shows the stable agent ID plus cumulative tokens, cost, and the count of distinct runs that have at least one completed captured model call in the current live view. ### Normalized model-call inspection With `DELTA_CAPTURE_CALLS=1`, each successfully served main-loop model turn stores Delta's normalized request and response representation: - the assembled system spine - active conversation messages - ephemeral context, request instructions, and retrieved capability blocks - active tool schemas - reasoning effort and cache key - returned assistant message and tool calls - actual model and provider - token usage, cost, and latency Images remain file markers instead of copied base64. Capture occurs before provider-specific wire transformation. It is not raw HTTP and does not include the final URL, headers, SSE chunks, failed attempts, or retry traffic. The Cockpit's turn detail shows this normalized request and response after redaction on read. Captured rows contain full prompts and outputs in SQLite before the read-time redaction. There is no automatic retention for the `calls` table. Leave capture off in production unless that storage and sensitivity are intentional. Capture records the provider call that successfully served a model turn. Failed provider attempts, internal transport retries, and cascade candidates that fail before a successful result are not stored as call rows. Hydration, capability lookup, reflection, promotion, compaction, and judging can call models or tools through internal paths that do not all pass through the ordinary tool journal, checkpoint, or call capture. Cockpit provides deep visibility into the main execution loop, but it is not a complete record of every internal invocation or decision. ### Replayable event stream The Cockpit and `delta watch` use: ```text GET /v1/dev/stream?since=&run=&session=&live=1 ``` Persisted frames have monotonically increasing IDs and can be replayed. Live text and reasoning deltas are ephemeral and have no durable ID. The Cockpit stream terminates a client after its buffer grows beyond 2,048 frames. Bundled Cockpit and `delta watch` do not automatically resume, so a robust custom client must persist the last durable ID and reconnect with `since`. ### Inspection API | Route | Purpose | |---|---| | `GET /v1/dev/config` | Safe resolved configuration and secret-presence booleans. | | `GET /v1/dev/stream` | Replay and follow correlated events. | | `GET /v1/dev/runs` | List runs with optional session cursoring. | | `GET /v1/dev/runs/:id` | Inspect a redacted transcript and run metadata. | | `GET /v1/dev/runs/:id/calls` | Inspect captured model calls. | | `GET /v1/dev/files` | Browse or read workspace and operator paths. Treat it as root inspection. | | `PUT /v1/dev/files` | Edit when `DELTA_INSPECT_WRITE=1`. | | `GET /v1/dev/self/revisions` | Read current and previous `DELTA.md` versions. | | `POST /v1/dev/self/revert?id=N` | Restore a revision when writes are enabled. | | `GET /v1/dev/tables` | List fixed peekable tables. | | `GET /v1/dev/tables/:name?ack=root` | Read a fixed redacted projection. | ### Apply edits Cockpit edits write bytes to disk. They do not hot-reload the daemon: - an edited `DELTA.md` applies to the next run - an edited `POLICY.md`, `PROMPT_CONTEXT.md`, or `vocab.json` requires restart - changed provider, MCP, or environment configuration requires restart This keeps the dev launcher behavior equal to production behavior. The Files tree hides `.delta`, but that is presentation, not an access-control boundary. With root inspection, a direct `/v1/dev/files` request can currently address paths under the workspace `.delta` directory; when inspection writes are enabled, it can damage live state such as the development database. Treat the inspection token as root-equivalent, keep writes off in production, and block arbitrary inspection-file calls at a proxy unless the operator explicitly needs them. ### Inspect production safely `delta send` and `delta watch` connect only to localhost. `delta dev` is a local launcher that opens a localhost Cockpit URL; it is not a remote-attachment command, although an explicit bind override can make its child daemon listen elsewhere. Production inspection uses the same `/dev` and `/v1/dev/*` surfaces, with these gates: - `DELTA_INSPECT_TOKEN` protects inspection data with a distinct bearer - without that token, inspection data is loopback-only - `DELTA_INSPECT_WRITE=1` separately enables edits and self-file revert - `DELTA_INSPECT=off` removes both `/dev` and the inspection API - `DELTA_CONTROL_TOKEN` protects run-driving endpoints but does not grant inspection The bundled browser UI does not store or attach bearer tokens. For remote use, put a trusted same-origin proxy in front of the daemon. It must authenticate the operator, inject the inspect bearer for `/v1/dev/*`, and inject the control bearer for Cockpit task and upload requests. Loopback is checked from the immediate socket peer, so a same-host reverse proxy makes remote traffic appear local. Never rely on tokenless loopback inspection behind a proxy; set a distinct inspect token or disable inspection, and block `/dev` plus `/v1/dev/*` unless the operator is authenticated. ## Telemetry and events Delta emits one correlated event stream and gives you three ways to consume it. Two are free and built in; the third ships events off the box to a collector you run. 1. **Local, always on** — every durable event is written to the local SQLite `events` table. 2. **Live tail** — in-process subscribers drive task SSE and the Cockpit's `GET /v1/dev/stream` (replay from a cursor, then follow). 3. **Ship it out (optional)** — a background exporter pushes NDJSON envelopes to an HTTP endpoint you own. The stream is a **correlated event log, not distributed tracing**: there are no spans or parent links, only a flat correlation spine — `user → agent → session → run → task → entity → turn`. Field names follow a subset of OpenTelemetry GenAI conventions (`gen_ai.request.model`, `gen_ai.usage.*`, `latency_ms`), but Delta embeds no OpenTelemetry SDK and emits no OTLP; the wire format is line-delimited JSON. ### The fastest path: no collector at all Before wiring anything, note that (1) and (2) already exist with zero configuration. `delta dev` opens the Cockpit; `GET /v1/dev/stream?since=&run=&live=1` replays the persisted `events` table from a cursor and then follows live (gated by `DELTA_INSPECT_TOKEN`). For a single agent, that is your dashboard. The exporter below is for when you run many agents and want their events in one queryable place. ### Enable export Set two variables on the daemon and the exporter starts: ```dotenv TELEMETRY_URL=https://your-collector.example/telemetry/ingest TELEMETRY_TOKEN=a-dedicated-collector-token ``` `TELEMETRY_URL` is the only thing that turns the exporter on. If `TELEMETRY_TOKEN` is absent, Delta falls back to `DELTA_CONTROL_TOKEN` as the bearer — convenient, but it sends a control credential off the box, so prefer a dedicated token. The exporter does not enforce HTTPS or an allowlist, so validate `TELEMETRY_URL` yourself and never post a control token to a plaintext or untrusted endpoint. ### Capture the useful attributes This is the setting most people miss. By default the three highest-value event types — `model.call`, `tool.call`, `tool.result` — are exported with their `attributes` object **stripped entirely**. You get event counts but no per-call model, provider, latency, token/cost, or tool names. Turn them on: ```dotenv DELTA_CAPTURE_PAYLOADS=1 ``` Despite the name, this does **not** export prompts, tool arguments, or tool results — those are never placed in these events. It only lets the *operational* attributes survive the export filter: model, provider, token usage, cost, latency, and tool names. The privacy cost is low and the observability gain is large; enable it wherever you already trust the collector. A separate flag, `DELTA_CAPTURE_CALLS`, captures true-to-life full request/response bodies into a local `calls` table for the Cockpit. It is local-only, storage-heavy, and never exported — right for deep local debugging, wrong for the telemetry stream. Don't confuse the two. ### A reference collector you can copy A complete, single-agent collector lives in the repo — a Postgres table and a ~60-line Bun handler, distilled from the multi-tenant one Carrara Labs runs in production: - [`docs/telemetry-collector/schema.sql`](https://github.com/Carrara-Labs/delta-harness/blob/main/docs/telemetry-collector/schema.sql) — the `agent_events` landing table - [`docs/telemetry-collector/server.ts`](https://github.com/Carrara-Labs/delta-harness/blob/main/docs/telemetry-collector/server.ts) — the NDJSON ingest handler - [`docs/telemetry-collector/README.md`](https://github.com/Carrara-Labs/delta-harness/blob/main/docs/telemetry-collector/README.md) — run it, point an agent at it, and the dashboard queries Apply the schema, run the server, set the three variables above, and rows appear in `agent_events` within a couple of seconds of the agent doing work. ### Delivery contract The exporter: - sends `application/x-ndjson` - wakes every two seconds - sends up to 200 records per batch - gives each request a 15-second timeout - marks rows exported only after a `2xx` - retries network and non-`2xx` failures - provides at-least-once delivery only while an unexported row remains in the bounded outbox - gives every record a restart-stable `:` deduplication key - bounds its outbox at 50,000 rows by default - deletes exported rows first on overflow - drops the oldest unexported records only when the unexported backlog alone exceeds the cap Retries are serial on a fixed two-second cadence with no exponential backoff or jitter. When telemetry is active, normal event retention is disabled: the exporter owns `events` deletion up to the outbox cap, and visibility reports backlog count rather than oldest age. **Your collector must deduplicate on `event.id`** (the exporter is at-least-once), and you should monitor collector health and backlog externally — if the collector stays down, the oldest unexported rows are dropped once the outbox fills. ### Record shape With `DELTA_CAPTURE_PAYLOADS=1`, an exported `model.call` record has this shape: ```json { "event.id": "daemon-uuid:123", "event.name": "model.call", "event.time_unix_ms": 1783950000000, "user.id": "user_123", "agent.id": "agent_123", "session.id": "sess_123", "run.id": "resp_123", "task.id": "task_123", "entity.id": "entity_123", "turn": 2, "attributes": { "gen_ai.request.model": "anthropic/claude-sonnet-5", "gen_ai.provider": "openrouter", "gen_ai.response.finish_reasons": ["tool_calls"], "gen_ai.usage.input_tokens": 1200, "gen_ai.usage.output_tokens": 300, "gen_ai.usage.cached_tokens": 900, "gen_ai.usage.cost_usd": 0.004, "latency_ms": 1800, "tool_calls": ["read_file", "run_code"] } } ``` ### Events and their attributes The stream carries run lifecycle, turn boundaries, model and tool calls, context management, the learning loop, and errors. The attributes worth querying: | Event | Key attributes | Notes | | --- | --- | --- | | `run.finished` | `status`; `gen_ai.usage.{input,output,cached,total}_tokens`; `gen_ai.usage.cost_usd` | Aggregate per run. Always present. No model id here. | | `model.call` | `gen_ai.request.model`, `gen_ai.provider`, `gen_ai.response.finish_reasons`, per-call usage + `gen_ai.usage.cost_usd`, `latency_ms`, `tool_calls` (names requested) | One per turn. **Attributes only with `DELTA_CAPTURE_PAYLOADS=1`.** | | `tool.call` / `tool.result` | `gen_ai.tool.name`, call id, error/interrupted state | **Attributes only with capture on.** | | `compaction` | `summary_tokens`, `summary_cost_usd`, `compacted_turns`, `kept`, `merged` | The cost of context management. | | `error` | `error.type` (e.g. `budget`, `context_irreducible`), `message` | Error-as-value; the daemon does not crash. | | `recall`, `hydrate`, `reflection`, `run.{enqueued,started,resumed,cancelled}` | lifecycle + learning-loop counters | — | Where the model comes from: the executing model is only on `model.call` (capture on). `run.finished` carries usage but not the model id, so for per-run model read it from `model.call` or from your own agent registry. ### The dashboard is SQL Once events land, the dashboard is just queries — no UI required to start. A few starters (full set in the [collector README](https://github.com/Carrara-Labs/delta-harness/blob/main/docs/telemetry-collector/README.md)): ```sql -- Spend and tokens per agent (dollar figures are token×price; notional under a flat subscription) SELECT agent_id, count(*) AS runs, round(sum((attributes->>'gen_ai.usage.cost_usd')::numeric), 4) AS cost_usd FROM agent_events WHERE event_name = 'run.finished' GROUP BY 1 ORDER BY cost_usd DESC; -- Which model / provider is actually executing (needs DELTA_CAPTURE_PAYLOADS=1) SELECT attributes->>'gen_ai.request.model' AS model, attributes->>'gen_ai.provider' AS provider, count(*) AS calls FROM agent_events WHERE event_name = 'model.call' GROUP BY 1, 2 ORDER BY calls DESC; -- Errors, clustered by type SELECT attributes->>'error.type' AS error_type, count(*) AS n FROM agent_events WHERE event_name = 'error' GROUP BY 1 ORDER BY n DESC; ``` ### Mistakes to avoid 1. **Leaving `DELTA_CAPTURE_PAYLOADS` off.** You get event counts but no model, tool names, latency, or per-call cost. The most common gap. 2. **Assuming that flag leaks prompts.** It doesn't — operational attributes only. Don't stay blind out of a misplaced privacy fear. 3. **Returning `5xx` on a malformed line.** The daemon then retries the whole batch forever. Skip bad lines and answer `2xx` for accepted work; reserve `5xx` for a genuine storage failure — which *should* make the daemon retry. 4. **Not deduping on `event.id`.** Delivery is at-least-once; without `ON CONFLICT DO NOTHING` you double-count everything. 5. **No body cap.** Bound the batch (the reference collector caps at 8 MB → `413`) so an authed client can't OOM the collector. 6. **Trusting identity from the payload.** Derive tenant/owner from the authed token, never from ids on the wire, or one caller can forge another's rows. 7. **Posting a control token to a plaintext or untrusted collector.** Use a dedicated `TELEMETRY_TOKEN` over TLS. 8. **Not monitoring backlog.** When telemetry is on, the daemon stops its own event retention; if the collector is down, unexported rows climb to the 50,000 cap and then the oldest are dropped. Watch it externally. 9. **Expecting the model on `run.finished`.** It's on `model.call` (capture on) or your own registry. 10. **Treating "capture off" as content-free.** Non-payload events (`recall`, `error`) can carry bounded excerpts or diagnostic strings. Give the collector a data-classification and retention policy regardless. ### Privacy Payload filtering does not mean content-free telemetry. Other events retain attributes: recall can include a bounded memory excerpt, and errors or promotion events can include diagnostic strings. Credential-like patterns are scrubbed, but personal or business data can remain. Send telemetry only to a trusted collector with an explicit data-classification and retention policy. ### OpenTelemetry The stream is NDJSON with OTel-GenAI-inspired names, but it is not OTLP and not semantic-convention complete: fields such as `gen_ai.provider`, `gen_ai.usage.cached_tokens`, `gen_ai.usage.cost_usd`, and `latency_ms` are Delta-specific or legacy-shaped. Send NDJSON to a collector that understands this contract (like the reference one above), or add a translation layer before an OTLP-only collector. ## Deploy Delta ### Production shape The intended unit is one daemon per agent: ```text trusted gateway or control plane -> one Delta process or VM -> one persistent SQLite database -> one persistent workspace -> model providers and MCP services -> optional telemetry collector ``` Do not run multiple writers against one database. Do not share one workspace between unrelated agents. A single persistent mount may contain both, but they remain separate logical state. ### Run the compiled binary directly Create a production workspace containing only the four non-secret bundle files. Do not reuse a development workspace that still contains a populated `delta.env`: ```sh mkdir -p runtime/workspace cp my-agent/DELTA.md my-agent/POLICY.md my-agent/PROMPT_CONTEXT.md my-agent/vocab.json runtime/workspace/ ``` Set real environment variables, then launch the daemon: ```sh export PORT=8080 export DELTA_BIND=127.0.0.1 export DELTA_DB="$PWD/runtime/delta.db" export DELTA_WORKSPACE="$PWD/runtime/workspace" export DELTA_AGENT_ID=my-research-agent export OPENROUTER_API_KEY=your-key export DELTA_MODEL_PRIMARY=anthropic/claude-sonnet-5 export DELTA_CONTROL_TOKEN=replace-with-a-long-random-token export DELTA_INSPECT_TOKEN=replace-with-a-different-long-random-token ./dist/delta ``` The direct defaults are `PORT=8080`, `DELTA_DB=data/delta.db`, and `DELTA_WORKSPACE=workspace`. The default bind is Bun's all-interface behavior, so set `DELTA_BIND` deliberately. Check liveness and build identity: ```sh curl -sS http://127.0.0.1:8080/healthz ``` ```json {"ok":true,"version":"0.2.4","build":"optional-commit"} ``` `build` appears only when `DELTA_BUILD` is set. This endpoint does not test the model, MCP servers, telemetry, or other dependencies. It is liveness and version metadata, not readiness. ### Run a production acceptance smoke Use the gateway URL and the two distinct credentials provisioned on that agent. Replace `crm__get_dashboard` with one required, read-only MCP tool and include any arguments its policy expects. Run this on a placement with reflection off if the smoke must not create local memory or external proposals; request metadata cannot disable a daemon-wide `DELTA_REFLECT=1`. ```sh export DELTA_URL=https://agent.example export DELTA_CONTROL_TOKEN=run-api-token export DELTA_INSPECT_TOKEN=separate-root-inspection-token curl -fsS "$DELTA_URL/healthz" SMOKE_RESPONSE="$(curl -fsS "$DELTA_URL/v1/responses" \ -H "authorization: Bearer $DELTA_CONTROL_TOKEN" \ -H 'content-type: application/json' \ -d '{"input":"Call crm__get_dashboard once using read-only smoke-test arguments, then report its result and the exact tool name."}')" SMOKE_RUN_ID="$(SMOKE_RESPONSE="$SMOKE_RESPONSE" bun -e ' const response = JSON.parse(process.env.SMOKE_RESPONSE ?? "{}"); if (!response.id || response.status !== "completed") { console.error(JSON.stringify(response)); process.exit(1); } process.stdout.write(response.id); ')" curl -fsS "$DELTA_URL/v1/dev/config" \ -H "authorization: Bearer $DELTA_INSPECT_TOKEN" curl -fsS "$DELTA_URL/v1/dev/runs/$SMOKE_RUN_ID" \ -H "authorization: Bearer $DELTA_INSPECT_TOKEN" ``` The status assertion matters because a terminal Delta failure is a valid Responses object returned with HTTP `200`; `curl -f` alone cannot detect it. In `/v1/dev/config`, verify the harness version and build, model route, `mcp_servers`, required names under `tools.mcp`, and credential-presence booleans. Inspect `/v1/dev/runs/$SMOKE_RUN_ID/calls` as an additional check when normalized successful-call capture is deliberately enabled. Finally, verify that the collector received correlated run, model, and expected tool events for `SMOKE_RUN_ID`, with stable `event.id` values. Delta has no dependency-readiness endpoint, so provider, MCP, and telemetry verification must exercise those paths. ### Build and run the container Build with optional commit provenance: ```sh docker build \ --build-arg DELTA_BUILD="$(git rev-parse HEAD)" \ -t delta:local . ``` `DELTA_BUILD` is exact provenance only when the build context is a clean, immutable checkout of that commit. On a dirty tree it is merely a HEAD label and does not describe the changed bytes. The root `.dockerignore` allowlists only the Dockerfile, build metadata, source, Cockpit asset, and entrypoint, so credentials and unrelated workspace state are not sent to the builder. The final image contains the compiled binary, CA certificates, `unzip` for lightweight document extraction, Litestream, and the entrypoint. It does not contain Bun or a coding CLI. Seed the four non-secret bundle files on the first start: ```sh DELTA_SELF_MD_B64="$(base64 < my-agent/DELTA.md | tr -d '\n')" DELTA_POLICY_MD_B64="$(base64 < my-agent/POLICY.md | tr -d '\n')" DELTA_CONTEXT_MD_B64="$(base64 < my-agent/PROMPT_CONTEXT.md | tr -d '\n')" DELTA_VOCAB_JSON_B64="$(base64 < my-agent/vocab.json | tr -d '\n')" export DELTA_SELF_MD_B64 DELTA_POLICY_MD_B64 DELTA_CONTEXT_MD_B64 DELTA_VOCAB_JSON_B64 ``` Run with persistent `/data` and real API protection: ```sh docker volume create delta-data docker run -d \ --name delta-agent \ --restart unless-stopped \ -p 127.0.0.1:8080:8080 \ --env-file my-agent/delta.env \ -e DELTA_DB=/data/delta.db \ -e DELTA_WORKSPACE=/data/workspace \ -e DELTA_CONTROL_TOKEN=replace-with-a-long-random-token \ -e DELTA_INSPECT_TOKEN=replace-with-a-different-long-random-token \ -e DELTA_SELF_MD_B64 \ -e DELTA_POLICY_MD_B64 \ -e DELTA_CONTEXT_MD_B64 \ -e DELTA_VOCAB_JSON_B64 \ -v delta-data:/data \ delta:local ``` The entrypoint writes seed files only when the target does not exist. A later image or environment change does not overwrite an evolved `DELTA.md` or other workspace file. Malformed base64 is skipped with a warning, so startup can continue with a missing or neutral bundle; verify all four files after first boot. Explicit `/data` paths after `--env-file` prevent an edited local `delta.env` from relocating persistent state. The container defaults are: ```text PORT=8080 DELTA_DB=/data/delta.db DELTA_WORKSPACE=/data/workspace ``` Persist the entire `/data` mount. Persisting only the database loses uploaded files, working artifacts, and the current self-file. Persisting only the workspace loses conversations, checkpoints, memory, events, and revisions. The checked-in Dockerfile has no `USER` instruction, so the standard image runs as root. Before a production deployment, derive an image or set a runtime user with only the permissions Delta needs, and make sure that identity owns the persistent `/data` paths. The restart policy in this example is only a local Docker baseline; a production orchestrator or service manager must also supervise the process and its persistent volume. ### Add the coding tool to an image The `code` tool degrades to a normal tool error when its executable is missing. Build a derived image when advanced coding is required: ```dockerfile FROM delta:local # Install the chosen coding CLI here, then run Delta as a dedicated user. # Provision CLI authentication at runtime rather than baking it into the image. ``` Exact installation depends on the chosen CLI. Confirm that `DELTA_CODE_CLI` resolves inside the final image and that its home-directory credentials survive VM restarts without being baked into a public layer. ### Deploy on Fly Machines `fly.toml.sample` is a topology example, not a one-command managed service. It describes: - one agent per app or Machine - one shared CPU - 512 MB starting memory, to be load-tested against the real workload - `/data` on a persistent volume - health checks against `/healthz` - no Fly Proxy service; a trusted private 6PN control plane or gateway owns access - lifecycle owned by an external controller Create an app-specific `fly.agent.toml` from this checked-in topology, replacing the app name and region: ```toml app = "delta-" primary_region = "iad" [build] dockerfile = "Dockerfile" [env] PORT = "8080" DELTA_BIND = "0.0.0.0" DELTA_DB = "/data/delta.db" DELTA_WORKSPACE = "/data/workspace" DELTA_AGENT_ID = "my-research-agent" DELTA_BUILD = "" DELTA_MODEL_PRIMARY = "anthropic/claude-sonnet-5" [[mounts]] source = "delta_state" destination = "/data" [checks] [checks.delta_health] type = "http" port = 8080 method = "get" path = "/healthz" interval = "15s" timeout = "2s" grace_period = "5s" [[vm]] memory = "512mb" cpus = 1 cpu_kind = "shared" ``` Create the Fly app and a persistent volume named `delta_state` in the configured region. Provision the provider, control, inspection, MCP, and telemetry credentials as Fly secrets. On the first boot, also provide `DELTA_SELF_MD_B64`, `DELTA_POLICY_MD_B64`, `DELTA_CONTEXT_MD_B64`, and `DELTA_VOCAB_JSON_B64` from the bundle-seeding step above, or populate `/data/workspace` through another trusted provisioning path. Without a stable `DELTA_AGENT_ID` and those bundle files, the Machine starts a neutral daemon rather than the intended agent. Deploy with `fly.agent.toml` after adapting the provider, region, tools, telemetry, and exact source commit. This private topology intentionally declares neither `[[services]]` nor `[http_service]`, so it does not create a direct Fly Proxy URL. A `[[services]]` block requires at least one `[[services.ports]]` entry; do not add an incomplete service merely to attach a health check. The top-level check reaches port 8080, and Delta must listen on `0.0.0.0` inside the Machine. Route access through the trusted private control plane, or add a deliberate public gateway using Fly's current service-port syntax. The repository sample does not automate app, volume, secret, gateway, or lifecycle provisioning. Never put secret values in the TOML file. Because the sample has no Fly Proxy service, it has no proxy auto-start or auto-stop policy. Suspend and resume through an external lifecycle controller using the Machines API. That controller must gate new traffic and avoid stopping while `/v1/busy` reports work in flight unless it deliberately accepts interruption and recovery. `/v1/busy` is the purpose-built suspend-safety signal; the [hosting contract](https://github.com/Carrara-Labs/delta-harness/blob/main/docs/hosting.md) documents the full wake-before-dispatch, busy-check-before-suspend, suspend-after-terminal lifecycle. Enforce exactly one Machine for the volume, for example by deploying without high availability and asserting the invariant in the controller. A standalone deployment without that controller should keep the Machine running. Delta itself does not provision, suspend, or resume cloud infrastructure. The 100 MB upload batch limit is not a streaming memory guarantee: multipart parsing buffers and copies request data. A 256 MB microVM can exhaust memory near the cap, especially with document extraction, coding CLIs, or subagents. Set a lower gateway upload limit or size and load-test the Machine for worst-case concurrent work. ### Back up SQLite with Litestream The entrypoint uses Litestream when either of these is configured: - a mounted `LITESTREAM_CONFIG`, default path `/etc/litestream.yml` - a direct `LITESTREAM_REPLICA_URL` Use this S3-compatible configuration. It provides one-second replication, hourly snapshots, and seven-day retention: ```yaml dbs: - path: ${DELTA_DB} replicas: - type: s3 bucket: ${LITESTREAM_S3_BUCKET} path: ${LITESTREAM_S3_PATH} endpoint: ${LITESTREAM_S3_ENDPOINT} region: ${AWS_REGION} access-key-id: ${AWS_ACCESS_KEY_ID} secret-access-key: ${AWS_SECRET_ACCESS_KEY} sync-interval: 1s snapshot-interval: 1h retention: 168h ``` Save it outside the image or use `etc/litestream.yml`. The image does not copy that file to `/etc/litestream.yml`, so mount it explicitly: ```text -e LITESTREAM_CONFIG=/etc/litestream.yml -e LITESTREAM_S3_BUCKET=your-bucket -e LITESTREAM_S3_PATH=agents/my-agent -e LITESTREAM_S3_ENDPOINT=https://your-s3-endpoint -e AWS_REGION=auto -e AWS_ACCESS_KEY_ID=your-key -e AWS_SECRET_ACCESS_KEY=your-secret -v "$PWD/etc/litestream.yml:/etc/litestream.yml:ro" ``` Add those options to the earlier `docker run` command. Keep the model, bundle seed, authentication, port, and `/data` volume options from that command. For one genuine first boot against an empty replica, also add: ```text -e DELTA_BOOTSTRAP=1 ``` Recreate the container without that option as soon as the first database exists. First-boot rules are intentionally strict: 1. If the local DB exists, Delta keeps it and Litestream starts replication. 2. If the DB is missing and a replica is configured, restore must succeed. 3. A genuine first boot against an empty replica must set `DELTA_BOOTSTRAP=1`. 4. Remove `DELTA_BOOTSTRAP=1` after the database exists. 5. If the volume is later lost while bootstrap remains enabled, the container starts blank instead of restoring. `DELTA_BOOTSTRAP` has an effect only when backup is configured and the local database is absent. An existing local database always wins, even when a replica contains newer state. A restored stale volume can therefore start and replicate from stale local data. The entrypoint has no replication-freshness watchdog, and `/healthz` remains green if backup export has stalled. Monitor Litestream's last successful sync externally and alert on lag. Litestream backs up only `DELTA_DB`. It does not back up the workspace, including the current `DELTA.md`, uploads, working files, spill files, or trash. Use volume snapshots, object-storage synchronization, or another file backup for `DELTA_WORKSPACE`. Complete recovery also needs the current rotating MCP refresh-token file when that feature is enabled; never restore a stale, already-spent token. Database, workspace, and refresh state have no coordinated point-in-time snapshot. For a consistent backup, quiesce new and active work before capturing them together, or define and test an ordering plus reconciliation procedure for database rows that refer to missing files. ### Upgrade and roll back Delta runs SQLite migrations transactionally when opening the database. It records harness and schema versions and refuses to open a database whose schema is newer than the binary understands. Before a production upgrade: 1. record `/healthz` version and `DELTA_BUILD` 2. confirm SQLite replication and workspace backup freshness 3. stop or suspend the single writer cleanly 4. take a database and workspace snapshot 5. deploy the pinned image digest 6. verify `/healthz`, a model request, required MCP tools, Cockpit access, and telemetry 7. monitor resumed runs and promotion failures There is no automatic pre-migration snapshot. A rollback after a schema-changing release may require restoring the pre-upgrade database together with a compatible workspace snapshot. ## Security model ### Put a trusted gateway in front Set both credentials in production: ```dotenv DELTA_CONTROL_TOKEN=run-api-token DELTA_INSPECT_TOKEN=separate-root-inspection-token ``` When `DELTA_CONTROL_TOKEN` is set, every `/v1/*` route except the separately gated `/v1/dev/*` routes requires `Authorization: Bearer `. `/healthz` remains public and data-light. The run API is a daemon seam for a trusted control plane, not a complete public multi-tenant API: - task lookup and cancellation are ID-based, without per-user ownership checks - `x-delta-user` is trusted only for queue projection - the request JSON endpoints have no built-in body-size limit - the daemon has no rate limiter - one control token can drive every run on that daemon The gateway should authenticate users, authorize every task and `previous_response_id`, inject one canonical user identity and the machine token, strip caller-supplied privileged metadata, cap request bodies and queue depth, rate limit, log access, and expose only required routes. It must construct review and widening claims from verified control-plane state. `POLICY.md` is model guidance and cannot replace these checks. ### Separate run access from root inspection The inspect token grants access to transcripts, prompts, memory, configuration shape, files, and selected database state. It is intentionally different from the control token. Keep Cockpit writes off unless an authenticated operator needs them: ```dotenv DELTA_INSPECT_WRITE=0 ``` For hardened agents with no inspection requirement: ```dotenv DELTA_INSPECT=off ``` Without an inspect token, `/v1/dev/*` is available only to the immediate loopback socket peer. A same-host proxy defeats the intended remote distinction unless it injects a token. The `/dev` application contains no embedded runtime data, but direct public access to it is still unnecessary when inspection is disabled. ### Treat the VM as the execution boundary The built-in file tools are workspace-confined, and model-directed `code` and subagent processes receive a narrowed service-token environment. That does not make Delta a general untrusted-code sandbox: - `read_file` can read workspace files, including a workspace `delta.env` - the delegated coding CLI has every filesystem and process capability available to the daemon's OS user; only the CLI's own sandbox can narrow it - stdio MCP servers are trusted local child processes and inherit the daemon environment plus their explicit `env` object - HTTP MCP tools can perform whatever side effects their servers authorize - subagents share the workspace Run one agent under a dedicated OS identity or isolated VM. Keep provider, control, telemetry, and MCP credentials in the process environment or secret manager, not workspace files. Add a purpose-built sandbox if arbitrary generated code must be treated as hostile. ### Protect stored state SQLite contains raw run requests, which can include `metadata.authToken`, plus messages, tool arguments and results, memory, and optional normalized call captures. Cockpit projections redact on read, but the database and its replica retain the original bytes. Use short-lived user tokens, encrypted storage, restricted backup credentials, and an explicit retention policy. Do not enable call capture by default in production. Remember that Litestream copies the sensitive database to the replica. ### Network protections `web_fetch` blocks private, loopback, link-local, and cloud-metadata destinations and revalidates redirects. Keep that protection on in production. Validation and the actual fetch perform separate DNS resolution, so a hostile hostname can still attempt a public-to-private DNS rebinding between them. Use network egress controls when SSRF is a hard security boundary. `DELTA_FETCH_ALLOW_PRIVATE=1` is an explicit local-development escape hatch and lets model-driven fetches reach private services. Subscription model tokens are sent only to exact HTTPS hosts in `DELTA_BROKER_ALLOWED_HOSTS`. Static `MODEL_HEADERS` cannot override authentication or protocol headers. MCP HTTP authorization can be replaced per run, so the MCP service must validate token audience and scope. That broker route is the exception. Static model credentials sent to `MODEL_BASE_URL`, static and per-run MCP credentials, rotating MCP refresh credentials, scheduling control tokens, and telemetry credentials do not receive an equivalent harness-enforced HTTPS host allowlist. Treat every configured endpoint as trusted operator input, require validated HTTPS outside local development, and apply outbound allowlists where credentials are valuable. Be especially careful with `TELEMETRY_URL` because it can fall back to the production control token. ## Configuration reference The values below are the current public operating surface. Unless noted otherwise, changing them requires a daemon restart. ### Process, identity, and state | Variable | Default | Purpose | |---|---|---| | `PORT` | `8080` | HTTP port. `delta dev` replaces it with the selected port. | | `DELTA_BIND` | all interfaces | Bind host. `delta dev` defaults it to `127.0.0.1`. | | `DELTA_DB` | `data/delta.db` | SQLite path. Container default is `/data/delta.db`. | | `DELTA_WORKSPACE` | `workspace` | Agent bundle and working-file root. Container default is `/data/workspace`. | | `DELTA_AGENT_ID` | unset | Stable identity for memory and telemetry. Set it in every real deployment. | | `DELTA_BUILD` | unset | Build or commit identifier returned by `/healthz`. | | `DELTA_LEASE_HOLDER` | `FLY_MACHINE_ID`, then hostname | Override the machine-scoped writer identity. Usually leave unset. | | `DELTA_LEASE_TTL_MS` | `30000` | Writer lease TTL, clamped to at least 5 seconds. | ### Primary model | Variable | Default | Purpose | |---|---|---| | `DELTA_MODEL_PRIMARY` | `anthropic/claude-sonnet-5` | Primary model. Deprecated alias: `DELTA_MODEL`. | | `DELTA_MODEL_FALLBACKS` | empty | Comma-separated model fallback list on the primary endpoint. | | `MODEL_BASE_URL` | `https://openrouter.ai/api/v1` | Provider base URL, without the final API path. | | `MODEL_API_KEY` | unset | Primary provider key. Falls back to `OPENROUTER_API_KEY`. | | `OPENROUTER_API_KEY` | unset | OpenRouter key and primary-key fallback. | | `MODEL_API` | compatible chat | `anthropic` or `responses`; any other value uses `/chat/completions`. | | `MODEL_HEADERS` | unset | JSON object of non-reserved static request headers. | | `DELTA_REASONING_EFFORT` | provider default | Main-model reasoning effort. Request metadata can override it. | | `DELTA_UTILITY_MODEL` | `anthropic/claude-haiku-4.5` | Model for compaction, reflection, and judging. Empty uses the main cascade. | | `DELTA_MODEL_PRICES` | built-in table | JSON map of model prices per million input, output, and cache-read tokens. | | `DELTA_PROVIDERS` | empty | JSON array of fallback provider objects. | | `DELTA_MODEL_TIMEOUT_MS` | `600000` | Absolute model-call ceiling. | | `DELTA_STREAM_IDLE_MS` | `60000` | Abort a model stream after this long without a network chunk. `0` disables. | | `DELTA_VISION` | auto | `1` forces vision support; `0` disables it. | | `DELTA_VISION_MODELS` | built-in regex | Custom regular expression for automatic vision detection. | `OPENAI_API_KEY` and `ANTHROPIC_API_KEY` are not read automatically as the primary key. Put the chosen value in `MODEL_API_KEY`, or reference another key with `apiKeyEnv` inside `DELTA_PROVIDERS`. ### Codex subscription broker | Variable | Default | Purpose | |---|---|---| | `DELTA_BROKER_TOKEN_URL` | unset | HTTPS endpoint that mints a short-lived Codex bearer. Deprecated alias: `DELTA_BROKER_MINT_URL`. | | `DELTA_BROKER_AUTH` | unset | Bearer sent to the broker endpoint. | | `DELTA_BROKER_ALLOWED_HOSTS` | `chatgpt.com` | Comma-separated exact hosts allowed to receive subscription tokens. | ### Tools, MCP, and context | Variable | Default | Purpose | |---|---|---| | `EXA_API_KEY` | unset | Enables `web_search`. | | `DELTA_FETCH_ALLOW_PRIVATE` | off | `1` allows `web_fetch` to private addresses. Local development only. | | `DELTA_CODE_CLI` | Codex command | Space-separated coding CLI command. No shell parsing is performed. | | `DELTA_SUBAGENT_DEPTH` | `0` | Internal depth guard. Top-level agents should keep `0`. | | `DELTA_MCP_SERVERS` | `[]` | JSON array of HTTP or stdio MCP servers. | | `DELTA_MCP_REFRESH_URL` | unset | Rotating MCP token endpoint. | | `DELTA_MCP_REFRESH_FILE` | unset | Persistent refresh-token file. | | `DELTA_MCP_REFRESH_SERVER` | unset | Name of the HTTP MCP server receiving the rotating credential. | | `DELTA_MCP_REFRESH_TOKEN` | unset | Optional first-boot refresh token. | | `DELTA_MCP_REFRESH_CLIENT_ID` | `delta-agent` | Refresh client ID. | | `DELTA_HYDRATE_TOOLS` | empty | Comma-separated product read tools called at first session run. | | `DELTA_HYDRATE_SEARCH_TOOL` | unset | Task-keyed knowledge search tool. Requires per-run user auth. | | `DELTA_CAPABILITY_SEARCH_K` | `5` | Number of capability references surfaced per run, capped internally. | | `DELTA_TOOL_TIMEOUT_MS` | `120000` | Outer ordinary-tool timeout. `0` removes it; queued search and fetch then have no separate fallback ceiling. MCP and scheduling keep internal ceilings. | | `DELTA_TOOL_RESULT_MAX_BYTES` | `20000` | Inline character cap before a full result spills to disk. | ### Identity, policy, and learning | Variable | Default | Purpose | |---|---|---| | `DELTA_SELF_MAX_TOKENS` | `800` | `DELTA.md` budget. Prompt loading estimates JavaScript characters times four; writes estimate UTF-8 bytes times four, so non-ASCII limits can differ. | | `DELTA_POLICY_MAX_TOKENS` | `800` | Maximum custom `POLICY.md`; overflow fails boot. | | `DELTA_VOCAB` | file or neutral | JSON vocabulary override with precedence over `vocab.json`. | | `DELTA_REFLECT` | off | `1` enables post-success reflection by default. | | `DELTA_MEMORY_NAMESPACE` | derived | Stable product namespace for local memory and promotion. | | `DELTA_PROMOTE_MIN_RUNS` | `2` | Distinct self-reflection occurrences before shared promotion. | | `DELTA_PROMOTE_CLAIM_TTL_MS` | `60000` | Reclaim timeout for a stuck promotion claim. | ### Profiles, context, and retention | Variable | Default | Purpose | |---|---|---| | `DELTA_PROFILE` | `work` | Placement ceiling. Validate exactly `work` or `chat`; another value currently widens to `work`. | | `DELTA_MAX_TOKENS` | profile value | Optional lower fresh-token cap. | | `DELTA_MAX_COST_USD` | profile value | Optional lower model-cost cap. | | `DELTA_COMPACT_AT_TOKENS` | `120000` | Previous-input threshold for automatic compaction. | | `DELTA_RETENTION_MS` | 7 days | Event and tool-journal age limit. With telemetry active, the ordinary event sweep is skipped. | | `DELTA_RETENTION_MAX_EVENTS` | `50000` | Event row cap when the local sweep owns events. | | `DELTA_RETENTION_MAX_JOURNAL` | `50000` | Tool-journal row cap. | | `DELTA_RETENTION_SWEEP_MS` | `3600000` | Periodic sweep interval. `0` disables periodic, not boot, sweep. | ### API, Cockpit, scheduling, and telemetry | Variable | Default | Purpose | |---|---|---| | `DELTA_CONTROL_TOKEN` | unset | Bearer for run-driving `/v1/*` routes and scheduling. | | `DELTA_CONTROL_URL` | unset | External control-plane base URL; enables scheduling tools with the token. | | `DELTA_INSPECT_TOKEN` | unset | Separate bearer for `/v1/dev/*`; otherwise loopback-only. | | `DELTA_INSPECT_WRITE` | off | `1` enables Cockpit edits and self-file revert. `delta dev` enables it. | | `DELTA_INSPECT` | on | `off` removes `/dev` and every `/v1/dev/*` route. | | `DELTA_CAPTURE_CALLS` | off | `1` stores normalized successful main-loop model request and response captures. `delta dev` enables it. | | `TELEMETRY_URL` | unset | NDJSON collector URL. | | `TELEMETRY_TOKEN` | control token fallback | Dedicated collector bearer. | | `DELTA_CAPTURE_PAYLOADS` | off | `1` keeps model and tool event attributes in export. It does not add full payloads. | ### Container backup and bundle seeding | Variable | Default | Purpose | |---|---|---| | `LITESTREAM_CONFIG` | `/etc/litestream.yml` | Explicit config path. A set but missing path fails startup. | | `LITESTREAM_REPLICA_URL` | unset | Direct Litestream replica alternative. | | `DELTA_BOOTSTRAP` | off | With backup configured and no local DB, `1` permits a fresh DB instead of requiring restore. Use only for genuine first boot. | | `DELTA_SELF_MD_B64` | unset | First-boot `DELTA.md` seed. | | `DELTA_POLICY_MD_B64` | unset | First-boot `POLICY.md` seed. | | `DELTA_CONTEXT_MD_B64` | unset | First-boot `PROMPT_CONTEXT.md` seed. | | `DELTA_VOCAB_JSON_B64` | unset | First-boot `vocab.json` seed. | The checked-in Litestream template also expects `LITESTREAM_S3_BUCKET`, `LITESTREAM_S3_PATH`, `LITESTREAM_S3_ENDPOINT`, `AWS_REGION`, `AWS_ACCESS_KEY_ID`, and `AWS_SECRET_ACCESS_KEY`. Configuration validation is intentionally uneven in the current runtime. Invalid or reserved `MODEL_HEADERS` and an invalid vision regular expression fail startup. Malformed model-price JSON falls back, while negative prices are accepted. Many positive count settings are floored or clamped to at least one, the lease TTL is clamped to five seconds, and a zero retention sweep disables only periodic work. Other numeric values use raw JavaScript `Number` conversion with limited range validation. Empty exported budget values become zero. Validate the full environment against an operator-owned schema before launching production. Removed bundle variables are ignored with a migration warning: `DELTA_PLAYBOOK_FILE`, `DELTA_STEERING_FILE`, `DELTA_CHARTER_FILES`, `DELTA_CHARTER_TOOL`, `DELTA_PLAYBOOK_MAX_TOKENS`, and `DELTA_STEERING_MAX_TOKENS`. Move their content or limits to the fixed files and current variables shown above rather than relying on those names. ## Production checklist Before serving real work: - [ ] Assign one stable `DELTA_AGENT_ID`. - [ ] Keep one daemon, one database, and one workspace per agent. - [ ] Prevent a second same-host process from opening that database, even on another port. - [ ] Enforce one live Machine or volume owner outside the best-effort writer lease. - [ ] Use a persistent volume for both database and workspace. - [ ] Seed all four non-secret bundle files before the first container or Fly run. - [ ] Set `DELTA_CONTROL_TOKEN` and put a trusted gateway in front. - [ ] Have the gateway authorize task and prior-response ownership, inject `user_id`, and strip all caller-supplied privileged metadata. - [ ] Set a different `DELTA_INSPECT_TOKEN`, or set `DELTA_INSPECT=off`. - [ ] Keep `DELTA_INSPECT_WRITE` and `DELTA_CAPTURE_CALLS` off unless explicitly needed. - [ ] Configure a real provider fallback for long-running production work. - [ ] Add exact pricing for every non-OpenRouter model used by a dollar budget. - [ ] Verify that every provider and child route reports usage; treat budgets as tracked-model loop guards, not total spend caps. - [ ] Validate `DELTA_PROFILE`, budgets, prices, headers, regular expressions, and every numeric setting before launch. - [ ] Give MCP calls the least privilege and use per-run user tokens where required. - [ ] Validate MCP discovery, framing, result shapes, and rotating-token persistence against the actual server. - [ ] Validate every model, MCP, refresh, control, and telemetry endpoint as trusted HTTPS. - [ ] Name mutating MCP tools without read-like underscore or dot segments. - [ ] Review `POLICY.md` and verify the product's human-review rail end to end. - [ ] Verify that `vocab.json` matches real MCP names and schemas. - [ ] Test workspace path, upload, image, and document behavior with production-like files. - [ ] Size or cap upload memory and queue depth at the gateway. - [ ] Install and authenticate the coding CLI only if the agent needs it. - [ ] Run the container as a dedicated non-root identity with writable persistent paths. - [ ] Test one subagent task if delegation is part of the agent design. - [ ] Send telemetry only to a validated HTTPS endpoint and deduplicate event IDs. - [ ] Classify recall and error attributes as potentially sensitive, and monitor exporter backlog and age externally. - [ ] Back up SQLite and the workspace separately. - [ ] Quiesce or reconcile unmatched database, workspace, and rotating refresh-token snapshots. - [ ] Remove `DELTA_BOOTSTRAP=1` after first boot. - [ ] Put body limits, rate limits, user authorization, and task ownership at the gateway. - [ ] Set journal retention longer than the longest possible run. - [ ] Gate new work and check queue activity before planned stop or cloud suspension. - [ ] Test a kill and restart during a tool-using task, then verify recovery behavior. - [ ] Record the image digest, harness version, schema version, and build commit. ## Troubleshooting ### `delta send` cannot connect `delta dev` chooses a free port unless `--port` is supplied. Read the launcher output and pass that port to `delta send`. The CLI always connects to localhost. The default async `delta send` creates work through the control API and then tails the inspection stream. A gated daemon therefore needs both `DELTA_CONTROL_TOKEN` and `DELTA_INSPECT_TOKEN` in the CLI process. `delta send --json` needs only the control token. `delta watch` needs the inspect token. None of these clients loads the bundle's `delta.env` automatically. ### The daemon has no usable model credential For the primary provider, use `MODEL_API_KEY` or `OPENROUTER_API_KEY`. `OPENAI_API_KEY` and `ANTHROPIC_API_KEY` are not primary-key aliases. Confirm that the direct daemon actually received the variables, because only `delta dev` loads `delta.env`. ### Direct Anthropic or Responses calls hit the wrong path Set `MODEL_BASE_URL` to the versioned base, such as `https://api.anthropic.com/v1` or `https://api.openai.com/v1`. Delta appends `/messages`, `/responses`, or `/chat/completions`. ### An MCP server or tool is missing Check that `DELTA_MCP_SERVERS` is valid JSON. Malformed JSON, a non-array value, or an unusable entry (no `name`, an `http` entry with no `url`, or a `stdio` entry with no `command`) is dropped with a specific startup warning while the daemon boots with the remaining backends. A missing `transport` is inferred from the entry shape. Restart the daemon after changes. Read boot logs for the server-specific connection failure. Confirm that the combined `__` name uses only letters, numbers, underscores, and hyphens and fits within 128 characters. If the tool is connected but not initially visible, ask the model to use `search_tools`, or add its exact suffix to `vocab.coreVerbs`. ### A bundle edit has no effect `POLICY.md`, `vocab.json`, all of `PROMPT_CONTEXT.md`, provider config, and MCP config require restart. `DELTA.md` changes apply to the next run. An existing session still carries prior conversation history, so start a new thread when testing a changed identity. ### `POLICY.md` prevents startup A custom policy over `DELTA_POLICY_MAX_TOKENS` is a fatal configuration error. Trim it or deliberately increase the budget. Delta never truncates fixed rules. ### The `code` tool returns an executable error The standard container does not include Codex or Claude Code. Install the selected CLI, confirm it is on `PATH`, set `DELTA_CODE_CLI`, and provision its home-directory authentication. Delta does not parse shell quoting, so use an executable path and arguments without spaces. ### A subagent cannot call the model or tools Children have a restricted environment and do not inherit MCP, Exa, broker, telemetry, control, or fallback-provider secrets. A broker-only parent cannot currently power a child through its broker credential. Give the parent a supported static primary key, or avoid subagent delegation for that deployment. If the child reaches the provider but selects the wrong model, set `DELTA_MODEL` to the same value as `DELTA_MODEL_PRIMARY`. The current child environment forwards the legacy name only. Routes that require `MODEL_HEADERS` are not reproducible in a child, and child usage does not use a parent `DELTA_MODEL_PRICES` override. ### A second local daemon opens the same bundle Stop it immediately. Two processes on the same host share the default lease-holder identity, and different ports bypass the port-bind collision. Never run two `delta dev` commands against one bundle. Keep exactly one process per database and workspace. ### The production Cockpit loads but shows no data The application asset contains no embedded runtime data, but its API calls need the inspect bearer, which the page does not attach itself. Use a trusted same-origin proxy that authenticates the operator and injects the correct inspect and control tokens. A `403` without an inspect token usually means the client is not loopback. A `404` can mean `DELTA_INSPECT=off`. ### Usage cost stays at zero OpenRouter can report cost directly. Other paths use Delta's price table. Add the served model ID to `DELTA_MODEL_PRICES`, including input, output, and cache-read dollars per million tokens. If tokens also remain zero, the compatible endpoint may be omitting usage; do not rely on token or dollar caps until that route reports it correctly. ### A run reports an interrupted tool The daemon restarted after arming a non-idempotent call but before recording its result. The tool may or may not have committed its side effect. Inspect the external system and the tool journal before retrying. ### A timed-out tool may still be running Delta races the configured timeout and tells signal-aware asynchronous tools to abort. A tool that ignores the signal may continue detached, while synchronous blocking work cannot be preempted at all. Verify its outcome before another mutation. ### Telemetry does not arrive in an OTLP collector Delta sends a custom newline-delimited JSON schema with a subset of GenAI-inspired fields, not OTLP. Point it at a compatible ingestion route or translate the records at the collector boundary. ### The container restores or starts incorrectly If a backup is configured and the local DB is absent, restore must succeed unless `DELTA_BOOTSTRAP=1`. Use bootstrap only for the initial empty replica, then remove it. A missing explicitly configured Litestream file is fatal. An existing local DB always wins even if stale. Workspace and rotating MCP refresh-token recovery are separate from Litestream. ### An older binary refuses the database The database schema is newer than the binary's migrations. Do not force it open. Deploy a compatible binary or restore a database snapshot taken before the schema upgrade. ## Current boundaries These are deliberate truths of the current codebase: - Human-review rails are prewired through policy, vocabulary, MCP, and reflection. Delta does not include a universal approval backend or UI. - `delta send` and `delta watch` are localhost-only clients; `delta dev` is a local launcher, not remote attachment. - MCP configuration, policy, vocabulary, and all prompt context do not hot-reload. - The writer lease blocks different machine identities but is not a same-machine mutex when processes use different ports. - A Fly sample exists, but cloud provisioning and suspend or resume lifecycle remain external control-plane work. - Litestream protects SQLite only. The workspace needs its own backup. - The run API needs a trusted gateway for end-user authentication, authorization, body limits, and rate limits. - Task, response, session, metadata identity, and review claims have no built-in tenant-ownership enforcement. - `/healthz` is liveness and version metadata, not dependency readiness. - Captured calls, conversations, and most database tables are not automatically age-pruned. - Captured calls are normalized successful main-loop calls, not raw provider traffic or complete internal-call traces. - Root inspection can directly address hidden workspace `.delta` paths; its token is root-equivalent and writes can corrupt live state. - Token and dollar budgets cover recorded model usage only and can overshoot by one call plus background work. - An invalid daemon `DELTA_PROFILE` currently falls open to `work`; deployment validation must reject it. - Four workers bound queued sessions, not sibling tools or untracked background reflections. - Shutdown does not drain tasks, reflection, or telemetry, and long-lived SSE endpoints do not impose production backpressure. - Events are operational, at-least-once observability rather than a transactionally complete audit ledger. - Subagents share the workspace and receive a restricted provider and tool environment. - Subagent model forwarding uses the legacy `DELTA_MODEL` name and omits parent headers, custom prices, and reasoning configuration. - `eval_n` is not safe for concurrent edits to shared files. - Exported model and tool events omit attributes unless `DELTA_CAPTURE_PAYLOADS=1`. - Bundled promotion adapters do not guarantee remote idempotency, and retry drain depends on a later reflected run. - Task-start hydration and capability lookup can outlive their 20-second wait and bypass the ordinary tool journal. - Reflection is best-effort background work with no durable reflection queue. - The standard container runs as root unless the deployment overrides it. - `store` is accepted for compatibility but does not turn off durable state. Within those boundaries, Delta gives a small bundle a capable execution core: start with one request, inspect the durable main loop, add the tools and learning rails the work needs, and keep the result understandable all the way to production.