# Dispatch — tasks (live progress) > **Live status + roadmap only.** Completed milestones are summarized, not > narrated. Old blow-by-blow history is pruned — it lives in git (`git log`). > Keep this lean and current; do not let it re-accrete a step-by-step changelog. ## Status (current) `tsc -b` EXIT 0 · biome clean · **784 vitest + 109 bun = 893 tests**. Built and verified live (full-fidelity: every feature is a manifest-loaded extension through the host): - **kernel** — contracts (ABI), bus, `runTurn` turn loop, extension host. - **core extensions** — storage-sqlite, auth-apikey, provider-openai-compat (OpenCode Go), conversation-store, session-orchestrator, transport-http, credential-store; tool extensions `read_file` (files + directory listing), `run_shell`, `edit_file`, `write_file`. - **observability** — structured Logger/Span ABI + journal-sink → out-of-process collector → trace-store (`bun:sqlite`); host-bin supervises the collector; nested turn→step→{prompt, provider.request, ttft, decode} spans; D5 verbatim provider capture (self-redacted); `trace-replay` record/replay lib + fixtures. - **CLI** — one-shot HTTP client (`bun packages/cli/src/main.ts`); `GET /models`, `--cwd`, `--conversation`. - **web frontend** — SEPARATE repo `../dispatch-web`. Slice 1 (surface system) shipped via `ui-contract` + `surface-registry` + `transport-ws` + `surface-loaded-extensions`. Slice 2 (browser chat) in progress there. ## How to run ```bash KEY1=$(grep DISPATCH_API_KEY_OPENCODE1 .env | cut -d= -f2) PORT=4567 DISPATCH_API_KEY="$KEY1" bun packages/host-bin/src/main.ts # boots app + collector curl -s -X POST localhost:4567/chat -H 'content-type: application/json' \ -d '{"conversationId":"c1","message":"Say hello in 3 words."}' # field = conversationId ``` Process cleanup uses the `[x]` bracket trick (ORCHESTRATOR §8) — leaked server/collector procs poison the next run's counts. ## Foundation (done — summarized; details in git) - **MVP + multi-turn:** curl → transport-http → session-orchestrator → host/registry → provider → OpenCode Go → AgentEvents → NDJSON; `conversationId` threads history. - **Post-MVP:** auth→provider seam; `read_file` tool (live tool-dispatch loop); `getHostAPI()` hygiene; `tabId → conversationId` rename. - **Observability Phase A/B:** the substrate + collector/store + supervision + replay fixtures (see bullet list above). - **CLI MVP:** credential-store + transport-contract + cli; model catalog; cwd threading; multi-turn. - **FE Slice 1:** the surface system across both repos (live WS probe verified). - **FE Slice 2 backend prereqs:** `@dispatch/wire` split; per-chunk `seq` cursor; read endpoint `GET /conversations/:id?sinceSeq=`; WS chat-deltas (transport-ws); turn-lifecycle events (`turn-start`/`done`/`turn-sealed`); step grouping (`stepId` on tool chunks/events); live stream metrics (`step-complete` + `usage`/`done` token/timing — "Pass 1"); CORS. ## Metrics — token + timing (current milestone) - [x] **Pass 1 — live stream metrics** (done): `step-complete` event + `usage`(stepId) + `done`(durationMs + aggregate usage). - [x] **Observability spans** (done): turn & step span-close stamp all four `Usage` fields (added cacheRead/cacheWrite; normalized `usage_*` → `usage.*`). - [x] **Pass 2 — persisted replay metrics** (done, was deferred): `StepMetrics`/ `TurnMetrics` wire types; conversation-store `appendMetrics`/`loadMetrics` (separate key space, turn-append order); session-orchestrator accumulates per-step+turn metrics from the event stream and persists after seal; transport-http `GET /conversations/:id/metrics` → `ConversationMetricsResponse`. `@dispatch/wire` + `@dispatch/transport-contract` → `0.4.0`. Commit `6db12ff`. - [x] **Live-verified end-to-end** (against flash): live `step-complete`/`done` metrics ↔ persisted `GET /conversations/:id/metrics` byte-match (aggregate + per-step `stepId` + ttft/decode/genTotal + durationMs); journal turn/step spans carry dotted `usage.*` incl. `usage.cacheReadTokens` (the #2 fix). - [x] **FE courier handoff** written: `frontend-metrics-pass2-handoff.md` (in this repo; user couriers to `../dispatch-web`; ORCHESTRATOR §7). ## dedup / storage growth (DONE) Design `notes/observability-design.md` §12. User-gated calls: extend existing pipeline (no new ext); scope = **de-dup + retention/rotation** (D9 roll-ups deferred); dedup = **content-addressed bodies** (body-hash, NOT fingerprint-gated). - [x] **Wave 1 — `trace-store`**: content-addressed `bodies` table (SHA-256), at-rest gzip (>1 KiB), `prune(policy)` (age + drop-oldest byte-cap + orphan GC) / `RetentionPolicy` / `PruneSummary` / `DEFAULT_RETENTION` (7d/256MiB); reads transparent. - [x] **Wave 2 — `observability-collector`**: pure `shouldPrune` cadence helper; `main.ts` calls `store.prune(DEFAULT_RETENTION)` on a coarse cadence (`--prune-interval-ms`, default 60s; host-bin-overridable), log-and-continue on error. - [x] Glossary: added content-addressed body, trace retention, prefix fingerprint, warm vs real. - [x] **Migration bug** (found by live boot, fixed): Wave 1 created the `idx_records_bodyHash` index BEFORE running `migrateOldBodies`, so opening a pre-existing OLD-schema `traces.db` crashed the collector (`no such column: bodyHash`, crash-looped). Fix = reorder migration before the index + 3 regression tests that seed a real old-schema DB. bun 106→109. - Tests: bun 89→109. typecheck/biome clean. **Live-verified** against a real old-schema `traces.db`: 0 crashes, collector stays up, schema migrates (bodyHash + content-addressed bodies), real-data dedup (318 body refs → 270 stored bodies), prune cadence fires cleanly (14× `prune completed`). Optional follow-up: host-bin env-override for the retention policy. ## Standard tools — fs + shell (DONE) User-gated calls: **one tool per extension** (matches `tool-read-file` precedent); tools are **standard** tier (a turn completes with `tools:[]`, §2.6/§2.8). **Zero ABI change** — the `ToolContract`/`ToolExecuteContext` already carry `signal`/`onOutput`/`cwd`/`log`. - **Wave 1 (parallel, disjoint pkgs, kernel-only dep) — all green:** - [x] `tool-read-file` — EXTENDED `read_file` to list directory contents (sorted, `/`-suffixed subdirs; files unchanged). 41 tests. - [x] `tool-shell` (new) — `run_shell`: foreground, streamed via `ctx.onOutput`, `ctx.signal` cancel, `ctx.cwd`, timeout + output cap, `concurrencySafe:false`; injected `spawn`. 31 tests. - [x] `tool-edit-file` (new) — `edit_file`: `oldString`/`newString`/`replaceAll`; errors on absent/non-unique/identical; workdir-contained; `concurrencySafe:false`. 38 tests. - [x] `tool-write-file` (new) — `write_file`: explicit `overwrite` flag (absent+unset→create; exists+unset→error; exists+true→overwrite; absent+true→error); no parent auto-create. 33 tests. - **Wave 2 (done):** orchestrator added 3 root tsconfig refs + `bun install`; host-bin owner registered the 3 new extensions in `CORE_EXTENSIONS` (same pattern as `read_file`). - **Live-verified:** clean boot (`Dispatch booted`, collector up, no activation/capability-gate error — the new `shell` capability is accepted); full-graph `tsc -b` EXIT 0, biome clean. - **Recovery notes (scar tissue):** `tool-write-file` first returned plan-only (§5a) → re-summoned with "IMPLEMENT NOW". `tool-edit-file` hung vitest at collection — `computeReplacement` infinite- looped on empty `oldString` (`"".indexOf("") === 0`, index never advances) invoked at a test's `describe` scope; fixed with an early empty-string guard + validation. One agent deleted `ORCHESTRATOR.md` out-of-lane → caught by post-wave `git status`, restored from git. - Deferred (not selected): `glob`, `grep`/`search_code`, background shells. ## Skill system + load_skill tool (DONE) User-gated calls: skills list lives in the **`load_skill` tool definition** (NOT the system prompt), refreshed **per new turn** (cache-stable across steps), **live file read** on execute. One `skills` standard extension (loader + filter + tool). Skill = md in `.skills/`; discovered from `~/.skills` + `/.skills` (cwd shadows home); name = filename w/o `.md`. Format: line1 = summary, line2 = `---`, body = line3+; on load the first two lines are stripped; malformed (no `---`) = no summary but still loadable. Glossary: added `skill`, `skill summary`, `tools filter`. - **Mechanism — the per-turn `tools` filter chain** (first concrete use of the §3.2 context-assembly chain; reusable for persona/agents later): - [x] **kernel** — exposed `HostAPI.applyFilters` (delegates to the bus's existing `applyFilters`). - [x] **session-orchestrator** — defines+exports `toolsFilter`/`ToolAssembly`; applies it ONCE per turn (injected `applyToolsFilter` dep) before `runTurn`, threading `cwd`+`conversationId`. - [x] **skills** (new ext, `dependsOn session-orchestrator`) — pure parse/merge/render + `load_skill` tool (live read, strips first two lines, path-contained) + a `toolsFilter` filter that rewrites `load_skill`'s description + `name` enum with the per-cwd catalog. 42 tests. - [x] **host-bin** — registered `skills` in `CORE_EXTENSIONS`. - [x] **Fan-out (§5.3):** `applyFilters` was a required `HostAPI` addition → broke one consumer (transport-http `server.bun.test.ts` inline HostAPI stub) → fixed by its owner. - **Live-verified:** clean boot (`skills` activates, filter registered, no crash); full-graph `tsc -b` EXIT 0, biome clean. (End-to-end load_skill via a real LLM turn not yet exercised — unit/integration tests cover the filter rewrite + live read.) ## Cache warming (core DONE; control surface PARTIAL) User-gated calls: target the external **Claude** provider (`../claude` provider-anthropic, loaded via `DISPATCH_EXTERNAL_EXTENSIONS`); warm-assembly lives in **session-orchestrator** (`warm()` reuses the real turn's assembly → byte-identical prefix, provider-agnostic); **surface system** for controls; **per-conversation** controls; interval default 4 min, free value. Old-code invariants honored (primary-model/full-prefix via reuse; refuse mid-turn; never persist/emit; in-flight invalidation; arm-on-settle/cancel-on-start; `pct = round(clamp(cacheRead/input,0,1)*100)`). - **Mechanism (2nd use of bus hooks; first event-hook emit):** - [x] **kernel** — exposed `HostAPI.emit` (delegates to bus.emit), counterpart of `on`. - [x] **session-orchestrator** — `turnStarted`/`turnSettled` event hooks (carry conversationId/cwd/ modelName) emitted per turn; `warm()` service (`cacheWarmHandle`) reusing assembly, refusing mid-turn, never persisting/emitting; returns Usage. - [x] **cache-warming** (new ext) — per-conversation timers (arm/cancel/in-flight token), calls `warm()`, computes `lastPct`, persists `{enabled,intervalMs}` (default on/240s) in host.storage; registers a controls Surface. 19 tests. - [x] **host-bin** — registered cache-warming; **transport-http** HostAPI stub fixed for `emit`. - **Manual trigger endpoint:** `POST /chat/warm {conversationId, model?, cwd?}` → `WarmResponse` `{inputTokens,outputTokens,cacheReadTokens,cacheWriteTokens,cachePct}` (409 if generating). Powers a FE "warm now" button + fast tests. Types in `@dispatch/transport-contract`; route in transport-http. - **LIVE-VERIFIED against Claude haiku:** automatic timer warm → journal `warm complete pct:100`; manual `POST /chat/warm` → `cacheReadTokens:6799, cachePct:100` (100% hit), HTTP 200. The external `../claude` provider-anthropic is loaded via `bin/up` (`DISPATCH_EXTERNAL_EXTENSIONS`). - **Surface framework extended (DONE):** added `NumberField` to `ui-contract` + per-conversation surface scoping (optional `conversationId` on subscribe/unsubscribe/invoke + surface/update; new `SurfaceContext` on `SurfaceProvider.getSpec/invoke`; transport-ws keys subscriptions by `(surfaceId, conversationId)` and tags updates). cache-warming now serves a PER-CONVERSATION surface: `Toggle`(enabled) · `Number`(interval, seconds, `cache-warming/set-interval`) · `Stat`(last cache %). All backward-compatible (global surfaces like `surface-loaded-extensions` unchanged). **FE courier:** `frontend-cache-warming-handoff.md` (this repo) — the web must render the `number` field kind + send/handle `conversationId` on the surface WS protocol. ## Open items - **`prefix.fingerprint` / `warm|real` cache-bust attributes (deferred):** decoupled from dedup by the content-addressed decision; also gated on cache-warming being built (not yet) so `warm|real` can't be honestly stamped. Later cache-bust-debug milestone (`notes/observability-design.md` §3.1, §12). - **D9 analytics roll-ups (deferred):** rollup table shape + `GROUP BY` indexes + retention asymmetry + periodic rollup job (`notes/observability-design.md` §2 D9, §12). The scheduler mechanism (`host.scheduler.register`) already exists. - **D8 `prompt.assembly` segments:** deferred-by-design (await the context-filter chain). ## Roadmap 1. **CLI** — done. 2. **Web frontend** (in progress, SEPARATE repo `../dispatch-web`; Svelte + DaisyUI, same methodology). Slice 2 = browser chat MVP consuming the wire/transport-contract + metrics. Cross-repo contract changes are couriered via the user (ORCHESTRATOR §7); `lsp references` does not span repos. 3. **dedup / storage growth** — after the frontend (see Open items).