| Age | Commit message (Collapse) | Author |
|
When a provider doesn't include a usage field in the SSE stream, the
span attributes (usage.inputTokens, usage.outputTokens) are now absent
instead of defaulting to 0. This makes it clear in the journal that the
provider didn't report usage, rather than looking like 0 tokens were
used.
|
|
Without stream_options.include_usage, OpenAI-compatible providers omit
the usage field from the SSE stream entirely. Umans returned 0 tokens
for everything; OpenCode's proxy happened to include usage without it.
Now both providers return proper prompt_tokens + completion_tokens.
Note: Umans does not report cache_read_tokens or
prompt_tokens_details.cached_tokens — cache hit rate will be 0% for
Umans regardless. This is a provider limitation, not a parsing issue.
|
|
dispatch <model> --text "..." --open now starts a new conversation AND
signals the frontend to open the tab — no need for a separate
'dispatch send --open' step.
|
|
Bun.file() returns an empty MIME type for .js files, causing the browser
to reject module scripts with strict MIME checking. Added an explicit
MIME type map for common static file extensions (.js, .css, .html, .svg,
.woff2, etc.).
|
|
bin/build: compiles standalone binaries (dispatch-server + dispatch CLI)
via bun build --compile, builds the frontend static bundle with
VITE_HTTP_PORT=24991 + VITE_WS_PORT=24990, copies to dist/web/.
bin/install: installs binaries to /usr/bin/, frontend to
/usr/share/dispatch/web/, systemd service to /etc/systemd/system/,
config to /etc/dispatch/env, data dirs to /var/lib/dispatch/ +
/var/log/dispatch/. Enables + starts the dispatch systemd service.
Supports --uninstall and --no-build flags.
systemd/dispatch.service: Type=simple, reads /etc/dispatch/env,
restarts on failure, logs to journald.
systemd/dispatch.env: template config (ports 24991 HTTP + 24990 WS,
DISPATCH_WEB_DIR, API key, data paths).
transport-http: optional webDir static file serving — unmatched GET
requests fall through to Bun.file() serving with SPA index.html
fallback. Gated on DISPATCH_WEB_DIR env var (backward compatible).
|
|
scanSkillsDir now recurses into subdirectories (e.g. ~/.skills/general/,
~/.skills/tech/), not just the top level. The load_skill execute path
also searches recursively for the named .md file. Duplicate names are
deduped (first found wins; top-level before nested). 42 tests pass.
|
|
CLI gains three new sub-commands:
- dispatch list [--server] — list conversations (short ID + title + activity)
- dispatch read <id> [--server] — block until turn settles, print last AI message
- dispatch send <id> --text [--queue] [--open] [--cwd] [--effort] [--server]
- Default: blocking (consumes NDJSON stream, prints accumulated text + conv ID)
- --queue: non-blocking (POST /conversations/:id/queue, exit immediately)
- --open: signals frontend to open the conversation tab (POST /conversations/:id/open)
Short-ID resolution: 4+ char prefix → GET /conversations?q= → resolve to full ID.
32+ char input is treated as a full UUID (no resolution). Errors on 0 or >1 matches.
48 new tests (108 total in cli). Pure arg parser + HTTP client functions, zero vi.mock.
|
|
transport-http: GET /conversations (list with ?q= prefix filter),
GET /conversations/:id/last (blocks until turn settles, returns last AI
text), POST /conversations/:id/open (emits conversationOpened hook),
PUT /conversations/:id/title (set title). emit threaded from host.emit.
extractLastAssistantText pure helper. 21 new tests (166 total).
transport-ws: subscribes to conversationOpened hook, broadcasts
ConversationOpenMessage to all connected WS clients. 2 new tests.
session-orchestrator: conversationOpened hook descriptor (exported).
|
|
Implement listConversations(), getConversationMeta(), setConversationTitle()
on the ConversationStore. Auto-track createdAt (first write), lastActivityAt
(every append), and title (first user message, truncated 80 chars). A
conv-index key tracks all conversation IDs. 21 new tests (81 total).
|
|
Additive contract changes for the CLI milestone (roadmap items 2 + 4):
@dispatch/wire 0.8.0 → 0.9.0:
- ConversationMeta { id, createdAt, lastActivityAt, title }
@dispatch/transport-contract 0.12.0 → 0.13.0:
- ConversationListResponse, LastMessageResponse, OpenConversationResponse
- SetTitleRequest, TitleResponse
- WS conversation.open broadcast (additive to WsServerMessage)
ConversationStore interface:
- listConversations(), getConversationMeta(), setConversationTitle()
- Stub implementations in real store + 11 test fakes (Wave 1 fills in)
Transport-http manifest: new routes declared
(GET /conversations, GET /conversations/:id/last,
POST /conversations/:id/open, PUT /conversations/:id/title)
|
|
- Always write the full transcript to /tmp/dispatch/youtube-transcribe/{video_id}.txt
(not just on truncation)
- Description no longer claims to return the full transcript; instead says
it returns transcript text (truncated if very long) and the full version is
always saved to the file path
|
|
truncation
When the formatted transcript exceeds the 50K char output cap, the tool now
writes the full output to /tmp/dispatch/{video_id}.txt and returns the
truncated output with a notice pointing to the file path. The writeFile dep
is injectable so tests verify without touching the filesystem.
|
|
convention
Leaner tool description and queued response — no longer instructs the model
to append URLs to a pending file. The tool just returns status, ETA, and URL.
|
|
New standard tool extension backed by a self-hosted transcriber service
(http://100.102.55.49:41090, Tailscale, no API key). One tool
youtube_transcript — fetches transcripts for YouTube videos. Returns
completed (full text + timestamped segments), queued/processing (position
+ ETA + .youtube_subtitles_pending retry convention), or failed (error).
Pure core: validateUrl + format* functions + truncateOutput. Injected
edge: TranscriptClient (injectable fetchFn, AbortSignal.any for
cancellation). concurrencySafe true, capabilities network. 30 tests.
Verified: tsc EXIT 0, 1152 vitest, biome clean (327 files). Boot smoke
clean.
|
|
New standard tool extension with a single todo_write tool (opencode
todowrite pattern: full-list replace, returns JSON, no business-rule
enforcement — the description guides the model). Per-conversation in-memory
state + per-conversation surface (rendererId: todo, scope: conversation)
via subscriber-notify (message-queue pattern).
Wave 0 (kernel contract): added conversationId?: string to ToolExecuteContext
(additive, backward-compatible). Wired in dispatch.ts — the kernel already
had it but wasn't passing it through to tools.
Wave 1 (todo extension): pure core (validateTodos — shape only; getTodos/
setTodos/clearTodos; buildTodoSpec; formatTodoResult). Shell:
createTodoWriteTool + surface provider. Tool description matches opencode's
todowrite.txt depth (when-to-use, examples, task states). Priority field
removed (bloats the tool with little value). 25 tests.
Wave 2 (host-bin): registered todo in CORE_EXTENSIONS + dep + root tsconfig ref.
Verified: tsc EXIT 0, 1123 vitest, biome clean (314 files). Boot smoke clean.
FE handoff: frontend-todo-handoff.md.
|
|
New standard tool extension with one tool web_search supporting 4 modes
(search, scrape, crawl, map) against a self-hosted Firecrawl instance.
Pure core: validateArgs (discriminated union by mode) + format* functions
+ truncateOutput. Injected edge: FirecrawlClient (injectable fetchFn/sleep/now,
AbortSignal.any for per-request timeout + caller cancellation). concurrencySafe
true, capabilities network. 38 tests, zero vi.mock.
Live-verified: umans-glm-5.2 called web_search → real Firecrawl results (also
the first live Umans API call).
|
|
Extract a generic @dispatch/openai-stream library from provider-openai-compat
(convert-messages, convert-tools, parse-sse, listModels, stream, provider),
parameterizing createOpenAICompatProvider with uid=1000(tradam) gid=1000(tradam) groups=1000(tradam),966(docker),968(ollama),998(wheel) + hook.
Refactor provider-openai-compat to import from the lib (byte-identical behavior).
New @dispatch/provider-umans extension wraps the Umans OpenAI-compatible backend
(https://api.code.umans.ai/v1). Self-contained: reads UMANS_API_KEY from env
directly (no auth-apikey dep). transformBody maps reasoningEffort →
reasoning_effort (capping xhigh/max → high). Dynamic listModels via GET /v1/models.
host-bin: registered provider-umans in CORE_EXTENSIONS + umans credential
(gated on UMANS_API_KEY — the credential is the model-catalog index).
Verified: tsc EXIT 0, 1059 vitest, biome clean (293 files). Boot smoke:
umans models appear in GET /models (7 models live).
|
|
A per-conversation message queue (new message-queue extension) holds user
messages enqueued while a turn generates; delivered mid-turn as steering at the
tool-result boundary (or carried to a new turn if no tool call fires).
- kernel: RunTurnInput.drainSteering callback (generic; kernel stays pure)
- wire 0.7.0->0.8.0: QueuedMessage, QueuePayload, TurnSteeringEvent (additive)
- transport-contract 0.11.0->0.12.0: POST /conversations/:id/queue + chat.queue WS op
- message-queue ext: queue state + per-conversation custom surface (rendererId message-queue)
- session-orchestrator: enqueue facade + drainSteering wiring + post-seal carry
- transport-http/ws: queue endpoint + chat.queue op (fixes WsClientMessage exhaustive switch)
- host-bin: register message-queue
1043 vitest + 199 transport bun pass; tsc/biome clean; boot smoke clean.
FE courier: frontend-message-queue-handoff.md.
|
|
beforeSeq/limit)
Coerce sinceSeq to a non-negative integer lower bound in loadSince (omitted/0/
non-positive/non-integer/NaN/Infinity -> 0; valid as-is). The transport layer
400s these upstream, but loadSince stays total for direct callers. Byte-identical
to the prior ?? 0 for the only values any caller ever passed.
58 bun tests pass.
|
|
threaded to providers
- conversation-store: get/setReasoningEffort (own key space, mirrors cwd)
- session-orchestrator: resolveReasoningEffort (override -> stored -> 'high'),
StartTurnInput.reasoningEffort, warm() parity (cache-safe)
- transport-http: /chat validation (400 on bad level) + GET/PUT
/conversations/:id/reasoning-effort
- transport-ws: chat.send threading + validation
- cli: --effort <low|medium|high|xhigh|max>
993 vitest + 189 bun tests green; typecheck + biome clean.
|
|
ProviderStreamOptions/ChatRequest fields, per-conversation GET/PUT types
wire 0.6.1->0.7.0, transport-contract 0.10.0->0.11.0. Additive only; typecheck+biome clean.
|
|
/conversations/:id
Selection sinceSeq < seq < beforeSeq; newest-limit window, ascending; positive-
integer validation (400, store never sees an invalid window); 1-based gap-free
seq codified as the contractual has-older mechanism (no earliestSeq field).
transport-contract 0.9.0->0.10.0, wire 0.6.0->0.6.1 (doc-only).
conversation-store +8 tests, transport-http +20; 935 vitest + 112 bun green.
Live-verified: 6/6 probe checks OK. FE courier: frontend-history-windowing-handoff.md
|
|
conversation close (+CR-1 table, CR-2 scope)
CR-4a: warming defaults OFF (opt-in per conversation); re-enabling restores
the persisted interval.
CR-4b: re-arm BEFORE surface notify so post-warm updates carry the FUTURE
nextWarmAt; turnSettled/turnStarted now also push (fresh schedule after seal,
null while generating).
CR-4c: POST /conversations/:id/close — per-turn AbortController wired to the
kernel runTurn signal (partial persist + normal seal, done.reason "aborted"),
new conversationClosed hook, cache-warming disables sync + persists OFF.
Disconnect/chat.unsubscribe semantics unchanged.
CR-4d: no change needed — initial surface echo already at HEAD (stale up2 boot
on the FE probe).
CR-1: loaded-extensions emits a single custom rendererId:"table" field
(TablePayload exported; Name|Version|Trust|Activation, all trust tiers).
CR-2: SurfaceCatalogEntry.scope?: "global"|"conversation" on both surfaces.
Contracts: ui-contract 0.1.0→0.2.0, transport-contract 0.8.0→0.9.0 (additive).
907 tests pass (+13); live-verified against bin/up (warms @5s with future
nextWarmAt; mid-turn close → abortedTurn:true + done.reason aborted).
Courier: frontend-cache-warming-lifecycle-handoff.md.
|
|
A pure watcher (subscribed but not the sender) couldn't see the user prompt
until the turn sealed: the user message was only persisted at seal and never
entered the live/replayable stream. Add an additive TurnInputEvent
{type:"user-message", conversationId, turnId, text} to the AgentEvent union and
emit it via the broadcast/buffer path as the first event of every turn, so it is
replayed to all subscribers (live + late-join) and on the HTTP path. Persistence
and metrics unchanged; the union widening breaks no exhaustive switch.
- @dispatch/wire 0.5.0->0.6.0; @dispatch/transport-contract 0.7.0->0.8.0 (re-export)
- session-orchestrator: emit user-message at runTurnDetached start; +3 tests,
3 Wave-1 tests updated (user-message precedes turn-start)
- FE courier: frontend-cr3-user-message-handoff.md
Live-verified vs flash: watcher receives user-message (correct text) as its first
chat.delta before turn-sealed. 894 vitest + transport bun green; tsc -b EXIT 0.
|
|
A turn no longer dies when its WebSocket connection closes. The turn-broadcast
hub moves into the core (session-orchestrator): turns run detached, persist at
seal regardless of clients, and fan out AgentEvents to N subscribers per
conversation with in-flight buffer replay for late-joiners. transport-ws stops
aborting turns on socket close and gains chat.subscribe/chat.unsubscribe so a
second device (or a reloaded browser) can watch a running turn.
- @dispatch/transport-contract 0.6.0->0.7.0: chat.subscribe/chat.unsubscribe WS ops
- session-orchestrator: startTurn/subscribe/isActive; persistent subscribers +
per-turn buffer (two-map model); handleMessage = convenience wrapper (no signal)
- transport-ws: per-connection chat-subscription fan-out; no turn-abort-on-close
- transport-http: test fakes updated for the widened interface (runtime unchanged)
- design notes/turn-continuity-design.md; FE courier frontend-turn-continuity-handoff.md
Live-verified vs flash (2-client WS): sender disconnect mid-turn -> other client
streams to done + turn persists; late-join replays turn from turn-start. 891 vitest
+ transport bun green; tsc -b EXIT 0; biome clean.
|
|
contextSize = the turn's FINAL step inputTokens+outputTokens (true context
occupancy; NOT the aggregate usage, which sums per-step prompts and overcounts
multi-step turns). Stamped on both the live done event (kernel) and persisted
TurnMetrics (session-orchestrator); a client reads the latest turn's value.
- @dispatch/wire 0.4.0->0.5.0: optional contextSize on TurnDoneEvent + TurnMetrics
- @dispatch/transport-contract 0.5.0->0.6.0 (re-export only)
- glossary: context size (reserve 'context window' for the model limit, later)
- FE courier: frontend-context-size-handoff.md
881 vitest pass; tsc -b EXIT 0; biome clean.
|
|
cache bust
LSP + per-conversation CWD feature:
- new bundled `lsp` extension: hand-rolled JSON-RPC codec (framing/rpc), lazy
one-server-per-(serverID,root), per-cwd config resolution, on-demand `lsp` tool
- `conversation-store`: getCwd/setCwd (cwdKey); `session-orchestrator` defaults a
turn's cwd from the store
- `transport-http`: cwd + lsp status endpoints; wire types in transport-contract
- host-bin: register lsp; config wiring
Cache-warming fix (the warm read 0% on the first reheat after a message):
- warm assembled tools under a different cwd than the real turn (a reheat sends no
cwd, and the warm service had no store fallback). The skills filter rewrites the
cwd-sensitive `load_skill` description, so the tools block — the first bytes of
the prompt-cache prefix — diverged and the cache missed entirely. Warm now
resolves cwd as opts.cwd ?? conversationStore.getCwd(), mirroring handleMessage.
- capture warm sends as `provider.request` spans flagged `warm:true` (thread a
child logger into providerOpts) so warm vs real bodies are diffable (obs §3.1).
- kernel logger: span-close now merges child-bound attrs like span-open, so a
`warm:true` query finds the closed span (with usage/status), not just the open.
Tests: warm forwards a warm-flagged logger; warm falls back to stored cwd; logger
open/close attr consistency. Full suite green (873).
|
|
nextWarmAt/lastWarmAt surface
FE CR-3 (backend-handoff-cache-warming-timer.md). The inversion: session-orchestrator's
warm() (the single chokepoint for manual /chat/warm AND the automatic timer) emits a
warmCompleted bus event; cache-warming subscribes and does ALL post-warm handling. So a
manual warm now re-arms the timer + refreshes the surface with NO transport-http change
(core can't depend on the standard cache-warming ext).
- session-orchestrator: warmCompleted event hook + emit from warm() on success
- cache-warming: warmCompleted subscriber unifies result handling (manual + automatic);
adds nextWarmAt/lastWarmAt state + a custom 'cache-warming-timer' surface field
- fix: createWarmService was missing the emit dep (deps.emit?. silently no-oped) →
wired it + made emit REQUIRED so it can't regress
Live-verified vs claude haiku: manual POST /chat/warm now logs cache-warming 'warm
complete' ~2s after the turn (not the 4-min timer) → manual warm reaches the warmer.
800 vitest + 109 bun green; tsc -b 0; biome clean.
|
|
The Claude cache % read 100% whenever anything was cached, because the metric's
denominator (inputTokens) excluded cached tokens on Anthropic. Fixed upstream in
../claude/provider-anthropic (inputTokens = total prompt); this commit adds the
companion retention metric and exposes it:
- transport-contract: WarmResponse += expectedCacheRate
- transport-http: POST /chat/warm returns expectedCacheRate = cacheRead/(cacheRead+cacheWrite)
- cache-warming: computeExpectedCacheRate + a per-conversation 'cache retention' surface stat
- handoff: documents the fix + cache-rate vs expected-cache (cross-turn) for the FE
Live-verified vs claude haiku: real turn cache rate 61% (was inflated 100%);
warm within TTL expectedCacheRate=100%, after expiry=0%.
|
|
cache-warming controls
Extend the surface framework so cache-warming exposes per-conversation controls:
- ui-contract: add NumberField (settable free-value numeric) to SurfaceField;
add optional conversationId to subscribe/unsubscribe/invoke + surface/update
- surface-registry: SurfaceContext { conversationId? } on getSpec/invoke (backward-compatible)
- transport-ws: thread conversationId; key subscriptions by (surfaceId, conversationId);
tag surface/update replies with conversationId
- cache-warming: per-conversation surface — Toggle(enabled) + Number(interval seconds,
cache-warming/set-interval) + Stat(last cache %); drop the currentConversationId closure
Global surfaces (surface-loaded-extensions) unchanged. 784 vitest + 109 bun = 893 tests;
tsc -b EXIT 0; biome clean.
|
|
A frontend 'warm now' button (and fast tests) can trigger a warm on demand
instead of waiting for the automatic timer.
- transport-contract: WarmRequest / WarmResponse wire types
- transport-http: POST /chat/warm → cacheWarmHandle.warm(); 200 with cachePct,
409 when the conversation is generating, 400 on missing conversationId
Live-verified vs claude haiku: seed turn cacheWrite=6799 → POST /chat/warm
returns cacheReadTokens=6799 cachePct=100 (100% hit). 760 vitest + 109 bun green.
|
|
Backend-driven warming targeting whatever provider a conversation uses (incl. the
external Claude provider-anthropic). Core engine + on/off + last-cache-% done;
interval-as-view-control pending a ui-contract NumberField (surface-system gap).
Mechanism:
- kernel: expose HostAPI.emit (typed bus event emit; counterpart of on)
- session-orchestrator: turnStarted/turnSettled event hooks (conversationId/cwd/model);
warm() service (cacheWarmHandle) reusing the real-turn assembly (byte-identical prefix,
provider-agnostic), refuses mid-turn, never persists/emits, returns Usage
- cache-warming (new ext): per-conversation timers (arm on settle, cancel on start,
in-flight invalidation), calls warm(), pct=round(clamp(cacheRead/input,0,1)*100),
persists {enabled,intervalMs} (default on/240s), registers a controls surface
- host-bin: register cache-warming; transport-http: HostAPI stub +emit (fan-out)
Honors old-code invariants. 760 vitest + 109 bun = 869 tests; tsc -b EXIT 0; biome clean.
|
|
Skills are markdown in .skills/ dirs (~/.skills + <cwd>/.skills, cwd shadows home;
name = filename). Format: line1 summary, line2 ---, body line3+; load strips the
first two lines; malformed = no summary but still loadable.
Mechanism (first use of the context-assembly filter chain, §3.2):
- kernel: expose HostAPI.applyFilters (delegates to bus.applyFilters)
- session-orchestrator: define/export toolsFilter + ToolAssembly; apply once per turn
before runTurn (cache-stable across steps), threading cwd + conversationId
- skills (new ext): pure parse/merge/render + load_skill tool (live read, path-contained)
+ a toolsFilter filter rewriting load_skill's description + name enum per cwd
- host-bin: register skills in CORE_EXTENSIONS
- transport-http: fix HostAPI test stub for the new applyFilters method (fan-out)
734 vitest + 109 bun = 843 tests; tsc -b EXIT 0; biome clean; clean live boot.
|
|
Four standard-tier tool extensions (one tool per extension, zero ABI change):
- tool-read-file: read_file now lists directory contents (sorted, /-suffixed subdirs)
- tool-shell: run_shell (foreground, streamed, cancellable, cwd, timeout + output cap)
- tool-edit-file: edit_file (oldString/newString/replaceAll; errors on absent/non-unique)
- tool-write-file: write_file (explicit overwrite flag)
Registered in host-bin CORE_EXTENSIONS. Live boot clean (shell capability accepted).
686 vitest + 89 bun = 775 tests; tsc -b EXIT 0; biome clean.
|
|
Wave 1 created idx_records_bodyHash BEFORE migrateOldBodies ran, so opening a
pre-existing old-schema traces.db crashed the collector with 'no such column:
bodyHash' (crash-looped 168x in ~20s). Fresh DBs hid it (CREATE TABLE already
has bodyHash); only a real old-schema DB exposed it.
- reorder schema(): migrateOldBodies (ALTER ADD bodyHash + content-address
backfill + drop old bodies) runs BEFORE the bodyHash index.
- add 3 regression tests that seed a real old-schema DB and open it.
Live-verified: old-schema traces.db migrates on boot with 0 crashes; 318 body
refs collapse to 270 content-addressed bodies; prune cadence fires cleanly.
typecheck EXIT 0; biome clean; bun 106->109, 0 fail.
|
|
Wave 2 (final) of the dedup/storage-growth milestone (notes §12).
- pure shouldPrune(now,lastPruneAt,intervalMs) cadence helper (injected clock).
- main.ts calls store.prune(DEFAULT_RETENTION) on a coarse cadence
(--prune-interval-ms, default 60s; host-bin-overridable), far less frequent
than a drain. Prune errors are logged and never stop the tail loop.
- confirmed body inserts flow through trace-store's content-addressed path.
- glossary: content-addressed body, trace retention, prefix fingerprint,
warm vs real.
typecheck EXIT 0; biome clean; vitest 576; bun 100->106, 0 fail.
|
|
Wave 1 of the dedup/storage-growth milestone (notes §12).
- bodies table is now content-addressed (SHA-256 hash key); identical verbatim
bodies (cache-warming resends, any repeat) collapse to one stored row,
referenced by hash from records. Transparent to insert/read callers.
- at-rest gzip compression for bodies >1 KiB (node:zlib), decompressed on read.
- prune(policy): age-based delete + drop-oldest byte-cap eviction + orphan-body
GC. Exports RetentionPolicy/PruneSummary/DEFAULT_RETENTION (7d / 256 MiB).
typecheck EXIT 0; biome clean; vitest 576; bun 89->100, 0 fail.
|
|
New throughput-store extension records one token-weighted sample per turn
(model, output tokens, pure generation time = Σ step genTotalMs) into a
day-bucketed KV store, and aggregates per-model tok/s = Σtokens / Σgen-seconds
over a day/week/month (server-local boundaries; week = ISO Mon–Sun).
transport-http records a sample per turn (logged) and serves
GET /metrics/throughput?period=day|week|month&date=<...>. The response is typed
as transport-contract's ThroughputResponse, so store/wire drift is a compile
error. Pure period + aggregate logic fully unit-tested.
|
|
executeStep built the stream opts with only the logger, so providerOpts.model
(the selected model) never reached any provider — each fell back to its own
default. Carry providerOpts through StepContext into the per-step stream opts,
plus a regression test asserting the model is forwarded.
|
|
Add loadExternalExtensions(): fault-isolated dynamic import of out-of-repo
extensions declared via DISPATCH_EXTERNAL_EXTENSIONS. main.ts assembles the
credential-store in boot() so a 'claude' credential is registered when an
external anthropic provider is loaded; config.ts surfaces the anthropic model /
credential-key settings those extensions read.
|
|
Load-time history repair was invisible (createConversationStore got no logger).
Now: optional logger injected (extension passes host.logger); reconcile logic
moved into pure reconcileWithReport() returning a ReconcileReport (reconcile()
stays a thin byte-identical wrapper); load() emits a reconcile.repair span
(childed with conversationId, flat attrs repairedCount/firstRepairedToolCallId)
ONLY when a real repair occurs. No contract fan-out (factory is package-internal).
typecheck EXIT 0, biome clean, 550 vitest (+4) + 89 bun.
|
|
spans + persisted replay)
Two-part token-data improvement:
#2 Observability spans (kernel run-turn): turn & step span-close now stamp
ALL four Usage fields — added usage.cacheReadTokens/cacheWriteTokens (were
silently dropped) and normalized usage_* -> usage.* to match the
provider.request span (consistent D9 GROUP BY). No contract change.
#3 Persisted replay metrics (conversation-store + read endpoint): new
StepMetrics/TurnMetrics wire types; conversation-store persists per-turn
metrics in a separate key space (appendMetrics/loadMetrics, turn-append
order); session-orchestrator accumulates per-step+turn metrics from the
event stream (pure metrics.ts) and persists after seal; transport-http
serves GET /conversations/:id/metrics -> ConversationMetricsResponse.
Contracts: @dispatch/wire + @dispatch/transport-contract bumped 0.3.0->0.4.0
(additive). GLOSSARY: turn metrics / step metrics.
typecheck EXIT 0, biome clean, 546 vitest + 89 bun = 635 tests.
|
|
Expose the backend's authoritative token+timing metrics on the live AgentEvent
stream (observability-only -> now also client-facing). All additive/optional.
- [email protected]: new TurnStepCompleteEvent (type:step-complete) with per-step
ttftMs/decodeMs/genTotalMs; usage += stepId; tool-result += durationMs (exec);
done += durationMs (turn wall-clock) + usage (turn total). RunTurnInput += now?.
[email protected] (re-export bump).
- kernel-runtime: when now injected, measures + emits the above (reuses the
ttft/decode first-token detection); omits timing gracefully without a clock.
- session-orchestrator: adds now? to deps, threads into RunTurnInput; extension
activate injects () => Date.now().
- transport/cli/host-bin: untouched (verbatim pass-through; additive fields).
FE handoff: frontend-metrics-handoff.md. typecheck clean; 520 vitest + 89 bun;
biome 0/0. Replay/persistence = deferred Pass 2 (documented in tasks.md).
|
|
Split each step's generation into a ttft span (stream start -> first text|reasoning
token) and a decode span (first token -> stream end), children of the step span.
decode = generation total - TTFT; both retrievable from the trace-store. First token
counts reasoning deltas; a step with no content token ends ttft with firstToken:false
(no misleading decode). Span-based (no clock injection), no wire/contract change.
+3 runtime tests. GLOSSARY: TTFT + decode time.
typecheck clean; 512 vitest; biome 0/0.
|
|
tool calls
Expose a per-step grouping key so a client can render a model's batched/parallel
tool calls (those emitted in one step) as one unit, on both the live stream and
replayed history. Key = branded StepId, derived turnId#stepIndex (0-based).
- [email protected]: required stepId on Turn{Tool,ToolResult}Event; optional stepId on
Tool{Call,Result}Chunk (generation provenance on the chunk, not the StoredChunk
envelope — StoredChunk unchanged). [email protected] (re-export bump).
- kernel-runtime: mint stepId per step; stamp on tool chunks + tool events.
- conversation-store: chunk-carried stepId round-trips append/load/loadSince for
free; reconcile copies it onto synthesized (interrupted) results.
- cli: stepId added to event test fixtures (renderer unchanged).
typecheck clean; 509 vitest + 89 bun; biome 0/0. FE courier reply + reference
snapshots regenerated in ../dispatch-web.
|
|
2 handoff)
Unblock the browser frontend (Vite origin :24204 -> HTTP backend :24203):
- transport-http: wildcard CORS via hono/cors on all routes
(Access-Control-Allow-Origin: *, Allow-Methods GET/POST/OPTIONS,
Allow-Headers Content-Type) + OPTIONS preflight (204). Headers present on
the streamed POST /chat NDJSON response too. +4 app.fetch tests.
- wire / transport-contract / ui-contract: 0.0.0 -> 0.1.0 as the FE-consumable
baseline (semver convention §2.9: major = cross-repo fan-out signal).
Verified live: OPTIONS /chat -> 204 with CORS headers; GET /models -> 200 with
Access-Control-Allow-Origin: *. typecheck clean, 502 vitest + 89 bun, biome clean.
|
|
log scope)
Make transport-http a full-fidelity extension that runs its own Bun.serve
inside activate(host) — symmetric with transport-ws. The Hono app is now built
with the extension-scoped host, so all HTTP edge logs are correctly attributed
extensionId=transport-http instead of the host-bin __host__ scope (verified
live in the journal).
- transport-http: createTransportHttpExtension() factory; activate builds the
app + Bun.serve, reads host.config httpPort (?? 24203); deactivate stops it.
- host-bin: drops the HTTP Bun.serve + createServer call; config.ts maps
BACKEND_PORT/PORT -> httpPort. host-bin now serves no transport (both
transports self-serve); boot log -> 'Dispatch booted'.
- +5 bun lifecycle tests wired into test:bun.
No contract change (composition wiring). Verified live: HTTP serves on :24203;
journal edge logs now scoped transport-http. typecheck clean, 498 vitest + 89
bun, biome clean.
|
|
gap #2)
Both HTTP + WS transport edges now emit structured logs via the injected
logger (D7-compliant: no per-AgentEvent/chat.delta frame logging). Verified
live — the journal contains the edge records.
- transport-ws: connection open/close (debug), chat.send accepted (info),
surface-op + malformed-chat.send (warn), abort-on-close (debug). +4 bun tests.
Correctly scoped extensionId=transport-ws (owns its Bun.serve).
- transport-http: /chat accepted (info) / 400 (warn) / turn-failure (error),
GET /conversations read (info), /models + store failure (error). +4 vitest.
Known follow-up: transport-http edge logs are attributed to '__host__' (not
'transport-http') because host-bin runs the HTTP server via createServer(getHostAPI())
rather than the extension owning its Bun.serve. Logs are captured + correlated;
only the per-extension filter is mis-scoped. Tracked in tasks.md.
typecheck clean, 498 vitest + 84 bun, biome clean.
|
|
Close a gap found live: neither transport emitted turn-start/done/turn-sealed
(the wire defined them; nothing fired them). turn-sealed is the FE's
cache-commit signal (frontend-design §6.3); done ends the stream.
- kernel-runtime: runTurn emits turn-start first and done (with finishReason)
last, on every exit path (stop/tool-calls/max-steps/error/aborted).
- session-orchestrator: emits turn-sealed after conversationStore.append
succeeds (the kernel touches no DB, so the post-persist seal is the
orchestrator's). Not emitted if append throws.
No contract change (all three wire types already existed). Verified live: HTTP
/chat and WS chat both stream turn-start … done turn-sealed.
typecheck clean, 494 vitest + 80 bun, biome clean.
|
|
Add chat WS ops (chat.send / chat.delta / chat.error) + unified
WsClientMessage/WsServerMessage unions to @dispatch/transport-contract
(imports ui-contract; surface protocol unchanged — additive non-colliding
type variants, no channel wrapper). transport-ws drives
sessionOrchestrator.handleMessage, streaming each AgentEvent as chat.delta
over the same connection that carries surface ops; per-connection
AbortController cancels in-flight turns on socket close; error-isolated.
Verified live: one WS connection delivered the surface catalog AND a real
flash chat turn (chat.delta stream, reply 'Hello my friend').
Completes the FE Slice 2 backend prereqs. typecheck clean, 485 vitest + 80 bun,
biome clean.
Discovered (separate, pre-existing): runtime does not emit
turn-start/done/turn-sealed on either transport — needed for FE cache-commit;
tracked in tasks.md.
|