| Age | Commit message (Collapse) | Author |
|
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.
|
|
Additive types for GET /conversations/:id/mcp status endpoint, mirroring the
existing LSP status types. McpServerState, McpServerInfo, McpStatusResponse.
+2 type-test assertions. Version bump 0.21.0 → 0.22.0.
Handoff written: frontend-mcp-status-handoff.md (backend route + FE consumption).
|
|
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.
|
|
A chat's selected provider + model is now persisted per conversation (like cwd
and reasoningEffort). Opening a conversation in a new browser recalls the
originally selected model instead of defaulting.
- transport-contract 0.19.0→0.20.0: ModelResponse + SetModelRequest types
for GET/PUT /conversations/:id/model.
- conversation-store: getModel/setModel (model:<id> key, mirrors
getReasoningEffort/setReasoningEffort); forkHistory copies model; empty
string clears.
- session-orchestrator: resolve model from persisted store when no per-turn
override; persist the resolved model so it sticks; warm path parity.
- transport-http: GET/PUT /conversations/:id/model endpoints with validation.
1433 vitest pass; tsc + biome clean.
|
|
- @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
|
|
New @dispatch/system-prompt extension (standard tier):
- Pure parser: [type:name] variables, [if]/[else]/[endif] conditionals,
negated [if !...], nested blocks, unmatched-tag pass-through.
- Variable resolver (injected adapters): system:time/date/os/hostname,
prompt:cwd/model/conversation_id, git:branch/status, file:<path> (dynamic).
- Service handle: construct (resolve+persist) + get (cached, cache-safe).
- Default template: persona + AGENTS.md if exists + cwd.
- 52 tests (parser 29, resolver 12, catalog 3, service 8).
transport-contract 0.17.0→0.18.0: SystemPromptTemplateResponse,
SetSystemPromptTemplateRequest, SystemPromptVariable, SystemPromptVariablesResponse.
Design: notes/system-prompt-design.md (caching constraint, compaction
integration, wave plan). 1384 vitest pass.
|
|
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
|
|
compactedFrom
Reworked compaction to match the confirmed design:
- The compacted conversation KEEPS its original ID (messaging between
agents is unaffected — the ID never changes)
- The old full history is forked to a new archive conversation (new UUID)
- The archive inherits the source's compactedFrom, creating a chain:
A → Y → X (walk compactedFrom backward)
- A's history is replaced with [summary + recent N]
- A.compactedFrom = archive ID
forkHistory: inherit compactedFrom from source (not set to sourceId),
so archives chain backward to previous archives.
FE: no tab switching needed — the ID doesn't change. Just reload history.
|
|
Compaction now preserves the full pre-compaction history:
1. Forks the conversation to a new archive ID (complete copy: chunks,
metadata, cwd, reasoning-effort). Archive gets status=closed,
title='Archive: <original>', compactedFrom=<originalId>.
2. Replaces the original conversation's history with [system: summary]
+ recent N messages (same as before).
3. Sets compactedFrom=<archiveId> on the original conversation's metadata.
The original history is never destroyed. The archive is accessible via
GET /conversations/:id using the archive ID.
Wire/contract changes:
- ConversationMeta: add compactedFrom?: string
- CompactionResult: add archiveId: string
- ConversationCompactedMessage: add archiveId
- CompactResponse: add archiveId
Conversation store:
- forkHistory(sourceId, targetId): copies all chunks + metadata to a
new conversation ID
- setCompactedFrom(conversationId, archiveId): marks the conversation
|
|
Implement roadmap item 10: conversation compaction to reclaim context
window without losing the thread.
Wire (0.11.0):
- Add CompactionResult type
- Add ConversationCompactedMessage WS event
Transport-contract (0.15.0):
- Add CompactResponse, CompactThresholdResponse, SetCompactThresholdRequest
- Add ConversationCompactedMessage to WsServerMessage union
- Re-export CompactionResult
Conversation-store:
- replaceHistory: delete all chunks, reset seq, append new messages
- getCompactThreshold / setCompactThreshold (per-conversation setting)
- compactThresholdKey added to keys.ts
Session-orchestrator:
- CompactionService interface + compactionHandle
- conversationCompacted hook descriptor
- createCompactionService: load history, split old/recent, call provider
to summarize, replaceHistory with [system: summary] + recent N
- Auto-trigger: resolveCompaction lazy dep, fires after turn settles
(checks threshold, non-blocking)
- Hook declared in manifest contributes.hooks + services
Transport-http:
- POST /conversations/:id/compact (manual trigger)
- GET /conversations/:id/compact-threshold (read setting)
- PUT /conversations/:id/compact-threshold (set setting)
Transport-ws:
- Subscribe to conversationCompacted hook
- Broadcast conversation.compacted WS message
CLI:
- dispatch compact <conversationId> command
FE handoff: frontend-compaction-handoff.md
|
|
Implement roadmap item 9: tab persistence across devices.
Wire (0.10.0):
- Add ConversationStatus type (active | idle | closed)
- Add status field to ConversationMeta
Transport-contract (0.14.0):
- Add conversation.statusChanged WS message to WsServerMessage union
- Re-export ConversationStatus
Conversation-store:
- Track status in ConversationMetaRow (default: idle)
- getConversationStatus / setConversationStatus methods
- listConversations accepts { status: ConversationStatus[] } filter
- Old meta rows without status default to idle on read
Session-orchestrator:
- conversationStatusChanged hook descriptor
- Emit on transitions: idle→active (turn start), active→idle (turn settle),
→closed (closeConversation)
- Persist status to store as fire-and-forget side effect
- Declare hook in manifest contributes.hooks
Transport-ws:
- Subscribe to conversationStatusChanged hook
- Broadcast conversation.statusChanged WS message to all clients
Transport-http:
- GET /conversations?status=active,idle filter (parseStatusFilter pure helper)
- POST /conversations/:id/close now sets status to closed
CLI:
- dispatch list defaults to active,idle (excludes closed)
- --status <state> flag to filter by single status
- --all flag to include closed
FE handoff: frontend-conversation-lifecycle-handoff.md
|
|
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)
|
|
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.
|
|
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%.
|
|
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.
|
|
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.
|
|
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).
|
|
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.
|
|
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.
|
|
endpoint
Incremental rehydration endpoint for long-lived clients. Returns
ConversationHistoryResponse { chunks: StoredChunk[], latestSeq } — the RAW,
append-order, seq-filtered slice from conversation-store.loadSince, NOT
reconciled (reconcile conflicts with the per-chunk seq cursor, so it stays on
the turn path; the read path is a pure sync primitive).
- transport-contract: add ConversationHistoryResponse + StoredChunk re-export.
- transport-http: GET /conversations/:id route reaching the log directly via
conversationStoreHandle (dependsOn conversation-store); pure parseSinceSeq
(absent->0, invalid->400).
- build wiring: conversation-store dep + project ref.
FE Slice 2 backend prereq (read-side). typecheck clean, 481 vitest, biome clean.
|
|
split (B2)
FE slice 1 — backend-declared, frontend-agnostic surface system (verified live): new types-only @dispatch/ui-contract (SurfaceSpec / field kinds / region / ActionRef / catalog), surface-registry (typed service handle), transport-ws (Bun WS :24205, path-agnostic upgrade), surface-loaded-extensions (first real surface); kernel HostAPI.getExtensions; host-bin wiring; bin/up. Harness: retire AGENTS 'backend only', ORCHESTRATOR §3/§7/§8, frontend-design.md locked.
B2 — wire-types split (chat-slice prerequisite): new types-only @dispatch/wire single-sources the wire ABI (AgentEvent + 11 variants; conversation model Chunk/ChatMessage/Role/TurnId/StepId + 6 chunk variants; Usage) with zero @dispatch/* deps. @dispatch/kernel re-exports via shims so its public surface is byte-identical (zero consumer blast radius). transport-contract re-exports AgentEvent from @dispatch/wire and drops its @dispatch/kernel dependency, so HTTP clients (the web frontend) consume the wire without the kernel runtime.
tsc -b + biome clean; 460 vitest + 77 bun pass.
|
|
transport-contract wire package
|