| Age | Commit message (Collapse) | Author |
|
The LSP diagnostics path hung up to 60s per edit whenever a configured Ruby
language server was dead or slow (the reported Steep langserver case): a
killed/crashed server was never detected (stayed "connected" forever), servers
were queried sequentially with a 60s budget each, and a corrupted-but-alive
server (Steep's ~3h phantom-SyntaxError drift) had no recovery.
Four fixes, all in packages/lsp/ (the tool-edit-file call site lowered to 10s):
1. Dead-process detection: SpawnedProcess.onExit (Bun proc.exited) + stdout-end
defence flip the client to error, dispose the rpc, kill the proc. The manager
re-spawns a fresh server after the 30s backoff. Dead servers are now skipped
(0s) instead of polled for 60s.
2. Concurrent fan-out + 10s hard cap: new aggregateDiagnostics queries all
matching servers at once, each capped at 10s. A non-responder is skipped
with "LSP took too long (>10s), skipped — raise this to the user" instead of
blocking the fast server's results. Replaces the vague "unusually long"
warning (now structurally impossible: slow is always false).
3. Corruption self-heal: a detector flags a server re-emitting identical
non-empty diagnostics despite the file changing; after 5 repeats the client
is marked broken and re-spawned. Clean files never trip it. (Acknowledged
false-positive risk on persistent unfixed errors; CLI type-check gate stays
authoritative.)
4. sendRequest timeout: hover/definition/references cap at 10s so they can't
hang the turn against a dead server; the initialize handshake keeps its 45s
race.
Verification: typecheck clean; 1573 tests pass (96 files), +15 new LSP tests
(86 in packages/lsp); biome clean. No kernel/contract changes; onExit is
internal to packages/lsp.
|
|
The backend already supported GET /conversations?workspaceId= but the CLI
never sent it. Wire the list command to that filter:
- args.ts: parse --workspace / -w on 'list' (placed before the --catch-all
so the single-dash -w shorthand isn't taken for a positional prefix);
add workspaceId? to the list ParsedCommand.
- http.ts: add workspaceId? to FetchConversationsOpts; send ?workspaceId=
(after q/status, preserving URLSearchParams order).
- main.ts: forward parsed.workspaceId into fetchConversations; update USAGE.
Composable with --status and the <prefix> short-id arg. 'Open conversations
in workspace X' is now: dispatch list --workspace X (status defaults to
active,idle). No contract changes — purely additive CLI wiring.
Tests: +4 args (incl. composability + missing-value error), +2 http
(exact ?workspaceId= URL + combined status/workspaceId with %2C encoding).
typecheck EXIT 0, biome clean (364 files), full suite 1558 passed.
Live-verified against an isolated server.
|
|
Investigation of whether the backend supports listing open conversations
filtered by a specific worktree/workspace.
Findings:
- 'worktree' is not a Dispatch domain concept; canonical term is 'workspace'
(logical grouping) vs 'working directory' (cwd, filesystem path).
- GET /conversations already supports composable ?workspaceId=, ?status=, ?q=
filters. 'Open conversations in workspace X' = ?workspaceId=X&status=active,idle.
- Every conversation carries a workspaceId (default 'default'); ConversationMeta
is in @dispatch/wire; filter lives in conversation-store listConversations.
- A literal directory (git worktree) filter (?cwd=) is NOT supported; §3b
documents the small additive change needed across wire/store/transport-http.
- Test coverage verified: store-workspace.test.ts:369, store.test.ts:1463,
app.test.ts:3696.
Research notes only — no code/contract changes.
|
|
Add the same --file <path> support that the summon (chat) command has to the
'dispatch send' subcommand. When --file is given, the file's contents are read
and attached to the message (composed via composeMessage, identical to chat).
- args.ts: add 'file' to the send ParsedCommand, make 'text' optional, parse
--file, and require at least one of --text or --file.
- main.ts: read the file and compose the message in the send case, using the
composed message in both the --queue and streaming branches; update USAGE.
- args.test.ts: cover --file parsing (alone, with --text, missing value) and
update the existing send expectations + the both-missing error message.
|
|
bin/up ran `bun --watch main.ts` without setting BACKEND_PORT, so a
shell-exported BACKEND_PORT (e.g. 24991 in ~/.bashrc, set so the Dispatch
CLI hits the prod server) overrode .env's dev value 24203 — Bun lets shell
env win over .env — binding the dev server onto the production port and
colliding with the active dispatch-server systemd service. transport-http
then failed to activate (Bun.serve "port in use"), so the HTTP server
never came up and the frontend got "Failed to fetch".
Force BACKEND_PORT=24203 + SURFACE_WS_PORT=24205 in the setsid invocation
so the dev stack is deterministic regardless of the shell environment.
|
|
Mirrors the existing GET /conversations/:id/lsp route exactly: gates on the
persisted then effective cwd (null → empty servers), returns 503 when the
MCP service isn't loaded, and maps McpServerStatus → McpServerInfo
(conditionally including `error` per exactOptionalPropertyTypes).
Wires mcpService into CreateServerOptions + extension activate via a plain
host.getService (mirroring lspService; "mcp" added to dependsOn, route added
to contributes.routes), adds the @dispatch/mcp workspace dep, and re-exports
mcpServiceHandle / McpService / McpServerStatus from seam.ts. Adds 4 tests
mirroring the LSP status tests.
|
|
Two bugs caused the dispatch server to crash (15 times since Jun 24)
when chat cc6c edited packages/transport-http/src/app.ts — a 40KB file
with 23 multi-byte UTF-8 lines. The edit_file diagnostics hook sends the
file to tsserver, which sends back a large publishDiagnostics response.
When the response was split across stdout chunks at a multi-byte
character boundary, the server crashed.
Layer 1 — rpc.ts handleMessage: JSON.parse had no try/catch. A corrupted
message threw an unhandled SyntaxError → unhandled rejection → process
exit. Wrapped in try/catch; malformed messages are now skipped.
Also hardened client.ts handleBytes: the async handleMessage Promise was
fire-and-forget. Added .catch(() => {}) as defence-in-depth so no
rejection from the RPC layer can ever crash the server.
Layer 2 — framing.ts FrameDecoder: used a string buffer with
new TextDecoder().decode(chunk) (no { stream: true }), corrupting
multi-byte characters split across chunks. Worse, Content-Length counts
bytes but the buffer was sliced by character count — for multi-byte
content byte length ≠ char length, so the decoder extracted the wrong
slice as a message. Rewrote to use a Uint8Array byte buffer: header
separator search is byte-level, Content-Length comparison is byte-level,
and the body is decoded only after all bytes are confirmed present.
Tests: 5 new multi-byte framing tests (split at char boundary,
byte-vs-char Content-Length, two messages in one chunk, three-way split)
+ 1 rpc test (malformed JSON does not throw). All 1545 tests pass.
|
|
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).
|
|
- 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).
|
|
The previous fix (e03a96e) wrapped getService in try/catch to prevent the
activation crash, but that wasn't enough: tool-edit-file activates at position
5 in CORE_EXTENSIONS while lsp activates at position 20. So getService ALWAYS
threw at activation time, lspService was ALWAYS undefined, and the diagnostics
hook was NEVER wired — edits succeeded but never showed LSP feedback.
Fix: make the LSP service lookup LAZY — defer it to edit time (when the tool is
actually called), not activation time. By then all extensions have activated.
The diagnostics function tries getService on each edit call; if LSP isn't
loaded, it returns a no-op (graceful degradation).
|
|
The per-edit diagnostics change (8f6114b) called host.getService(lspServiceHandle)
during activate(). But getService THROWS when a service has no provider — so if
the LSP extension activates AFTER tool-edit-file (or isn't loaded at all), the
activate() function crashes and the edit_file tool is NEVER REGISTERED. This is
why the edit_file tool was missing from the agent toolset.
Fix: wrap getService in try/catch — if the LSP service isn't available yet,
lspService becomes undefined and edits proceed without diagnostics (the graceful
degradation the comment always promised but the code didn't deliver).
|
|
New `mcp` standard extension (`packages/mcp/`) that makes Dispatch an MCP
host: spawns configured MCP servers (stdio child processes), performs the MCP
handshake (initialize → notifications/initialized), discovers tools via
tools/list, and registers each as a first-class Dispatch ToolContract via
host.defineTool. When the model calls an MCP tool, the extension proxies the
call to tools/call on the MCP server and returns the flattened result.
Architecture (sibling of `lsp` extension):
- Config: .dispatch/mcp.json (servers key) → opencode.json mcp key fallback,
resolved per-cwd (mirrors LSP config resolution)
- Transport: StdioTransport (spawn child, Content-Length framing + JSON-RPC 2.0)
- Client: initialize → tools/list → tools/call; handles list_changed
notifications for dynamic tool updates
- Registry: tool name namespacing (<serverId>__<toolName>), ToolContract
adapter that proxies execute → callTool, content flattening (text/image/
resource → string)
- Manager: one client per server, lazy-spawn, status(), shutdownAll()
- Extension: manifest (dependsOn session-orchestrator, capabilities spawn),
registers tools + a toolsFilter (drops disconnected server's tools),
mcpServiceHandle, deactivate kills all child processes
Phase 1 scope: stdio only, Tools only (no Resources/Prompts/HTTP/sampling).
Hand-rolled JSON-RPC + framing (zero external deps, adapts LSP patterns).
Wave 1 (agent): 12 source + 8 test files, 69 new tests.
Wave 2 (orchestrator): root tsconfig ref, host-bin CORE_EXTENSIONS
registration + package.json dep, bun install.
Verified: tsc -b EXIT 0, biome clean, 1537 vitest pass (was 1468, +69).
|
|
|
|
- notes/mcp-design.md: full design — architecture fit (sibling of lsp ext),
per-cwd config (.dispatch/mcp.json + opencode.json mcp key), tool name
namespacing (<serverId>__<toolName>), ToolContract adapter, content
flattening, security, glossary additions, 6 open design decisions
- PLAN-mcp.md: wave breakdown (Wave 0 contracts/wiring, Wave 1 the mcp
extension, Wave 2 host-bin registration, Wave 3 live verification)
- Phase 1 scope: stdio only, Tools only, no surface, hand-rolled JSON-RPC
- No kernel contract change needed (existing ToolContract + defineTool +
toolsFilter are sufficient)
|
|
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
|
|
LSP extension:
- Multi-server aggregation: query ALL connected servers matching the
file's extension (not just the first), merge diagnostics tagged by source
- Incremental sync: capture each server's textDocumentSync.change during
initialize; compute prefix/suffix diff ranges for change:2 servers;
full content for change:1 (generic, works for any LSP)
- New diff.ts: pure computeChangeRange + offsetToPosition (O(n), tested)
- Buffer sync: change(filePath, newText) sends didChange with post-edit
in-memory content; openWithText for first open; tracks open doc text
- languageId mapping: extended with .rb/.rbs/.c/.cpp/etc. (was 'unknown')
- waitForDiagnostics: accepts text override + timeoutMs; returns
{ formatted, slow, timedOut }; polls for publishDiagnostics push
- DiagnosticsStore: hasReceivedPush/clearReceived tracking; formatFiltered
with minSeverity (1=Error, 2=Warning) for edit_file integration
- LspService.getDiagnostics: service method for cross-extension use
tool-edit-file:
- After successful edit, calls LSP getDiagnostics with post-edit buffer
- Only appends diagnostics with severity ≤ 2 (errors+warnings, no noise)
- Appends slow warning (>10s): 'LSP is taking unusually long...'
- 60s timeout; graceful degradation when no LSP available
- Optional dep on @dispatch/lsp (getService pattern, not manifest depOn)
1468 vitest pass (was 1453, +15 new diff tests).
|
|
|
|
- Web frontend section links to github.com/realtradam/dispatch-web
- Claude extension links to github.com/realtradam/dispatch-adapter-claude
- Workspace layout uses generic clone instructions, not /home/tradam paths
- Dev stack bin/up instructions show clone-both-repos-as-siblings
|
|
- Full HTTP API table (30+ endpoints: conversations, workspaces, LSP,
system-prompt, metrics, queue, cache-warming, compaction, etc.)
- Complete CLI commands (list, read, send, stop, compact, open + flags)
- All 37 packages documented with tiers and dependencies
- systemd deploy section (bin/build, bin/install, bin/sync-env)
- Dev stacks table (bin/up 24203, bin/up2 25203)
- Workspace layout (dispatch-backend, dispatch-web, claude, bin)
- Updated web frontend section (Slice 2 browser chat in progress)
- Links to all design docs
|
|
After consolidating to the dev branch and renaming the worktree,
update all path references in ORCHESTRATOR.md and .skills/ORCHESTRATOR.md.
|
|
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.
|
|
conversation
kernel: executeToolCall now races tool.execute against the abort signal
via Promise.race; on abort resolves (not rejects) with an "Aborted" result
so the step completes normally → finishReason "aborted" → turn seals
cleanly (done event) → finally clears activeTurns → conversation freed,
next message accepted. run-turn strips tool-call chunks from the assistant
message on abort (keeps text/thinking) and omits tool-result messages to
avoid persisting dangling tool calls that would 400 the provider next turn.
tool-shell: realSpawn spawns detached (own process group); on abort AND
timeout kills the entire group (process.kill(-pgid, SIGKILL)) and resolves
immediately — no child.on("close") dependency, so a grandchild holding the
pipes can't stall the spawn promise or leak.
Also: ORCHESTRATOR.md migrated to dispatch CLI summon mechanism; .skills
summary; bin/sync-env PATH injection; frontend handoff docs.
1453 vitest pass · tsc -b EXIT 0 · biome clean.
|
|
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.
|
|
|
|
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.
|
|
|
|
The system-prompt service cached the resolved prompt on first turn and reused
it on subsequent turns via get(). But the prompt is cwd-sensitive (file:AGENTS.md,
prompt:cwd variables). When a conversation's cwd changed after the first turn,
the cached prompt was stale — referenced files from the new cwd were not loaded.
system-prompt: added getWithMeta(conversationId) returning { prompt, cwd } and
stores resolved-cwd:<id> alongside resolved:<id> in construct().
session-orchestrator: subsequent turns now call getWithMeta, compare stored cwd
vs effective cwd, and reconstruct if they differ. Compaction path (always
constructs) and warm path (no system prompt) are unaffected.
1411 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
|
|
system:os now returns a descriptive string instead of the raw platform:
- Linux: reads /etc/os-release for distro name (PRETTY_NAME or NAME+VERSION_ID)
- WSL detection: checks /proc/sys/fs/binfmt_misc/WSLInterop or 'microsoft'
in /proc/version — appends (WSL) to the distro string
- Non-Linux: returns process.platform as-is (darwin, win32, etc.)
Examples: 'Ubuntu 22.04 LTS', 'Ubuntu 22.04 LTS (WSL)', 'Debian 12',
'Linux (WSL)', 'darwin'. All file reads use injected fs adapters (testable).
7 new resolver tests. 1403 vitest pass. FE CR-9.
|
|
Lets the AI know which workspace it's in — especially useful when summoning
agents. Wired through the construct context in both the regular turn flow
and the compaction flow.
|
|
|
|
Register @dispatch/system-prompt in CORE_EXTENSIONS (after skills, before
cache-warming). Add dep + tsconfig ref. 1396 vitest pass, typecheck + biome clean.
|
|
session-orchestrator:
- Wire systemPromptService as optional dep (lazy via host.getService)
- Regular turn: construct on first turn (new conversation), get on subsequent
turns, set on providerOpts.systemPrompt (cache-safe)
- Compaction: construct (fresh resolve) + append COMPACTION_SYSTEM_PROMPT
- 12 new tests (construct/get/service-unavailable/compaction)
transport-http:
- GET /system-prompt (returns template or DEFAULT_TEMPLATE)
- PUT /system-prompt (validate + setTemplate, 503 when unavailable)
- GET /system-prompt/variables (static catalog, always available)
- 6 new tests
system-prompt service: added getTemplate/setTemplate to interface + impl.
1396 vitest pass. typecheck + biome clean.
|
|
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.
|
|
|
|
|
|
cli (Wave 2+3)
session-orchestrator: workspaceId on StartTurnInput/EnqueueInput; effective cwd
resolution (getCwd → getEffectiveCwd); auto-create workspace on turn start;
warm parity (same effective cwd). 93 tests (+8).
transport-http: workspace routes (GET/PUT/DELETE /workspaces, title, default-cwd);
workspaceId threading on POST /chat + queue; ?workspaceId= filter on
GET /conversations; DELETE /conversations/:id/cwd (clears explicit cwd);
GET /conversations/:id/lsp uses effective cwd; slug validation. 166 tests.
transport-ws: workspaceId threading on chat.send + chat.queue. 32 tests.
cli: --workspace/-w flag; ConversationMeta test fakes fixed. 123 tests.
Full typecheck EXIT 0, biome clean. 1283 vitest + 199 transport bun pass
(1 pre-existing tool-shell failure unrelated to workspaces).
|
|
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
|
|
The backend now persists chunks at step boundaries during generation,
not only at turn-seal. This enables the FE to syncTail mid-turn and
pick up committed, seq'd chunks (eliminating the provisional state).
Changes:
- RunTurnInput: add onStepComplete callback (kernel contract)
- runTurn: call onStepComplete after each step's messages are finalized
- Orchestrator: persist userMsg at turn start + each step's messages
via onStepComplete. Falls back to batch persist if callback isn't
called (backward compatible with test fakes).
The user message gets seq numbers before the first step generates.
Each step's assistant + tool messages get seq numbers as they complete.
The FE's existing syncTail (?sinceSeq=N) picks them up during generation.
Also adds backend-to-fe-handoff.md with CR-6 response + full endpoint list.
|
|
Add stopTurn to the orchestrator: aborts the in-flight turn's
AbortController without changing conversation status. The turn
seals normally (finishReason: 'aborted'), partial messages are
persisted, and the conversation transitions active → idle via the
normal settle path.
Distinct from closeConversation which marks the conversation closed.
- POST /conversations/:id/stop endpoint
- dispatch stop <id> CLI command
- FE handoff: frontend-stop-generation-handoff.md
|
|
Also adds bin/sync-env script for updating system env keys.
|
|
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
|