| Age | Commit message (Collapse) | Author |
|
cross-cutting verified
FE confirmed whole-tree green (typecheck 0/0, 795/795 tests, biome clean, build
OK, git clean). All three handoffs GREEN with no integration gaps:
- provider-retry: yellow alert-warning bubble renders w/ countdown.
- SSH #1 wire types: defaultComputerId + Computer/ComputerEntry resolve.
- SSH #2 computer API: full src/features/computer/ feature wired + typecheck-clean.
Cross-cutting verified: provider-retry is WS-stream (TranscriptState.providerRetry
→ ChatView), computer is HTTP-only (AppStore.computerId → ComputerField sidebar) —
disjoint state/channels/regions/mount-keys; no collision. SSH support + provider-
retry integration is complete and validated end-to-end on both repos.
|
|
Brings dev's retry-with-backoff (the transient `provider-retry` AgentEvent the
web frontend consumes) + the LSP-dead-server per-edit-hang fix into the SSH
feature branch, alongside the SSH waves 0-5c.
All code files auto-merged cleanly (run-turn.ts, orchestrator.ts, runtime.ts,
wire/index.ts, tool-edit-file/extension.ts, run-turn.test.ts — both computerId
threading and retry-with-backoff coexist). Only tasks.md conflicted (status
section — orchestrator-resolved; both feature sections kept).
Verified post-merge: tsc -b EXIT 0, biome clean (391 files), 1730 vitest pass
+6 sshd-integration skipped (was 1690; +40 from dev's retry/LSP tests).
Wire dist rebuilt so the FE can re-sync the pinned @dispatch/wire dep and pick
up BOTH provider-retry AND the SSH Computer/defaultComputerId types.
No merge or push (into dev or otherwise).
|
|
When the upstream LLM API returns a retryable error (HTTP 429 / 5xx
"overloaded"), the kernel now retries provider.stream() with a stepped
backoff, visibly, until the 8h cumulative-sleep budget is exhausted — then
emits the final error and seals the turn. Retries fire only when no content
was emitted yet this step (safety invariant: never duplicate partial output).
- wire: new transient TurnProviderRetryEvent AgentEvent variant (emitted
before each sleep; not persisted to model history).
- kernel contracts: RetryStrategy (pure delayFor + injected sleep) + optional
retry? on RunTurnInput (omit = no retry, backward-compatible).
- kernel run-turn: retry loop in executeStep; providerRetryEvent constructor.
Kernel imports no timer (sleep injected).
- session-orchestrator: concrete schedule (5s..30m, repeat 30m, 8h budget) +
abortable setTimeout sleep, wired into RunTurnInput.retry.
tsc -b EXIT 0; biome clean; 1574 vitest pass (+16 new: 11 kernel retry tests
with injected fake sleep + pure delayFor, zero @dispatch/* mocks; 5 schedule
tests). Transports unchanged (transport-ws forwards AgentEvent verbatim in
chat.delta; transport-http is generic JSON.stringify).
Plan: notes/retry-with-backoff-plan.md. tasks.md updated with milestone +
optional CLI-renderer roadmap follow-up.
|
|
barrel
Wave 5c (final wiring) of transparent SSH support.
- host-bin: register exec-backend + ssh in CORE_EXTENSIONS (exec-backend before
the tool extensions that dependsOn it; ssh after, provides the remote-backend
factory + ComputerService at boot). +@dispatch/exec-backend/@dispatch/ssh deps +
tsconfig refs.
- transport-http: CR-5 — re-export computerServiceHandle + ComputerService type
from the package barrel (src/index.ts), mirroring lsp/mcp handles, so ssh imports
the typed symbol cleanly (no more dist/seam.js subpath workaround).
- orchestrator: added the @dispatch/exec-backend dep the host-bin agent missed +
bun install.
LIVE-VERIFIED: bun packages/host-bin/src/main.ts boots clean ('Dispatch booted',
no disabled extensions) — exec-backend + ssh + all tool extensions load together.
Verified: tsc -b EXIT 0, biome clean, 1690 vitest pass (+6 sshd-integration skipped).
DEFERRED (CR-6): listComputers usageCount stays 0 until a conversation-store
count-by-alias helper is added (non-blocking).
Refs: notes/ssh-support-plan.md. No merge or push.
|
|
Wave 5b of transparent SSH support. NEW standard extension @dispatch/ssh makes
remote execution actually work over SSH, transparently. ssh2 verified to run under
Bun (load-bearing decision #1 confirmed: connects to local sshd :22 + execs).
- config.ts: ~/.ssh/config reader via ssh-config -> Computer[]/ComputerEntry[]
(read-only discovery; resolves hostName/port/user/identityFile/knownHost).
- hostkey.ts: known_hosts auto-trust-and-pin (present->verify/reject-on-mismatch,
absent->accept+append; the accept-new analog).
- errors.ts: pure ssh2/SFTP -> node:fs-style .code error mapping (so tools'
existing ENOENT branches work unchanged).
- pool.ts: SshConnectionPool (per-alias ssh2.Client, lazy connect, keep-alive,
idle reap ~15m); key-only auth from ~/.ssh (config IdentityFile or default
id_ed25519/id_rsa); no agent-forwarding, no PTY.
- backend.ts: SshExecBackend implements ExecBackend (spawn via client.exec with
shell-quoted cwd; fs via SFTP).
- service.ts + extension.ts: activate provides BOTH handles the other units
consume — remoteExecBackendFactoryHandle (exec-backend: computerId->SshExecBackend)
AND computerServiceHandle (transport-http: listComputers/getComputer/getStatus/test).
- orchestrator: added packages/ssh to root tsconfig.json refs + bun install.
Tests: 45 pass + 6 sshd-integration skipped (it.skipIf(!process.env.SSH_TEST_HOST)).
Verified: tsc -b EXIT 0, biome clean, 1690 vitest pass (was 1641, +49).
CRs for wave 5c: host-bin registration; CR-5 transport-http barrel re-export;
CR-6 usageCount wiring (deferred-ok, defaults to 0).
Refs: notes/ssh-support-plan.md (decisions §0.5/§13). No merge or push.
|
|
Wave 4 of transparent SSH support (3 parallel owner-agents on disjoint packages).
- transport-http: computer routes — GET /computers, GET /computers/:alias,
GET /computers/:alias/status, POST /computers/:alias/test (all delegate to a
new ComputerService seam, graceful []/disconnected when ssh not loaded);
GET/PUT/DELETE /conversations/:id/computer; PUT /workspaces/:id/default-computer
(mirror the cwd/default-cwd routes); /chat threads computerId into the
orchestrator. Defines ComputerService interface + computerServiceHandle
(defineService<ComputerService>('ssh')) in seam.ts — the seam the ssh package
provides via host.provideService in wave 5.
- transport-ws: chat.send + chat.queue thread computerId onto the route result
(mirrors cwd/workspaceId), forwarded to the orchestrator input.
- mcp: CR-1 fix — filterMcpTools now preserves computerId on the returned
ToolAssembly (mirrors cwd preservation), so the filter chain stays consistent.
- orchestrator: added @dispatch/wire dep to transport-http (build/config, my lane)
so its seam.ts Computer/ComputerEntry import resolves.
Verified: tsc -b EXIT 0, biome clean, 1641 vitest pass (was 1620, +21).
Refs: notes/ssh-support-plan.md (decisions §0.5/§13). No merge or push.
|
|
transport-contract API types
Wave 3 of transparent SSH support (2 parallel owner-agents on disjoint packages).
- session-orchestrator: thread computerId end-to-end through the turn, mirroring
cwd exactly — StartTurnInput/EnqueueInput/handleMessage/TurnLifecyclePayload
gain computerId; runTurnDetached resolves effectiveComputerId via
conversationStore.getEffectiveComputer(convId, override), persists the override,
threads into RunTurnInput + ToolAssembly. Register a remote-degradation
tools-filter (filterRemoteIncompatibleTools) that, when assembly.computerId is
set (REMOTE), drops the 'lsp' tool + any '__'-namespaced MCP tool (local
processes that can't see remote files); LOCAL (computerId undefined) is a
passthrough — byte-identical to today. +21 tests.
- transport-contract: + computerId on ChatRequest (flows to ChatSendMessage) +
computer endpoint API types (ComputerListResponse, ComputerResponse,
ComputerStatusResponse, SetConversationComputerRequest,
ConversationComputerResponse, SetWorkspaceDefaultComputerRequest,
TestComputerResponse) — mirrors the cwd/workspace endpoint types.
- CR-1 (non-blocking, folded into wave 4): MCP filter doesn't preserve computerId
on the returned ToolAssembly.
- cache-warming computerId threading intentionally DEFERRED (user request) —
noted as a known performance-only limitation in tasks.md.
Verified: tsc -b EXIT 0, biome clean, 1620 vitest pass (was 1599, +21).
Refs: notes/ssh-support-plan.md (decisions §0.5/§13). No merge or push.
|
|
Wave 2 of transparent SSH support (4 parallel owner-agents on disjoint
tool packages). The tools now resolve an ExecBackend per-call from
ctx.computerId and call backend.spawn / backend.readFile / etc. instead of
node:fs and node:child_process directly — so they are transport-agnostic
(local now; remote over SSH later, transparent to the agent). Still LOCAL-ONLY
this wave (computerId always undefined -> LocalExecBackend, behavior-identical).
- tool-shell: factory takes resolveBackend; execute calls backend.spawn.
spawn.ts DELETED (realSpawn was a verbatim duplicate of exec-backend's
LocalExecBackend.spawn — logic moved to the sanctioned shared package).
manifest dependsOn:[exec-backend]; host.getService at activation.
- tool-read-file: readFile/stat/readdir -> backend.* (pure logic untouched;
ENOENT .code branches kept).
- tool-write-file: exists/stat/writeFile -> backend.* (pure logic untouched).
- tool-edit-file: readFile/writeFile -> backend.* + forward-compatible REMOTE
diagnostics skip (ctx.computerId set -> skip LSP, return empty — plan §6.1;
local path byte-identical to today). LSP lookup stays lazy.
- orchestrator: pre-wired @dispatch/exec-backend dep into the 4 tool
package.jsons + bun install (build/config, my lane) so isolated verify
resolved cleanly; agents added the ../exec-backend tsconfig ref.
Verified: tsc -b EXIT 0, biome clean, 1599 vitest pass (was 1592).
Refs: notes/ssh-support-plan.md (decisions §0.5/§13). No merge or push.
|
|
Wave 1 of transparent SSH support (parallel owner-agents on disjoint packages,
plus the orchestrator-authored kernel contract seam from wave 0):
- packages/wire: + Computer/ComputerEntry (read-only view over ~/.ssh/config
Host aliases) + Workspace.defaultComputerId (string|null, null=local). Types
only; 3 conformance tests.
- packages/exec-backend (NEW core extension): the ExecBackend abstraction
(spawn + minimal fs surface) the bundled tools will program against instead
of node:fs/child_process. LocalExecBackend wraps today's node calls
(behavior-identical; node:fs-style .code errors). execBackendHandle +
ExecBackendResolver (sync; computerId undefined -> local; set -> throws until
the ssh package wires remote resolution in wave 5). 20 tests.
- packages/kernel (runtime only): thread computerId through dispatch.ts +
run-turn.ts exactly as cwd is threaded (opaque, forwarded to
ToolExecuteContext; absent = local = byte-identical to today). +2 tests.
- packages/conversation-store: computer (SSH alias) assignment + resolution
mirroring cwd — WorkspaceRow.defaultComputerId + setWorkspaceDefaultComputerId
+ getComputerId/setComputerId/clearComputerId + getEffectiveComputer
(override -> per-conv -> workspace default -> null/local). Fixes the 3
Workspace literal sites the new required wire field broke. +18 tests.
- orchestrator: root tsconfig.json ref for exec-backend + bun install.
Verified: tsc -b EXIT 0, biome clean, 1592 vitest pass (was 1549, +43).
Refs: notes/ssh-support-plan.md (decisions §0.5/§13). No merge or push.
|
|
- MCP live-verified: test MCP server → tool discovery (test__ping) → tool call
→ pong result. Full turn lifecycle confirmed on production server.
- Per-edit diagnostics live-verified: type error in .ts file surfaces
[TypeScript Language Server] ERROR (2322) inline after edit.
- edit_file bug found + fixed during live-verify (lazy LSP lookup).
|
|
stale HANDOFF.md
- tasks.md: record per-edit LSP diagnostics auto-append milestone (commit
8f6114b), fix test count 1453→1468
- HANDOFF.md: retire stale post-MVP handoff (referenced arch-rewrite path,
178 tests, next-steps all done) → current accurate pointer file
|
|
Mark all 5 live-verify checkboxes as done (reasoning effort, todo tool,
CLI cross-client, abort-race, system-prompt builder). Slim the roadmap
from 11 items down to 3 open items (web frontend, close-with-queued-messages
product decision, FE crash-recovery status endpoint) by dropping the 8
completed/verified items.
|
|
reconcile() only repaired orphaned tool-calls. Two other broken states made
chats uncontinuable, and load() had no parse-error guard:
- A trailing assistant message whose only chunk is 'error' (a failed-
generation marker) serializes to empty content -> provider rejects/empty
-> chat never continues. 6 of 140 production conversations were stuck.
- A tool-call whose input is a raw malformed-JSON string (model emitted
broken JSON) re-sent as OpenAI arguments -> provider 400s on every
continuation (the 77574596 break).
- load() JSON.parse had no try/catch -> one corrupt row bricked the chat.
Fix = read-time repair (no DB surgery; append-only preserved). reconcile
runs on every load() BEFORE any provider sees messages, so Layer 1
protects ALL providers.
Layer 1 (conversation-store reconcile): strip error chunks from assistant
messages + drop the now-empty error-only messages (safe: never followed by
a tool message); orphaned-tool-call synthesis unchanged; ReconcileReport
+2 additive counts. loadSince (FE reads) intentionally unreconciled so the
user still SEES the error. load() wraps JSON.parse in try/catch (skip
corrupt rows).
Layer 2 (openai-stream): serializeToolArguments ensures tool-call
arguments is always valid JSON (malformed string -> fallback object),
neutralizing already-stored malformed args.
Layer 2 equiv (../claude provider-anthropic): safeJson returns a valid
object fallback on parse failure, not the raw string. (Separate repo.)
Live-verified: reproduced 77574596's real broken tail in the dev DB;
POST /chat continued it cleanly (no 400, model replied) — the provider
accepted the reconciled history.
tsc -b EXIT 0, biome clean, 1453 vitest pass.
|
|
shadow warning
All five live-verify checks passed against the dev stack (bin/up :24203):
configSource reaches the wire (built-in TS, 'built-in'); broken server reports
error + configSource + source-named error; recovery without restart (blocker,
error->connected after config fix); no retry storm; shadow warning logged via
host.logger when both configs declare lsp.
|
|
Two issues found by decompiling the running dispatch-server binary
(handoff from a ruby-lsp setup in raylib-jamstack):
Issue 2 (blocker): a failed LSP server was "broken" FOREVER — the
manager's broken set was cleared only in shutdownAll(), so a server
that failed (bad env, missing binary, or a since-fixed config) stayed
state:"error" for the whole process. For an agent running *inside*
dispatch the only recovery (server restart) kills its own session.
Now a broken server self-heals when its resolved config changes since
it was marked broken (discrete event → no retry storm), with a bounded
backoff for transient failures.
Issue 1: .dispatch/lsp.json silently shadowed opencode.json's lsp key
with no warning and no source attribution. Now: shadow warning via
host.logger when both declare lsp; configSource populated on status
(.dispatch/lsp.json / opencode.json / built-in); spawn-failure error
strings name the config source.
Contract: additive configSource?: string on LspServerInfo
(@dispatch/transport-contract 0.20.0→0.21.0). transport-http passes it
through to the wire (was a field-by-field map that dropped it — CR
resolved by the transport-http owner).
tsc -b EXIT 0, biome clean, 1443 vitest pass.
|
|
|
|
|
|
- @dispatch/transport-contract 0.18.0 -> 0.19.0:
add workspaceId: string to ConversationOpenMessage and ConversationStatusChangedMessage
- session-orchestrator: include persisted workspaceId in conversationOpened/
conversationStatusChanged payloads
- transport-ws: forward workspaceId in WS broadcasts
- transport-http: POST /conversations/:id/open resolves workspaceId before emit
- FE handoff to 29ae: frontend-workspace-open-handoff.md
|
|
|
|
GET /conversations/:id/lsp was calling getEffectiveCwd directly, which falls
through to serverDefaultCwd (process.cwd()) when no conversation cwd is set.
Now gates on getCwd first: returns {cwd:null, servers:[]} when no cwd
persisted; only resolves via getEffectiveCwd + calls lspService.status when
a persisted cwd exists.
PUT /conversations/:id/cwd now accepts optional workspaceId — validates with
isValidWorkspaceSlug, then ensureWorkspace → setWorkspaceId → setCwd (assigns
the workspace before persisting cwd, so getEffectiveCwd resolves relative
cwds against the workspace defaultCwd, not the server default).
transport-contract 0.16.0→0.17.0 (additive SetCwdRequest.workspaceId;
LspStatusResponse.cwd comment updated).
1332 vitest pass.
|
|
|
|
|
|
Wire 0.12.0: Workspace, WorkspaceEntry, ConversationMeta.workspaceId
Transport-contract 0.16.0: workspaceId on ChatRequest/QueueRequest/ChatQueueMessage;
workspace endpoint types (EnsureWorkspaceRequest, WorkspaceResponse, etc.)
Kernel: re-export Workspace/WorkspaceEntry from contracts
Conversation-store: workspace persistence + service methods (getWorkspace,
ensureWorkspace, setWorkspaceTitle, setWorkspaceDefaultCwd, deleteWorkspace,
listWorkspaces, getWorkspaceId, setWorkspaceId, getEffectiveCwd, isValidWorkspaceSlug);
listConversations filter by workspaceId; forkHistory/replaceHistory preserve
workspaceId. 111 tests pass.
FE handoff: frontend-workspaces-handoff.md (courier doc)
18 typecheck errors in session-orchestrator/transport-http/cli test fakes
(expected fan-out — fixed in Wave 2+3).
|
|
|
|
ModelInfo (kernel contract):
- Add contextWindow?: number field
OpenAI-stream listModels:
- Parse contextWindow from common field names (context_length,
context_window, max_context_length, max_tokens)
Transport-contract:
- ModelsResponse: add optional modelInfo map (model name → { contextWindow? })
- Add ModelMetadata type
- Rename CompactThresholdResponse → CompactPercentResponse
- Rename SetCompactThresholdRequest → SetCompactPercentRequest
Credential store:
- Add getModelInfo(modelName) method — resolves full ModelInfo
(including contextWindow) for a <credential>/<model> string
Transport-http:
- GET /models now includes modelInfo with contextWindow per model
- Rename compact-threshold endpoints → compact-percent
Session-orchestrator:
- Auto-compact now uses contextSize (not overcounted usage.inputTokens)
compared against contextWindow * (percent / 100)
- Default percent: 85 (was flat 350000)
- resolveModelInfo dep added to look up contextWindow
- Passes modelName from the settled turn to the compaction service
Conversation store:
- Rename getCompactThreshold/setCompactThreshold → getCompactPercent/setCompactPercent
- compactThresholdKey → compact-percent key
|
|
Also adds bin/sync-env script for updating system env keys.
|
|
|
|
Roadmap items 9 and 10:
- Tab persistence: active/idle/closed conversation lifecycle status,
restore tabs across devices on browser connect.
- Compacting: automatic (token threshold setting) + manual (button click),
summarize old history to reclaim context window.
|
|
|
|
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.
|
|
endpoints)
|
|
handoff pending
|
|
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).
|
|
|
|
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.
|