# Chat Runtime Module

Provides server-owned chat turn execution for existing sessions, including text/file user input, durable continuation queue items, AI SDK `UIMessageChunk` SSE streaming, final `UIMessage` snapshot persistence through AI SDK stream finish callbacks, usage writes, run state updates, cancellation terminal transitions, and ACP live runtime orchestration.
`messages.message_json` is the only hydration source of truth. Stored message snapshots are Cradle-internal trusted `UIMessage` JSON; this module parses them directly instead of running a second Zod projection schema over AI SDK-owned message state.
Provider turn context is intentionally reconstructed through a Cradle-owned transcript layer: ordinary new turns read recent complete top-level messages from `messages.message_json`, preserve the stored `UIMessage.parts` structure, and expose a bounded `CradleTurnTranscript` for provider-specific native history projectors. Native approval continuation still uses the explicit AI SDK `messages` request body because it must preserve tool lifecycle state exactly as supplied by the client. Final assistant snapshots are projected incrementally while chunks are published, so terminal finalization reads current state without reconstructing it from historical chunks. Durable snapshots are lossless: oversized tool payloads and text/reasoning overflow are moved into the blob store with a recoverable reference instead of being truncated away, so `message_json` stays bounded without destroying conversation bytes. Only transient surfaces bound payloads lossily.
Model-facing Cradle context is assembled under `harness/`. The stable prefix contains the Cradle runtime identity for both ordinary Chat Sessions and Work Sessions; Chat Sessions append stable fragment precedence instructions, and primary Work threads also append a stable closed-loop Work mode block (`CRADLE WORK MODE`) from `system-instructions.ts`. Every Chat Session contributes a Chat Runtime-owned `<cradle_session_state>` fragment containing authoritative runtime identity and routing metadata, while domain owners may register deterministic extension fragments without importing their state into Chat Runtime; Work contributes `<cradle_work_state>` with only the primary Work id and thread role. Dynamic identity stays out of the system-prompt string for cache stability. Codex persists fragments as developer items, Claude Agent appends synthetic non-query user messages because its live query lacks a developer channel, and providers without native persistent context append the fragments after the stable System Prompt. Content-derived fragment revisions prevent duplicate injection while ensuring changed metadata is reprojected; when a persistent provider thread contains multiple revisions for one key, the last injected revision supersedes earlier ones. Revisions are invalidated at provider compaction boundaries so the latest fragments are projected again after compaction. Runtime/DB state remains authoritative; the harness is only its model-facing projection.
Usage accounting has two explicit runtime modes. A `provider-events` runtime emits typed per-call `RuntimeUsageEvent` values; TurnExecutor enriches them with the active Cradle session/run/message/provider identity and the Usage module persists them durably before notification consumption continues. Codex uses this mode and never writes a second run-final summary. Other runtimes currently use `run-summary`, where finalization persists their existing aggregate usage once. This seam keeps native protocol projection in the provider, active-run identity in Chat Runtime, and ledger validation/query semantics in Usage.
Active top-level run sequencing is owned by `stream/run-chunk-sequencer.ts`. An active run retains one current `UIMessage` projection, its active text/reasoning/tool-input identities, one run-scoped monotonic cursor, scalar publication counters, and live subscribers; it never retains or persists raw chunk history. Every SSE late join and multiplex WebSocket connect/reconnect receives one transient `data-cradle-stream-snapshot` at cursor N, then only queued or future live chunks whose cursors are greater than N. The `{ runId, cursor }` Sync token still identifies the observed run and position, but the current snapshot is the recovery authority. A released or changed run has no live recovery state and ends as not found instead of manufacturing an interrupted chunk. The sync-gateway sender owns only frame/byte backpressure and never allocates business cursors. Provider-thread replay remains separately owned under `provider-threads/`.
`POST /chat/sessions/:sessionId/response` accepts ordinary text/file input, chat-owned `contextParts`, optional model/provider overrides, `low` / `medium` / `high` / `xhigh` thinking effort, and AI SDK `messages` history. The `messages` form is used for native tool approval continuation: the last assistant `UIMessage` already contains the `approval-responded` tool part, and the server reuses that assistant message id so AI SDK can continue the same tool lifecycle through `convertToModelMessages` and `collectToolApprovals`. Codex review picker submissions additionally carry a typed `reviewTarget`; the coordinator creates an ordinary Cradle-owned run and the Codex provider maps it to app-server `review/start` with inline delivery, so native review notifications share the normal run lifecycle and transcript stream rather than becoming a prompt-simulated turn.
Provider plan implementation is exposed through synthetic `plan_implementation` approval parts. `POST /chat/sessions/:sessionId/messages/:messageId/plan-implementation-approval` only persists the local approval response; the renderer submits approved plans as an ordinary follow-up turn using `PLEASE IMPLEMENT THIS PLAN:`.
`POST /chat/sessions/:sessionId/user-input/:requestId` resolves a Chat Runtime-owned pending user input request for the active provider turn. Provider adapters map native request shapes such as Codex `item/tool/requestUserInput` and Claude Agent `AskUserQuestion` into runtime questions, then wait on the runtime promise; submitted answers resolve that promise and publish a synthetic `tool-output-available` chunk so renderers clear the pending prompt after Cradle accepts the submission. While a request is pending for the active run, `/chat/sessions/:sessionId/runtime-status` reports `waitingForUserInput` instead of ordinary `streaming` so status surfaces can distinguish a blocked question from generation. `POST /chat/sessions/:sessionId/tool-approval/:requestId` resolves a separate pending tool approval promise for provider-native requests such as Codex command, file-change, permission, `applyPatchApproval`, `execCommandApproval`, and Claude Agent SDK `canUseTool`; Chat Runtime owns the pending lifecycle and provider adapters own the native response shape. While a tool approval is pending for the active run (and no user-input request is also pending), `/runtime-status` reports `waitingForToolApproval` so workspace sidebar and other status surfaces can show a needs-approval indicator instead of a plain running spinner. If the provider approval belongs to a provider-native subagent thread, the approval tool chunks are published to that provider-thread stream rather than the parent chat stream.

`POST /chat/sessions/:sessionId/bang-command` executes a user-entered single-line shell command through the session runtime and persists the command plus result as two complete user messages for renderer hydration. Standard runtimes use Chat Runtime's local executor from the session workspace. Codex runtime delegates execution to the Codex provider, which calls Codex app-server `thread/shellCommand` so Codex owns the native `commandExecution` protocol item. The persisted Cradle snapshots remain UI/history records: stdout is the provider-visible result text when present, stderr is used when stdout is empty, and full stdout/stderr/exit metadata is stored under `metadata.cradle.bangResult`.
`POST /chat/sessions/:sessionId/bang-transcript` persists an interactive Composer bang PTY transcript the same way without executing a command: the client-owned scrubbed terminal text becomes `bangResult.stdout`, while Cradle still writes the paired bang command/result user messages for renderer hydration and provider projection.
`POST /chat/sessions/:sessionId/observations` appends a metadata-only observation user message (`UserMessageAppended` only — no `RunStarted`) for eligible local `origin: 'jarvis-ambient'` sessions. Used by the renderer UI activity pipeline; observation body text must not contain `<cradle_context>`, and metadata is stored under `metadata.cradle.observation` with `kind: 'ui-activity'`.
`POST /chat/sessions/:sessionId/title/regenerate` asks the active runtime to generate a fresh title from the session's first user prompt, then writes the result back to the Cradle-owned `sessions.title` field. Chat Runtime owns validation, durable binding refresh, and the explicit overwrite semantics; provider adapters own the runtime-native title-generation and best-effort provider-session rename behavior.
`POST /chat/sessions/:sessionId/rollback-last-turn` rolls back the last completed top-level user turn through the Chat Runtime provider contract, then records a `LastTurnRolledBack` fact that deletes the named message projections from Cradle's transcript. The internal `rollbackTurns` composition API applies the same maintenance claim and projection path to multiple turns for historical checkpoint rewind. Codex implements both paths with app-server `thread/rollback` and the requested `numTurns`; it never calls `thread/fork` and does not revert workspace file changes. OpenCode selects the requested historical assistant message and calls native `session.revert`. Claude Agent does not declare rollback support.
`POST /chat/sessions/:sessionId/side-chat` creates a live-only side conversation from an existing parent session. Side chat semantics live in this module, not in provider slash-command namespaces: Chat Runtime resolves the parent runtime handle, prefers the provider `forkRuntimeSession` hook when native thread forking is available, otherwise starts a fresh live runtime session and seeds its `SideConversationRegistry` record with the parent transcript as Cradle-owned side history. It returns a `sideConversationId` for renderer-owned presentation and does not create `sessions`, `messages`, `backend_runs`, or durable provider bindings for the side conversation. `POST /chat/side-conversations/:sideConversationId/response` streams an ordinary side turn directly against the live provider handle, passes the side-local history to the runtime, appends completed side turns only to the in-memory side record, and forwards AI SDK chunks without persisting transcript rows. `DELETE /chat/side-conversations/:sideConversationId` releases the live handle; parent session archive/cleanup releases all side conversations for that parent. Once the live side registry entry expires or is released, side runtime actions fail with `side_chat_expired` instead of starting a replacement provider runtime session.
Passive observers hydrate through cursor-bounded `GET /chat/sessions/:sessionId/message-previews`, which returns the newest transcript-ordered display page as `{ revision, rows, nextCursor }`. `limit` defaults to 100 and is capped at 200; the opaque backwards cursor uses stable SQLite message insertion order. Preview rows retain text, reasoning, tool lifecycle metadata, and blob references, but omit inline tool input/output bodies. The existing `GET /chat/sessions/:sessionId/messages` route remains the lossless history compatibility path, and `GET /chat/sessions/:sessionId/messages/:messageId` is the full detail boundary. Mobile and other passive clients prepend earlier preview pages without replacing live streamed messages; the Web rich tool/approval surface currently keeps using the full history route because its renderer needs inline tool payloads for interactive workflow blocks. Complete transcript export remains the unbounded history path; ordinary session open never assumes it has loaded the full transcript. Observers then join the currently active run through `GET /chat/sessions/:sessionId/stream`; every join starts from one transient current-state snapshot and then continues with live chunks. The web durable cache treats the event revision as the only ordering source and never compares browser/server wall clocks. Active-run release detaches subscribers and clears projection maps, pending flush timers, and final-message parts after terminal diagnostics are recorded, so closed streams cannot pin completed assistant payloads in memory.
Active streaming message checkpoints are terminal-fenced against the persisted `backend_runs` row. While a run is still `streaming`, the ~10s snapshot timer upserts an ephemeral `run_stream_checkpoints` row (plain state, not a domain fact) for process-crash recovery; duplicate snapshots are skipped. Ordinary history reads do not overlay this potentially large payload, and live reconnect recovery uses the active run's richer in-memory stream state instead. `messages` is not updated during streaming because projections are written only by projectors reacting to facts. Once a terminal fact has projected the run to `complete`, `failed`, or `aborted`, the checkpoint row is deleted in the same transaction, and late snapshot timers / pending delta flushes / provider finalizers stop writing and release the active run.
`GET /chat/runs/completed` is a desktop-notification polling projection over terminal `backend_runs`. It returns `responseBody` as the completed run's latest persisted assistant response text for native notification descriptions, while `messagePreview` stays a short list-friendly preview of the same response.
Provider-native subagent/thread detail is also owned here as a session-scoped runtime capability, not as a Cradle child session. `GET /chat/sessions/:sessionId/provider-threads` lists provider threads such as Codex `subAgentThreadSpawn` entries and Claude Agent SubAgent tool-call threads, `GET /chat/sessions/:sessionId/provider-threads/:threadId` reads provider metadata, `DELETE /chat/sessions/:sessionId/provider-threads/:threadId` deletes a provider-native thread after provider-owned parent/thread validation, `GET /chat/sessions/:sessionId/provider-threads/:threadId/turns` hydrates provider turns and projected `UIMessage` records, and `GET /chat/sessions/:sessionId/provider-threads/:threadId/stream` replays live AI SDK chunks captured from the active parent run. Parent chat messages keep only the provider-owned parent tool lifecycle and reference metadata; full subagent transcripts are streamed and hydrated through provider-thread APIs instead of being persisted into parent `messages.message_json`. Chat Runtime resolves and validates the parent session/runtime/profile context, then asks Provider Runtime for an existing live side handle or durable binding; providers own native thread identity, tree membership, history projection, and notification semantics.

Claude Workflow runs use `GET /chat/sessions/:sessionId/workflows/:toolCallId/stream`. Chat Runtime validates the parent session and Workflow tool call, then exposes the provider-owned artifact subscription as bounded SSE. The endpoint emits reduced Workflow snapshots rather than raw JSONL; the Claude provider owns declaration discovery, artifact normalization, reconciliation, and watcher lifecycle.
Background terminals are exposed as the same kind of session-scoped runtime capability. `GET /chat/sessions/:sessionId/background-terminals` lists provider-owned background terminal processes for the active provider session, and `POST /chat/sessions/:sessionId/background-terminals/:processId/terminate` asks the provider to terminate one process. Chat Runtime owns session/profile/runtime resolution and the JSON-safe HTTP contract; providers own native process identity, listing, and termination semantics.
Every backend run also writes a durable Cradle-owned run snapshot under `backend_run_snapshots` plus ordered `backend_run_snapshot_events`. This is the runtime-neutral harness envelope: Chat Runtime records lifecycle, millisecond timing, usage, terminal failure, and stable AI SDK chunk phases such as model text/reasoning and tool input/output availability. Provider-specific goal, plan, tool, MCP, context-engine, and filesystem semantics stay owned by provider adapters; snapshots only provide enough shared structure for diagnostics, replay inspection, observability export, and future automation巡检. Run snapshots are forensic records rather than session-owned transcript rows: deleting a session/run nulls those foreign keys instead of deleting the snapshot, while snapshot retention prunes completed records older than `CRADLE_CHAT_RUN_SNAPSHOT_RETENTION_DAYS` (default 30, `0` disables pruning).
Runtime goal continuation is a Chat Runtime run-lifecycle concern, with provider-owned detection and message projection exposed through `ChatRuntime.goalContinuation`. When a run ends without user cancellation while the provider snapshot still contains a continuable active goal, Chat Runtime schedules a deduped follow-up system run, creates a new assistant message for that run, and asks the runtime hook to build the internal metadata-marked user message without inserting a visible user message. By default only active goals are continuable; enabling the App feature flag `continueBlockedCodexGoals` in Settings > Features also allows the Codex composition root to treat blocked goals as continuable, but those blocked goals are resumed through one direct `thread/goal/set` bridge call instead of an internal run. User cancellation and queued user turns take precedence: cancellation remains provider-owned, and pending queue rows drain before any automatic goal continuation. `/chat/sessions/:sessionId/runtime-status` exposes `hasActiveGoal` so passive renderers can keep polling long enough to discover the new active run and join its session stream.
Top-level user turns also invoke composition-root-registered Turn Checkpoint hooks immediately before provider streaming and after terminal persistence. Chat Runtime owns only the lifecycle seam and skips internal `runtimeGoal` continuation runs; `../turn-checkpoint` owns hidden Git refs, capture failures, diff totals, and coordinated restore. The hook registration in `runtime.ts` avoids a domain cycle while keeping checkpoint capture best-effort so Git failures cannot fail a provider turn.
`GET /chat/draft-runtime-capabilities?runtimeKind=...` returns provider-owned composer capability descriptors for pre-session surfaces such as New Chat and Workspace Capsule. It is read-only, does not create or resume a backend session, and exposes only command/slot availability that can be rendered before a thread exists; live slot state such as an active Codex goal still requires a session.
`GET` / `PUT` / `DELETE /chat/composer-drafts/:surfaceId` own server-authoritative Composer drafts for renderer surfaces. The current implementation is last-write-wins over a JSON payload with soft-delete tombstones so closed surfaces and cross-window clients do not resurrect older localStorage drafts; the JSON envelope intentionally leaves room for a future Loro payload without changing the route ownership boundary.
`GET /chat/runtimes` is the runtime catalog owned by Chat Runtime. It lists builtin and plugin-registered runtime providers with display metadata, supported provider kinds, icon keys, and the surfaces that may offer the runtime (`chat`, `jarvis`). Catalog items also expose session launch ownership through `sessionLaunchMode`, so callers can distinguish provider-launched chat runtimes from agent-terminal sessions without hard-coding runtime ids. Chat, Jarvis, Automation, and future launchers should read this catalog instead of hard-coding runtime ids. Provider compatibility is registered with the runtime provider metadata, so plugin runtime ids can participate in the same provider-target validation path as Codex, ACP Chat / Cloud Agent SDK, Claude Agent, and HiJarvis.
`GET /chat/runtimes/health` runs optional provider health checks for already-registered runtimes. Providers that do not expose `healthCheck` return `unknown`; health probing is intentionally separate from catalog listing so ordinary runtime selectors do not trigger provider IO.
`GET /chat/sessions/:sessionId/capabilities` owns the session-scoped runtime capability inventory returned to the renderer. Its payload now includes runtime-native slash commands, provider-projected UI slots, and skill ids, so composer presentation can stay provider-driven instead of hard-coded in the frontend. Runtime UI slots may also declare `commandAction` and `requiresSession`, allowing providers to define whether a slash row inserts text, submits a command, opens a Cradle UI action, or is hidden from pre-session draft composers without frontend runtime-id branches.
Internally, `ChatRuntimeCapabilities` is no longer this renderer presentation payload. It is the static provider routing contract declared on each runtime at construction time: live steering, shell execution, runtime settings updates, UI slot state support, dynamic provider capability discovery, native code review (`supportsNativeReview`, the `reviewTarget` gate — declared by Codex, not a runtime-id check), and session model-switch semantics. Provider-owned renderer descriptors are represented separately as runtime presentation capabilities and are returned through `getPresentation` / `getDraftPresentation`, preserving the existing HTTP payload while keeping routing flags cheap and synchronous.
`GET /chat/sessions/:sessionId/ui-slot-states` returns provider-owned state for declared UI slots. Renderer surfaces are selected from `RuntimeUiSlot.surfaces`: `composerState` can render primary workflow state near the composer, `slashCommand` can enter the slash panel, `toolbarPicker` can expose option summaries next to existing toolbar controls, `runtimePanel` can render Right Aside Runtime summaries, and `streamEvidence` is consumed by message/tool blocks from provider-emitted chunks. Polled slot state is a current snapshot and must not be used to synthesize transcript-positioned message-bubble content. Codex `goal` is read from the app-server thread goal object; Codex `compact` separates context-compaction item lifecycle from thread token-usage meter state, using `contextCompaction` item start/complete as the primary lifecycle source and deprecated `thread/compacted` only as a compatibility signal; Codex `status`, `model`, and `reasoning` are projected from thread start/resume responses, settings/status notifications, config, model list, and model-provider capability reads. Codex `plan`, `toolActivity`, `mcp`, `diff`, `terminal`, `approvals`, `alerts`, `filesystem`, and `search` are projected from their app-server notifications and bounded provider snapshots; Codex `terminal` also reads provider-owned background terminal rows at render time so Composer can show process ids, CPU, memory, and terminate actions without knowing Codex JSON-RPC method names. Claude Agent `progress` is projected from the latest normalized TodoWrite state or structured TaskCreate/TaskUpdate/TaskList state in the Claude Agent provider snapshot; the adapter does not parse human-facing task result strings to recover IDs. Codex `usage` is projected from `account/rateLimits/read` and rate-limit notifications, including primary/secondary window duration and reset timestamps so renderer composer and runtime-panel surfaces can show 5h/weekly-style account usage without reading Codex protocol payloads directly; `skills`, `plugin`, `crew`, and `config` use provider list/read methods at render time. Codex `crew` enumerates subagent threads with paginated provider `thread/list` and uses recent parent-turn reads only for call lifecycle summaries. Chat Runtime stores only the provider snapshot needed to replay those projections; it does not own goal, compaction, plan, progress, tool, MCP, model, reasoning, diff, terminal, approval, alert, filesystem, search, usage, skill, plugin, crew, or config lifecycle semantics.
`GET /chat/sessions/:sessionId/context-usage` exposes provider-owned context window usage details when the active runtime supports them. The response preserves the runtime-neutral `RuntimeContextUsage` sections/items shape so renderers can show system prompt, message, tool, file, skill, and provider-specific usage groups without inventing a Chat-owned tokenizer projection. Runtimes that only expose aggregate context usage should continue to publish `compact` UI slot state; callers must treat a `null` `usage` payload as "details unavailable" rather than as zero tokens.
Quick question `/btw` support is declared by providers as a `quick-question` UI slot with `slashCommand` and `composerState` surfaces, while `POST /chat/sessions/:sessionId/quick-question` owns the stateless response stream. Its provider context uses the same bounded, blob-resolved transcript projection as a normal turn, with separate `CRADLE_CHAT_QUICK_QUESTION_MAX_MESSAGES` / `...MAX_CHARS` controls. Providers own whether the slot is present; Chat Runtime transports the declaration and dispatches the quick-question hook.
User attachments are represented as AI SDK `FileUIPart` entries in `UIMessage.parts`; the OpenAI-compatible AI SDK provider converts those through `convertToModelMessages`, Codex maps image parts into app-server `UserInput` items, and text-only runtime providers reject unsupported parts explicitly instead of silently dropping attachments. Chat-owned context parts such as `data-cradle-skill` are persisted in the same snapshots but projected only by providers that support them.
`session_events` is the canonical chat-runtime fact log for session-owned run, message, queue, steer, provider-title, import, plan-implementation response, provider-target queue cleanup, and runtime interaction lifecycle transitions such as tool approval and user input requests. Streaming partial text is NOT a fact — it lives in overwritable `run_stream_checkpoints` and is promoted into `AssistantMessageCompleted` on crash recovery. Payloads are versioned domain facts (`v: 3`) hydrated through upcasters in `es/events.ts`; they do not import Drizzle row insert types. Aggregate `version` is monotonically increasing but not contiguous (legacy checkpoint-fact purge/filter may leave holes); consumers use `version > lastSeen`. `messages`, `backend_runs`, `chat_session_queue_items`, durable run snapshots, and runtime-owned `sessions` fields are same-transaction projections of those facts for HTTP reads, observability, notifications, and renderer hydration; interaction lifecycle facts are tail-only synchronization facts and do not write extra projection tables. Writes load the aggregate with `evolve`, validate invariants through `decide`, append with `expectedVersion`, project synchronously, then publish slim event-tail DTOs. Session metadata creation, archive, and read/unread remain owned by the session module; hard session deletion also deletes `session_events` and `run_stream_checkpoints` for those aggregates in the same transaction (projection tables cascade via FK).
Every top-level `RunStarted`, including `system` origin, participates in the same single-active-run aggregate invariant. Visible user-message append, assistant-message creation, queue claim metadata, and `RunStarted` commit atomically in one session-actor transaction, so rejected admission leaves no orphan transcript rows. The partial unique index on `backend_runs(chat_session_id) WHERE status = 'streaming'` is the final concurrency guard. `runRegistry` is only the live-resource/cancellation projection; correctness authority remains the fact log, same-transaction projections, and the database constraint.
Provider main-thread synthetic events are accepted concurrently but serialized by a session-owned inbox keyed by stable `providerTurnId`. The inbox installs its in-flight identity before awaiting run creation, preserves delivery order, creates at most one system run/assistant message, treats duplicate terminal delivery idempotently, observes queued handler rejection, and remains installed until durable completion and resource release.
`run/turn-completion.ts` is the single live-turn completion owner for normal, cancel, provider-synthetic, stale-fence, and shutdown outcomes. Its fixed barrier is durable terminal facts/projections, optional outcome-declared required bookkeeping for work that must block notification/handoff, observed best-effort usage/profile/trace/checkpoint/run-snapshot work, terminal notification, idempotent live-resource release, then exactly one queue or runtime-goal handoff. Run-snapshot finalization is forensic/diagnostic and stays best-effort after the durable terminal fact exists so snapshot failure cannot suppress terminal notification or queue/goal handoff. A durable terminal persistence failure triggers recovery for the exact active `runId`; recovery must prove a canonical durable terminal before completion continues. On recovery success, the canonical terminal, not the locally computed provider terminal, drives bookkeeping, notification, release, handoff, and the executor result. If recovery fails or cannot prove a terminal, Chat Runtime records an observability incident, blocks handoff, and propagates the lifecycle rejection. Every executor and stale-fence promise has an attached rejection observer.
`POST /chat/sessions/:sessionId/cancel` owns the canonical aborted transition: it marks active runs/messages terminal before asking the provider to stop, ignores late provider chunks after terminal state, and projects persisted `streaming` rows as `aborted` only for explicit user cancellation. Server startup treats orphaned persisted `streaming` runs as process interruptions, not user cancellation, and appends a deterministic `AssistantMessageCompleted` + `RunFailed` fact with `response.interrupted` using the run's `run_stream_checkpoints` content when present (otherwise the existing messages projection / empty assistant); the checkpoint row is deleted after promotion. Database migration first terminalizes inherited streaming rows before installing the unique index, and recovery uses a validation-scoped append path to add missing terminal facts for legacy multi-run storms without weakening normal admission. Terminal fact projection keeps `messages`, `chat_session_queue_items`, and `backend_run_snapshots` from staying in streaming/running states after crash or interrupted finalization. When historical projection-only runs are missing their start facts, recovery appends replayable `QueueItemEnqueued` / `RunStarted` bootstrap facts without re-projecting rows that already exist, then projects the terminal facts normally. Recovery also repairs historical terminal projection drift from `session_events` by replaying the matching stored terminal facts through the projector, and deletes stale checkpoint rows whose run already has a terminal fact. Database migration and persisted-run recovery finish before the server calls `listen`; recovery failure is a bootstrap failure, and background tasks/provider reconnection do not start against ambiguous run state. `es/rebuild.ts` can drop and rebuild `messages`, `backend_runs`, and `chat_session_queue_items` for one session from the log, then run parity; `scripts/chat-es-rebuild.ts <sessionId>` exposes this for local maintenance. Ordinary read/create/queue/steer/rollback APIs do not run opportunistic recovery. `GET` / `PATCH /chat/sessions/:sessionId/runtime-settings` reads and updates Cradle-owned `accessMode` / `interactionMode` controls plus the direct Chat Session Claude Agent model matrix override, persists access/interaction under `sessions.config_json.runtimeSettings` and the matrix under `sessions.config_json.claudeAgent.modelAliases`, and forwards live access/interaction updates only to providers that implement the runtime settings side-channel.
Runtime skill path discovery is cached per workspace for a short window so simultaneous chat turns do not repeatedly perform synchronous directory scans during provider startup. Providers still receive Cradle-owned skill paths only as read inputs; skill lifecycle stays under the skills namespace.
Runtime provider inputs carry `sessions.agentId` alongside `workspacePath` when a session is agent-scoped. Chat Runtime owns resolving that database context and forwarding it; provider directories own any runtime-specific filesystem interpretation such as Codex using the agent home as cwd.
Provider-native session titles are projected through the `StreamTurnInput.reportSessionTitle` callback. Chat Runtime owns the write into `sessions.title`; providers only report titles they read from their own runtime/session metadata. A provider title may replace the initial session title once, but later provider reports cannot rename an already-named Cradle session. This keeps the Cradle title stable when changing provider targets recreates the provider session. Explicit title regeneration uses the provider `generateSessionTitle` hook and intentionally overwrites an existing title only for that user-triggered action.
Route metadata includes `x-cradle-cli` descriptors for non-streaming generated CLI commands.
Chat stream traces are opt-in through `CRADLE_CHAT_STREAM_TRACE=1` and are written to `CRADLE_DATA_DIR/chat-runtime/traces/*.jsonl` with bounded payloads by default. Use `CRADLE_CHAT_STREAM_TRACE_FULL=1` only for short diagnostic runs because full provider raw events, mapper output, runtime chunks, and emitted AI SDK SSE frames can be very large.
Lightweight runtime profiling is opt-in through `CRADLE_CHAT_RUNTIME_PROFILE=1`; it logs per-run stream/finalize timings, final snapshot bytes, published chunk count, output diagnostics, and process memory deltas without writing extra trace files.
Chat Session follow-up ownership is split by transport semantics. `/chat/sessions/:sessionId/queue` stores durable follow-up turns in `chat_session_queue_items` (mode `queue` or `steer`), including per-item runtime settings snapshots and selected context parts for compatible runtimes, then drains those rows after the active run completes. `/chat/sessions/:sessionId/steer` is Synara-shaped: Codex (`steer: 'native'`) live-steers the active turn via `steerTurn` and persists `continuation.mode = "steer"`; non-Codex (`queue-fallback`) enqueues a front-of-queue steer item and interrupts the live turn so that item drains next. Product enqueue never mid-turn-pushes into a live provider input stream.
Codex app-server HTTP routes are owned here only as session-scoped Chat Runtime capabilities. `/chat/sessions/:sessionId/codex/app-server/capabilities` resolves the session and returns the provider-native capability manifest, `/invoke` performs one-shot provider-native calls, and `/stream` opens a provider-native notification stream. Chat Runtime owns the Cradle session/profile/runtime context resolution and persists the adapter-updated provider snapshot after an invoke. The Codex adapter owns the generated app-server method surface, JSON-RPC bridge execution, server-request/tool mapping, and provider-native state projection such as goal invoke results.
Provider tool envelope shape is owned by `../chat-runtime-providers/tools`, while provider-specific tool identity and payload semantics live in each provider directory such as `../chat-runtime-providers/claude-agent/tools` and `../chat-runtime-providers/codex/tools`. Provider adapters emit tool input/output payloads as `{ identifier, apiName, args, result }` envelopes while Chat Runtime only persists and streams the AI SDK chunks. Claude Agent `TodoWrite` state is carried inside the Claude Code tool result envelope as `result.pluginState.todos`, and the existing terminal tool message snapshot persistence writes that state into `messages.message_json`; the Claude Agent provider also stores the latest normalized TodoWrite list in its provider snapshot for the runtime-neutral `progress` UI slot.

## Durable Message Payload Ownership

Every durable write of a `UIMessage` goes through `toDurableMessagePayload`, which externalizes oversized bytes and then stringifies. This includes event projectors, external-session import, and thread handoff; no persist site may call `JSON.stringify` on a message directly, because that is how bytes silently disappeared before. Transactional writers pass their existing Drizzle handle into the seam so blob, ref, and message commit atomically.

Externalization is lossless. Inline attachment data URLs, oversized tool inputs/outputs, and text/reasoning that exceeds the shared per-message budget are written to the blob store, and the part keeps a recoverable reference: a `ChatBlobPayloadRef` for tool payloads, a `cradle-blob://` URL for files, and `providerMetadata.cradle.payloadRef` for prose overflow. `message_json` keeps a bounded prefix or preview so history reads and renderers stay cheap.

Browser-rendered blob bytes use `GET /chat/sessions/:sessionId/blobs/:blobId/content`. The session scope both verifies that the transcript owns the reference and lets linked-session proxying send the same URL to the remote Cradle server that owns the session. The route is part of OpenAPI/api-gen; renderers use its generated route type and URL builder, while `<img>`, Markdown images, and normal links navigate to the URL directly instead of downloading bytes through SDK JavaScript.

Anything model-facing must resolve those references first. `projectProviderInputMessage` / `projectProviderInputMessages` are the only sanctioned way to hand stored messages to a provider; they restore full bytes so a provider never receives an unresolved reference. When a referenced blob is missing, resolution substitutes an explicit unavailable marker carrying the original size and preview rather than a silent partial, so a model can tell that something is absent instead of reasoning over a truncated fragment as if it were complete.

Bounded transcript construction resolves and budgets one newest-first candidate at a time. The character budget is measured after blob rehydration, including data URLs and complete tool payloads, and only accepted messages stay resident. This avoids both under-counting compact reference objects and expanding an entire large session before the provider call.

Streaming checkpoints use the same lossless externalization as terminal messages because crash recovery can promote a checkpoint into a durable `AssistantMessageCompleted` fact. Each checkpoint writes blob, ref, and checkpoint state in one transaction; content addressing deduplicates unchanged payloads. Lossy bounding is reserved for AI observation records and must never be reachable from either durable writes or recoverable checkpoints. Reference lifetime is tracked in `chat_message_blob_refs`, and the blob GC deletes the row before the file so a live reference can never point at deleted bytes.

The backfill sweep converts historical rows under a terminating id-ascending cursor. It shrinks pre-existing legacy truncation markers in place, which is irreversible — those rows had already lost their original bytes before this seam existed, so only the surviving preview tail is given up.

## Files

- `index.ts`: Thin Elysia route aggregator for `http/*.routes.ts`.
- `http/*.routes.ts`: Route surface for response streaming, history reads, lifecycle commands, interactions, introspection, settings, and event-tail SSE.
- `model.ts`: Public `ChatRuntimeModel` schema aggregate used by the route modules.
- `model/common-schemas.ts`: Shared HTTP schemas for messages, queue items, run traces/snapshots, runtime catalog/model descriptors, provider threads, session-tail events, and request parts.
- `model/ui-slot-schemas.ts`: Runtime UI slot declaration/state schemas and provider-owned context-usage schemas.
- `runtime.ts`: Composition root for chat-runtime public APIs, dependency wiring, active-run streams, queue drain, side chat, cancellation, and recovery entry points.
- `bang-command.ts`: standard-runtime single-line Composer bang command executor; resolves the session workspace, spawns the command through the host shell with bounded stdout/stderr capture, handles timeout/request abort, and persists command/result user messages with Cradle-owned bang metadata.
- `pending-tool-approval.ts`: in-memory active-run registry for provider-native tool approval requests; resolves user decisions from the HTTP route back into the provider request promise and rejects pending approvals when a run ends.
- `es/event-store.ts`: append-only `session_events` persistence, optimistic `expectedVersion` checks, aggregate version assignment, and stored event hydration.
- `es/event-tail.ts`: Slim session/global event-tail DTO reads and in-process SSE subscriber fanout.
- `es/events.ts`: versioned chat session domain-fact payload contracts plus v1-to-current upcasters.
- `es/aggregate.ts`: pure in-memory `evolve` replay of session events for write-side state, active-run detection, queue state, and deterministic interrupted-run finalization.
- `es/decide.ts`: pure command decision layer for active-run, terminal-run, queue-claim, and rollback invariants before append.
- `es/projectors.ts`: synchronous same-transaction projections from session events into `sessions`, `messages`, `backend_runs`, `chat_session_queue_items`, and `backend_run_snapshots`.
- `es/session-actor.ts`: per-session serialized command execution so one session writes monotonically versioned events without concurrent SQLite writes.
- `es/commands.ts`: command helpers that decide, append, and project chat session events, including queue transitions, terminal run fact helpers, imported messages, provider-target queue cleanup, and last-turn rollback facts.
- `es/parity.ts`: projection parity checker that replays a session log through projectors inside a rollback-only transaction and classifies drift.
- `es/rebuild.ts`: session projection rebuild helper for dropping and replaying rebuildable chat projections from `session_events`.
- `es/recovery.ts`: interrupted-run finalization, missing terminal fact projection, and terminal projection drift repair through stored fact projection.
- `ui-message.ts`: trusted internal `UIMessage` helpers for stored message-JSON parsing, assistant/user message construction with text, context, and file parts, Codex internal goal-continuation metadata, bang command/result metadata, and text extraction. This file intentionally avoids Cradle-owned Zod schemas over AI SDK message internals. (Renamed from `message-snapshots.ts`; unrelated to the durable run snapshots in `run-snapshot.ts`.)
- `run-snapshot.ts`: durable run snapshot recorder/query helper; owns the Cradle harness envelope, bounded JSON payload persistence, event append, finalization, retention pruning, and run/session snapshot reads.
- `context-parts.ts`: Chat Runtime-owned structured context part helpers for validating and projecting Cradle-owned `data-cradle-*` message parts such as selected Skills.
- `transcript.ts`: Cradle-owned transcript reconstruction from stored `messages.message_json`, including bounded history selection, invalid-snapshot fallback, and structural `UIMessage` preservation for provider projectors.
- `message-durable-payload.ts`: the single durable serialization seam; externalizes oversized bytes and returns the `message_json` string every persist site writes.
- `message-blob-externalization.ts`: moves inline attachment bytes, oversized tool payloads, and prose overflow into the blob store, leaving recoverable references behind.
- `message-blob-backfill.ts`: background maintenance sweep that externalizes historical rows under a terminating id cursor and shrinks legacy truncation markers.
- `message-steer-split-backfill.ts`: terminating background sweep that compacts legacy steer `metadata.cradle.continuation.splitParts` rows (full tool/file copies → stream-boundary shape only).
- `message-snapshot-compaction.ts`: lossy payload bounding for disposable AI observation records only; not reachable from durable writes or recoverable stream checkpoints.
- `stream-trace.ts`: dev-mode JSONL trace writer/reader for the provider raw event, mapper output, runtime chunk, SSE emit, and run terminal chain.
- `pending-user-input.ts`: in-memory pending user input registry keyed by session and provider request id; owns request creation, duplicate/stale validation, run-scoped rejection, answer resolution, summary projection for live status/desktop notifications, and synthetic resolved tool-output publication for active run streams.
- `composer-drafts.ts`: server-authoritative Composer draft persistence with LWW revision increments, JSON payload storage, and soft-delete tombstones keyed by renderer `surfaceId`.
- `ui-message-input.ts`: provider-boundary helpers for extracting text from `UIMessage` and rejecting attachments for text-only runtimes.
- `harness/system-instructions.ts`: stable Cradle runtime identity (always) plus primary-Work closed-loop delivery mode (`getCradleWorkModeSystemInstructions`), appended only when the session is a primary Work thread.
- `harness/context-source-registry.ts`: composition-root registry through which domain owners contribute deterministic session context fragments without a Chat Runtime-to-domain dependency.
- `harness/provider-state.ts`: per-provider-thread fragment revision tracking and compaction invalidation.
- `harness/projection.ts`: System Prompt suffix fallback for providers without native persistent context injection.
- `harness/session-state.ts`: canonical all-Session runtime metadata fragment and content-derived revision.
- `harness/turn-context.ts`: stable System Prompt, registered session fragments, and bounded `CradleTurnTranscript` history resolution for ordinary turns.
- `provider-threads/live-streams.ts`: provider-native thread live chunk replay/fanout state, replay buffer limits, provider-turn start chunk synthesis, and subscriber cleanup.
- `run/final-message-projection.ts`: incremental `UIMessageChunk` to final assistant `UIMessage` projection, including text/reasoning flushing and tool input/output/approval part mutation.
- `run/stream-chunks.ts`: AI SDK stream chunk terminal detection, runtime and durable-snapshot coalescing keys, buffered SSE merge helpers, and terminal status projection.
- `run/errors.ts`: provider/runtime error serialization, session-title generation failure reporting, and turn failure observability code selection.
- `run/run-coordinator.ts`: create-run orchestration, runtime/session resolution, draft creation, active-run registration, snapshot start, and executor dispatch.
- `run/turn-executor.ts`: runtime stream pump and typed normal outcome construction; completion bookkeeping is classified before delegation to the completion owner.
- `turn-checkpoint-hooks.ts`: optional composition-root-registered checkpoint lifecycle contract used by the turn executor without importing the Turn Checkpoint owner.
- `run/turn-draft.ts`: event-sourced user/assistant draft turn creation from text/files/context or supplied AI SDK messages.
- `run/terminal-finalizer.ts`: durable terminal message/fact projection and post-barrier terminal notification primitives used only by the completion owner.
- `run/turn-completion.ts`: exactly-once completion owner and ordering barrier for terminal persistence, required/best-effort bookkeeping, notification, release, and queue/goal handoff.
- `run/active-run-snapshot.ts`: active-run snapshot start/event/finalization wrapper around durable run snapshots.
- `run/active-run-release.ts`: idempotent active-run memory/resource cleanup after the completion owner passes its notification barrier.
- `run/runtime-goal-continuation.ts`: Runtime goal continuation detection through provider hooks, backoff, timer scheduling, and dependency-injected continuation run startup.
- `run/snapshot-events.ts`: runtime-neutral run snapshot phase mapping, chunk payload summarization, event append, and terminal snapshot finalization.
- `run/output-diagnostics.ts`: provider output event counters and empty-output terminal validation for streamed turns.
- `run/profile.ts`: opt-in chat runtime profiling session that records stream/finalize timings, memory deltas, publication/final-message sizes, and output diagnostics.
- `run/usage.ts`: total and per-step token usage persistence plus model-cost estimation used by run finalization and snapshot events.
- `queue/session-queue.ts`: durable queue DTO/input types, queue row JSON parsing/serialization, persisted thinking-effort normalization, runtime-settings projection, and queue row ordering.
- `queue/drain.ts`: durable queue drain scheduling, event-sourced queue item claiming/release/failure transitions, and dependency-injected queued run startup.
- `stream/sse.ts`: buffered AI SDK `UIMessageChunk` SSE writer with snapshot bootstrap, terminal close handling, and bounded delta coalescing for run and provider-thread streams.
- `stream/checkpoint-store.ts`: ephemeral `run_stream_checkpoints` upsert/read/delete helpers for streaming partial-message state (not session-actor / event-transaction machinery).
- `stream/active-run-stream.ts`: active-run snapshot timer, checkpoint upsert, and live chunk publish/flush controller.
- `stream/run-chunk-sequencer.ts`: active-run-owned monotonic cursor, scalar publication diagnostics, and non-retaining live subscription interface shared by SSE and multiplex WebSocket transports.
- `stream/direct.ts`: direct async `UIMessageChunk` to SSE transport for stateless provider streams such as quick questions.
- `side-chat/create.ts`: live-only side conversation creation, parent runtime handle resolution, provider fork/start dispatch, host lease registration, and side conversation registry seeding.
- `side-chat/live-stream.ts`: live-only side conversation provider stream transport, final assistant projection, completion history callback, and best-effort cancellation.
- `chat-runtime-provider-registry.ts`: runtime provider registry for ACP Chat, Claude Agent, Codex, System Agent (`jar-core`), plugin runtimes, and debug/mock variants. It creates the shared `ProviderContext`, validates runtime contract shape and capability-hook consistency at registration time, registers builtin adapters through provider-owned factory functions, publishes runtime catalog metadata and optional health status, updates provider-kind compatibility, allows builtin-owned runtime replacement for tests while preserving plugin namespace protection, and keeps cached runtime skill path discovery under Chat Runtime because skills are resolved per chat workspace.
- `runtime-settings.ts`: JSON helpers for reading and writing session runtime settings and Claude Agent session matrix overrides without creating a separate session settings table.
- `runtime-settings-api.ts`: HTTP-facing service for `GET` / `PATCH /chat/sessions/:sessionId/runtime-settings`; merges access/interaction patches, writes optional Claude Agent matrix overrides, and attempts live access/interaction updates for active runtimes.
- `runtime-settings-model.ts`: TypeBox schemas for runtime settings and Claude Agent session matrix request/response payloads.
- `runtime-provider-types.ts`: chat runtime provider contracts, including static provider routing capabilities, runtime presentation descriptors, shared provider dependency context, structured provider error tags, runtime catalog metadata, UI slot surfaces, provider-owned UI slot state unions for goal, compact usage and lifecycle, plan, tool activity, MCP, model, reasoning, status, diff, terminal, approvals, alerts, filesystem, skills, plugin, search, crew, usage with account rate-limit windows, and config; provider-native thread list/read/turns/live-event and app-server hooks; optional `forkRuntimeSession`, `steerTurn`, title generation, shell execution, runtime settings update, UI slot state, dynamic capability, health-check, and dispose hooks; and the `reportSessionTitle` callback for provider-native title projection.

`../chat-runtime-engine` owns provider-agnostic AI SDK execution helpers, compaction, and provider selection. `../chat-runtime-providers` owns concrete backend adapters. Chat Runtime depends on both through narrow contracts and keeps HTTP routes, persistence, queueing, trace files, and run lifecycle semantics here.

## Provider MCP Ownership

The server MCP registry owns MCP server discovery for both plugin-registered servers and host-owned builtin servers such as Chronicle. Chat runtime providers in `../chat-runtime-providers` only read that registry when their upstream runtime supports MCP-style tool servers:

- Claude Agent receives the registry through SDK `mcpServers` query options.
- ACP receives the registry on `newSession`, `loadSession`, and `unstable_resumeSession` as ACP `McpServer[]`.
- Codex receives the registry through `config.mcp_servers`.

OpenAI-compatible and System Agent (`jar-core`) providers do not currently expose a compatible plugin-MCP injection surface in this module.

## Live Steer Ownership

Live-steer `splitParts` are compact stream boundaries containing only part type/tool identity and text or reasoning prefixes. Attachment URLs and tool payloads stay owned by the canonical assistant message instead of being copied into every steer; stored legacy boundaries are compacted during message hydration.

`/steer` is Synara-shaped. Codex (`steer: 'native'`) calls optional `ChatRuntime.steerTurn` on the matching active run, persists a completed user message with `metadata.cradle.continuation.mode = "steer"`, and does not open a second Cradle run. Non-Codex runtimes (`queue-fallback`, including Claude Agent and Kimi) enqueue a durable front-of-queue item with mode `steer` and interrupt the live turn so drain promotes that item next (annotated `continuation.mode = "steer"`). Unsupported runtimes reject `/steer`. Composer model / thinking / runtime settings describe future turns and queue items; they do not redefine live Codex steer identity for an already-running turn.

## Claude Durable Queue Projection

Product Composer Enter=queue is durable: Cradle stores the row and drains a new Run after the active turn settles. Low-level Claude UUID input-stream helpers (`submitNativeInput` / `cancelAsyncMessage`) remain on the live registry for tests and explicit native cancel, but `enqueueSessionQueueItem` does not auto-push into a live Query. Current-turn stop uses `interrupt()`; LinkCode-shaped detach applies when the interrupt ack precedes the cancelled turn's terminal `result`/EOF.

Chronicle registers a builtin `chronicle` MCP server from `../chronicle/mcp.ts`, exposing read-oriented memory, activity segment, and knowledge-card tools to the providers above. Chat Runtime remains a consumer of that registry and does not own Chronicle semantics or writes.

## Chronicle Context

Dynamic Chronicle memory injection is currently disabled in turn context resolution. If it is reintroduced, it must use a lower-volatility harness/message projection rather than rebuilding the stable System Prompt on every turn. Chronicle remains the authority for memory state and Chat Runtime must not infer durable memory state from model context.

## ACP Client Filesystem Ownership

ACP `fs.writeTextFile` writes to the client filesystem, not to Cradle-owned data. The legacy Cradle approval SSE owner has been removed from chat runtime, so `../chat-runtime-providers/acp/connection-manager.ts` must fail closed and leave the target file unchanged until ACP filesystem writes are represented as AI SDK tools with native `needsApproval`. Do not reintroduce a separate approval stream or wrapper for this path.
