# `@dispatch/wire` — in-repo reference (read THIS, not node_modules) > MIRRORS the backend's `@dispatch/wire` package source so headless FE agents can read the wire > types WITHOUT following the `file:` dep symlink out of this repo (which hangs on a permission > prompt). Your CODE still imports `@dispatch/wire` normally — this file is for READING only. > > **Orchestrator:** SNAPSHOT of `wire@0.11.0` (compaction). Regenerate > whenever `@dispatch/wire` changes. > > **2026-06-22 delta (compaction handoff — package bumped `0.10.0` → `0.11.0`, ADDITIVE):** > adds `CompactionResult` — the result of a compaction operation (`summary`, `messagesSummarized`, > `messagesKept`). The summary text is the model's output; the FE doesn't render it directly (it > becomes the conversation's first system message after compaction). > > **2026-06-22 delta (conversation lifecycle handoff — package bumped `0.9.0` → `0.10.0`, ADDITIVE):** > adds `ConversationStatus` (`"active" | "idle" | "closed"`) — the per-conversation lifecycle > status. `ConversationMeta` gains a `status` field. `active` = a turn is generating; `idle` = > exists, not generating; `closed` = dismissed (hidden from the tab bar). Transitions are > backend-owned: `idle → active` on turn start, `active → idle` on turn settle, `→ closed` on > `POST /conversations/:id/close`. Pushed to all WS clients via `conversation.statusChanged` > (see `transport-contract@0.14.0`). > > **2026-06-21 delta (conversation.open handoff — package bumped `0.8.0` → `0.9.0`, ADDITIVE):** > adds `ConversationMeta` — metadata for a conversation (id, title, createdAt, lastActivityAt), > returned by `GET /conversations` (the list endpoint, see `transport-contract@0.13.0`). > > **2026-06-21 delta (message-queue + steering handoff — package bumped `0.7.0` → `0.8.0`, ADDITIVE):** > adds the per-conversation **message queue** + **steering** feature. While a turn is GENERATING, > a client enqueues a user message (via the `chat.queue` WS op or `POST /conversations/:id/queue`, > see `transport-contract@0.12.0`); it is delivered mid-turn as **steering** — injected at the next > tool-result boundary so the model sees it alongside the tool results and can adjust course. If the > turn ends with a non-empty queue (no tool call fired), the queue is carried into a NEW turn as its > opening prompt (no `steering` event — the new turn's `user-message` covers it). > > Adds: > - **`QueuedMessage`** (`{ id, text, queuedAt }`) — a message held in the queue (stable id for UI > keying + dedup). > - **`QueuePayload`** (`{ messages: QueuedMessage[] }`) — the payload of the message-queue > extension's per-conversation `custom` surface field (`rendererId: "message-queue"`). Carried on > the SURFACE channel (NOT the chat stream) — the queue is control/state. Empty `messages` = empty > queue. See `transport-contract.reference.md` for the surface + the enqueue op. > - **`TurnSteeringEvent`** (`{ type: "steering"; conversationId; turnId; text }`) — a NEW > `AgentEvent` union member, emitted on the chat stream when the kernel drains a non-empty queue > at a tool-result boundary. Render `text` as a USER bubble in the transcript (positioned after > the tool-result it followed); the queue surface separately clears on drain. One event per drain; > `text` is the combined text of all drained messages. Late-join safe (buffered into the in-flight > turn's event buffer, mirroring `user-message`). Carry-to-new-turn does NOT emit `steering`. > ADDITIVE to the union — if you have an exhaustive `AgentEvent` switch, add a `steering` case. > > **2026-06-12 delta (reasoning-effort handoff — package bumped `0.6.1` → `0.7.0`, ADDITIVE):** > adds the **`ReasoningEffort`** type — the per-request thinking-depth ladder > `"low" | "medium" | "high" | "xhigh" | "max"`. Provider-agnostic; the Anthropic provider maps > levels to extended-thinking token budgets (low 4096 · medium 10240 · high 16384 · xhigh 32768 · > max 65536); providers without a thinking knob ignore it. Resolution is SERVER-owned (do not > re-implement): per-turn `ChatRequest.reasoningEffort` override → persisted per-conversation value > (`GET`/`PUT /conversations/:id/reasoning-effort`, see `transport-contract@0.11.0`) → default > `"high"`. Higher levels mean longer runs of `reasoning-delta` events before the first text delta. > See the `ReasoningEffort` definition below. > > **2026-06-12 delta (CR-5 history windowing — package bumped `0.6.0` → `0.6.1`, DOC-ONLY):** the > per-conversation `seq` numbering is now a WRITTEN CONTRACTUAL GUARANTEE on `StoredChunk`: > **1-based, monotonic, gap-free** — a conversation's first chunk is always `seq === 1` and > numbering never skips. A client holding only a windowed suffix of the log derives "older chunks > exist server-side" purely from `oldestLoaded.seq > 1` (no `earliestSeq`/`hasOlder` field exists). > > **2026-06-12 delta (CR-3 user-message handoff — package bumped `0.5.0` → `0.6.0`, ADDITIVE):** adds a > new `AgentEvent` union member `TurnInputEvent` (`{ type: "user-message"; conversationId; turnId; text }`) > that surfaces the turn's USER prompt INTO the outward event stream. Emitted ONCE as the FIRST event of > every turn (before `turn-start`), so it is buffered + replayed to every subscriber — live AND late-join > — and rides `chat.delta`/NDJSON like any other event. Fixes CR-3 (a pure watcher couldn't see the prompt > until seal). The sender still echoes its own prompt optimistically, so consumers DE-DUP against that > (by text); a pure watcher renders it directly. Persistence/metrics unchanged. See `TurnInputEvent` below. > > **2026-06-12 delta (context-size handoff — package bumped `0.4.0` → `0.5.0`):** adds an OPTIONAL > `contextSize?: number` to BOTH `TurnDoneEvent` (live `done`) and `TurnMetrics` (persisted) — the > turn's FINAL step `inputTokens + outputTokens` (current context occupancy), NOT the aggregate > `usage` (which overcounts multi-step turns). The two carriers are equal for the same turn. Current > value = the LATEST turn's `contextSize`; `undefined` ⇒ render "unknown", never `0`. See the field > doc-comments on `TurnMetrics`/`TurnDoneEvent` below. > > **0.3.0 changes (token + timing metrics):** > - **Live per-step/per-turn telemetry on the event stream** (transient — NOT persisted): > `TurnUsageEvent` gained an OPTIONAL `stepId?` (attribute tokens per step). A NEW > `TurnStepCompleteEvent` (`type: "step-complete"`, REQUIRED `stepId`) carries the per-step > generation timing `ttftMs?` / `decodeMs?` / `genTotalMs?` (all optional — present only when the > runtime had a clock; `ttftMs`/`decodeMs` additionally require a first content token). `TurnDoneEvent` > gained an OPTIONAL `durationMs?` (total turn wall-clock) + OPTIONAL `usage?` (aggregate across > steps). `TurnToolResultEvent` gained an OPTIONAL `durationMs?` (tool execution time). > - **Durable, replayable metrics** (persisted, keyed per turn): NEW `StepMetrics` + `TurnMetrics` > — the persisted counterparts of the live `usage` + `step-complete` + `done` packets. Served by > `GET /conversations/:id/metrics` (see `transport-contract.reference.md`). Build the SAME > `TurnMetrics` shape from the live events for the in-flight turn; the durable endpoint supplies it > for sealed turns. TPS is derived (`usage.outputTokens / (genTotalMs / 1000)`), not on the wire. > - **0.2.0 (still current — step grouping):** `ToolCallChunk`/`ToolResultChunk` carry an OPTIONAL > `stepId?: StepId`; `TurnToolCallEvent`/`TurnToolResultEvent` carry a REQUIRED `stepId: StepId`. > Group batched/parallel tool calls by `stepId` equality. Live: read `event.stepId`. Replay: read > `storedChunk.chunk.stepId` (NOT the envelope; tolerate absence). `StoredChunk` envelope is > UNCHANGED (`{ seq, role, chunk }` — carries NO `turnId`). ```ts /** * @dispatch/wire — pure wire types shared by the kernel, the transport * contract, and out-of-repo clients (the web frontend). * * Types ONLY: zero runtime, zero `@dispatch/*` dependencies, so a client can * depend on the wire without pulling the kernel runtime. */ // ─── Conversation model ───────────────────────────────────────────────────── /** Who produced a message. */ export type Role = "system" | "user" | "assistant" | "tool"; /** Opaque identifier for a turn (one user→assistant cycle). */ export type TurnId = string & { readonly __brand: "TurnId" }; /** * Opaque identifier for a step (one LLM round-trip within a turn). It is the * authoritative grouping key for the tool calls a model batches together in a * single step (parallel/batched calls): every `tool-call`/`tool-result` event * and every persisted tool chunk (`ToolCallChunk`/`ToolResultChunk`) from the * same step carries the SAME `stepId`, so a client groups a batch purely by * equality — identically on the live stream and in replayed history. Per-turn * unique and gap-free in step order; treat it as opaque (do not parse it). The * runtime derives it deterministically from the turn id + 0-based step index. */ export type StepId = string & { readonly __brand: "StepId" }; /** * A chunk is one ordered piece of a message — the atomic unit of the * append-only conversation log. Discriminated by `type`. */ export type Chunk = | TextChunk | ThinkingChunk | ToolCallChunk | ToolResultChunk | ErrorChunk | SystemChunk; /** A piece of plain text content from the assistant or user. */ export interface TextChunk { readonly type: "text"; readonly text: string; } /** A piece of model reasoning / thinking content (e.g. extended thinking). */ export interface ThinkingChunk { readonly type: "thinking"; readonly text: string; } /** * A model's request to run a tool. The kernel routes by `name`; the tool * implementation never sees this directly — it receives parsed `input` via * `ToolContract.execute`. */ export interface ToolCallChunk { readonly type: "tool-call"; readonly toolCallId: string; readonly toolName: string; readonly input: unknown; /** * The step that produced this call — generation provenance stamped by the * runtime when the model emits the call (NOT storage metadata like `seq`, * which is why it lives on the chunk and travels with it through persistence * and replay). Tool calls a model batches together in one step share the same * `stepId`: the grouping key for rendering a parallel batch as one unit, and * equal to the `stepId` on the matching `tool-call` AgentEvent. Optional: * absent on chunks reconstructed outside a turn and on rows persisted before * this field existed, so a consumer must tolerate its absence (render * ungrouped). */ readonly stepId?: StepId; } /** * The result of a tool execution, attributed to the originating tool-call id. * The kernel guarantees every tool-call chunk gets exactly one result chunk * (synthesized if interrupted — see reconcile). */ export interface ToolResultChunk { readonly type: "tool-result"; readonly toolCallId: string; readonly toolName: string; readonly content: string; readonly isError: boolean; /** * The step that produced the originating call — equal to the `stepId` on the * matching `tool-call` chunk (same `toolCallId`) and on the `tool-result` * AgentEvent, so a consumer groups a step's calls with their results. * Generation provenance, not storage metadata (see `ToolCallChunk.stepId`). * Optional for the same reasons; `reconcile` copies it from the originating * call onto a synthesized (interrupted) result. */ readonly stepId?: StepId; } /** An error that occurred during generation or tool dispatch. */ export interface ErrorChunk { readonly type: "error"; readonly message: string; readonly code?: string; } /** * A system-injected message (e.g. system prompt, context assembly output). * Kept distinct from text so the log records provenance. */ export interface SystemChunk { readonly type: "system"; readonly text: string; } /** * A chat message: a role plus an ordered sequence of chunks. Messages are the * unit passed to and from the provider; chunks are the unit persisted and * rendered. */ export interface ChatMessage { readonly role: Role; readonly chunks: readonly Chunk[]; } /** * A persisted chunk plus its sync metadata. The append-only conversation log * stamps every chunk with a **1-based**, monotonic, gap-free, per-conversation * `seq` (the sync cursor, assigned in append order) and records the `role` of * the message it belongs to. This makes a flat seq-ordered stream both * incrementally syncable ("give me chunks after seq N") and regroupable into * messages by the client. * * The 1-based start is a CONTRACTUAL GUARANTEE (not an implementation detail): * a conversation's first chunk is always `seq === 1` and numbering never skips, * so a client holding only a windowed suffix of the log can derive "older * chunks exist server-side" purely from `oldestLoaded.seq > 1` — no separate * has-older flag is needed (or provided). `chunk` is the content unit — `Chunk` carries no storage/sync cursor * (`seq` lives here on the envelope, not on the chunk, since it is assigned by * the store and the provider has no use for it). A chunk MAY still carry * generation provenance assigned at production time (e.g. a tool chunk's * `stepId`), which is intrinsic to the content and so travels with it. */ export interface StoredChunk { readonly seq: number; readonly role: Role; readonly chunk: Chunk; } // ─── Reasoning effort ─────────────────────────────────────────────────────── /** * The per-request thinking-depth knob: how much extended thinking / reasoning * the model should spend before answering. Provider-agnostic ladder; each * provider maps a level to its native knob in its own code (e.g. an Anthropic * provider maps it to a `thinking.budget_tokens` value) and MAY ignore levels * (or the field entirely) that its backend cannot express. * * Resolution (owned by the session-orchestrator): per-turn request value → * persisted per-conversation value → default `"high"`. */ export type ReasoningEffort = "low" | "medium" | "high" | "xhigh" | "max"; // ─── Usage ────────────────────────────────────────────────────────────────── /** * Token usage counters for a single step. All fields are counts of tokens. * Cache fields are optional because not all providers expose cache metrics. */ export interface Usage { readonly inputTokens: number; readonly outputTokens: number; readonly cacheReadTokens?: number; readonly cacheWriteTokens?: number; } // ─── Persisted metrics ─────────────────────────────────────────────────────── /** * Durable per-step metrics for a completed step — the persisted, replayable * counterpart of the live `usage` + `step-complete` events. Combines the step's * token usage with its generation timing so a client reopening a past * conversation renders the same per-step token/latency breakdown it would have * seen live. Built from the turn's events, stored by `conversation-store`, and * served by `GET /conversations/:id/metrics`. */ export interface StepMetrics { readonly stepId: StepId; /** The step's token usage (all four counters; cache fields optional per `Usage`). */ readonly usage: Usage; /** Time to first token (stream start → first text/reasoning delta). Optional — see `TurnStepCompleteEvent.ttftMs`. */ readonly ttftMs?: number; /** Decode time (first token → stream end). Optional — see `TurnStepCompleteEvent.decodeMs`. */ readonly decodeMs?: number; /** Total generation time for the step (stream start → stream end). Optional: present only when a clock was available. */ readonly genTotalMs?: number; } /** * Durable per-turn metrics for a completed (sealed) turn — the persisted, * replayable counterpart of the live `done` event's aggregate `usage` + * `durationMs`, plus the per-step breakdown. `usage` is the aggregate across all * steps; `steps` carries each step's `StepMetrics` in step order. Stored by * `conversation-store` keyed by `turnId` and served by * `GET /conversations/:id/metrics`. (`turnId` is the plain wire string carried * on every `AgentEvent`, the join key to the live stream.) */ export interface TurnMetrics { readonly turnId: string; /** Aggregate token usage across all steps in the turn. */ readonly usage: Usage; /** Total wall-clock duration of the turn (turn start → turn end). Optional: present only when a clock was available. */ readonly durationMs?: number; /** Per-step metrics in step order. */ readonly steps: readonly StepMetrics[]; /** * **Context size** — tokens the conversation occupies as of this turn: the * turn's FINAL step `inputTokens + outputTokens` (the last entry of `steps`), * NOT the aggregate `usage` (which sums per-step prompts and overcounts a * multi-step turn). The persisted, replayable counterpart of * `TurnDoneEvent.contextSize` and equal to it for the same turn. A client * reopening a past conversation reads the LAST turn's `contextSize` as the * current context usage. Optional: absent when no per-step usage was available. */ readonly contextSize?: number; } // ─── Message queue + steering ─────────────────────────────────────────────── /** * A user message held in a conversation's message queue, awaiting mid-turn * steering delivery. The message-queue extension owns the queue and exposes it * as a per-conversation `custom` surface field; this type is the shared shape * the surface payload, the enqueue response, and the extension's service all * use (so a separate frontend repo can depend on the wire alone to render it). */ export interface QueuedMessage { /** Stable id (client-visible) for UI keying + dedup. */ readonly id: string; /** The message text the client enqueued. */ readonly text: string; /** When the message was enqueued (epoch-ms). */ readonly queuedAt: number; } /** * The payload of the message-queue extension's per-conversation `custom` * surface field (`rendererId: "message-queue"`): the current queue snapshot a * frontend renders. Carried on the SURFACE channel (NOT the chat stream) — the * queue is control/state, distinct from turn content. An empty `messages` * array means the queue is empty (no pending steering). The frontend moves a * message from this queue surface into the transcript when it is drained (the * surface clears) and/or when the matching `TurnSteeringEvent` arrives. */ export interface QueuePayload { readonly messages: readonly QueuedMessage[]; } // ─── Outward events ───────────────────────────────────────────────────────── /** * The union of all events the runtime emits outward during a turn. * Consumers (transport, persistence, notifications) pattern-match on `type`. */ export type AgentEvent = | StatusEvent | TurnStartEvent | TurnInputEvent | TurnTextDeltaEvent | TurnReasoningDeltaEvent | TurnToolCallEvent | TurnToolResultEvent | TurnToolOutputEvent | TurnUsageEvent | TurnStepCompleteEvent | TurnErrorEvent | TurnDoneEvent | TurnSealedEvent | TurnSteeringEvent; /** Status change for a conversation (e.g. idle → running). */ export interface StatusEvent { readonly type: "status"; readonly conversationId: string; readonly status: string; } /** A turn has begun. */ export interface TurnStartEvent { readonly type: "turn-start"; readonly conversationId: string; readonly turnId: string; } /** * The user prompt that opened this turn, surfaced INTO the turn's outward event * stream so a WATCHER (subscribed but not the sender) can render the prompt * mid-turn — the user message is otherwise persisted only at seal. Emitted ONCE * as the FIRST event of the turn (before `turn-start`); buffered + replayed to * every subscriber (live + late-join). The sender echoes its own prompt * optimistically, so DE-DUP against that (by text); a pure watcher renders it * directly. Carries the raw `text` passed to the provider. (Turn-scoped: it * carries `turnId`, so a multi-turn transcript attributes each prompt to its turn.) */ export interface TurnInputEvent { readonly type: "user-message"; readonly conversationId: string; readonly turnId: string; readonly text: string; } /** Incremental text content from the model during a turn. */ export interface TurnTextDeltaEvent { readonly type: "text-delta"; readonly conversationId: string; readonly turnId: string; readonly delta: string; } /** Incremental reasoning / thinking content during a turn. */ export interface TurnReasoningDeltaEvent { readonly type: "reasoning-delta"; readonly conversationId: string; readonly turnId: string; readonly delta: string; } /** The model has requested a tool to be run. */ export interface TurnToolCallEvent { readonly type: "tool-call"; readonly conversationId: string; readonly turnId: string; /** * The step that produced this call. Tool calls a model batches together in * one step share the same `stepId` — the grouping key for rendering a * parallel batch as one unit. Matches the `stepId` on the matching * `tool-result` event and on the persisted tool chunk * (`StoredChunk.chunk.stepId`). */ readonly stepId: StepId; readonly toolCallId: string; readonly toolName: string; readonly input: unknown; } /** A tool has completed execution. */ export interface TurnToolResultEvent { readonly type: "tool-result"; readonly conversationId: string; readonly turnId: string; /** * The step that produced the originating call. Equal to the `stepId` on the * matching `tool-call` event (same `toolCallId`) and on the persisted tool * chunk (`StoredChunk.chunk.stepId`), so a client groups a step's calls with * their results. */ readonly stepId: StepId; readonly toolCallId: string; readonly toolName: string; readonly content: string; readonly isError: boolean; /** * How long the tool took to execute (dispatch → result), in milliseconds — * the backend's authoritative execution time, distinct from any client-side * wall-clock. Optional: present only when the runtime was given a clock. */ readonly durationMs?: number; } /** Streaming output from a tool execution (e.g. shell stdout/stderr). */ export interface TurnToolOutputEvent { readonly type: "tool-output"; readonly conversationId: string; readonly turnId: string; readonly toolCallId: string; readonly data: string; readonly stream: "stdout" | "stderr"; } /** Token usage for the current step or turn. */ export interface TurnUsageEvent { readonly type: "usage"; readonly conversationId: string; readonly turnId: string; /** * The step this usage report belongs to, so a consumer can attribute tokens * per step (and join with the matching `step-complete` timing by `stepId`). * Optional: absent when the runtime had no step context, and on usage emitted * before this field existed. */ readonly stepId?: StepId; readonly usage: Usage; } /** * A step (one LLM round-trip) has completed — the authoritative per-step metrics * packet, emitted once at the step's end (after the generation stream finishes), * so its timing is final (unlike `usage`, which may arrive mid-stream). Carries * the step's generation timing; join to the step's tokens via `stepId` on the * `usage` event. All timing fields are optional: present only when the runtime * was given a clock, and `ttftMs`/`decodeMs` additionally require that a first * content token (text or reasoning) was observed this step. */ export interface TurnStepCompleteEvent { readonly type: "step-complete"; readonly conversationId: string; readonly turnId: string; readonly stepId: StepId; /** Time to first token: stream start → first text/reasoning delta. */ readonly ttftMs?: number; /** Decode time: first token → stream end (generation total − TTFT). */ readonly decodeMs?: number; /** * Total generation time for the step: stream start → stream end. Present * whenever a clock was available, even if no first token was seen (in which * case `ttftMs`/`decodeMs` are absent). When a first token was seen, * `genTotalMs === ttftMs + decodeMs`. */ readonly genTotalMs?: number; } /** An error occurred during the turn. */ export interface TurnErrorEvent { readonly type: "error"; readonly conversationId: string; readonly turnId: string; readonly message: string; readonly code?: string; } /** The turn has completed (model finished generating). */ export interface TurnDoneEvent { readonly type: "done"; readonly conversationId: string; readonly turnId: string; readonly reason: string; /** * Total wall-clock duration of the turn (turn start → turn end), in * milliseconds. Optional: present only when the runtime was given a clock. */ readonly durationMs?: number; /** * Aggregate token usage across all steps in the turn — a convenience total so * a consumer need not sum the per-step `usage` events. Optional (absent if the * provider reported no usage). */ readonly usage?: Usage; /** * **Context size** — tokens the conversation occupies right now: the turn's * FINAL step `inputTokens + outputTokens` (the prompt sent into the last LLM * round-trip plus that round-trip's output). This is the "tokens in context" * figure a client renders as the chat's current context usage, and a client * treats the LATEST turn's value as the live total. * * Deliberately NOT the aggregate `usage` above: `usage` SUMS each step's * `inputTokens`, which overcounts a multi-step / tool-calling turn because every * step re-prefills the growing prompt — the final step's input already includes * all prior context, so its input+output is the true occupancy. Optional: absent * when no per-step usage was observed this turn (mirrors `usage`). A later field * will carry the model's max context-window LIMIT; this is only the current size. */ readonly contextSize?: number; } /** * The turn has been sealed — all chunks persisted, history is final. * This is the hook point for post-turn extensions (compaction, cache-warm). */ export interface TurnSealedEvent { readonly type: "turn-sealed"; readonly conversationId: string; readonly turnId: string; } /** * A steering message was injected into an in-flight turn at the tool-result * boundary (the model sees it alongside the tool results and may adjust * course). Drawn from the conversation's message queue (which the drain * clears); the cleared queue arrives as a message-queue SURFACE update, while * THIS event carries the injected `text` so a frontend can place a user bubble * in the transcript live — and so a late-joining watcher sees it before seal * (mirroring `TurnInputEvent` for the opening prompt; emitted into the * in-flight buffer by the session-orchestrator). * * Emitted by the session-orchestrator (in its `drainSteering` wrapper) only * when the kernel drained a non-empty queue at a tool-result boundary. If the * turn instead ENDS with a non-empty queue (no tool call fired), the queue is * carried into a NEW turn whose opening `user-message` event covers the * transcript — so no `steering` event is emitted in that case. One `steering` * event per drain; the combined text of all drained messages. */ export interface TurnSteeringEvent { readonly type: "steering"; readonly conversationId: string; readonly turnId: string; readonly text: string; } // ─── Conversation metadata ─────────────────────────────────────────────────── /** * The per-conversation lifecycle status. `active` = a turn is generating; * `idle` = exists, not generating; `closed` = dismissed (hidden from the tab * bar, not deleted). Transitions are backend-owned and pushed via the * `conversation.statusChanged` WS message (see `transport-contract`). */ export type ConversationStatus = "active" | "idle" | "closed"; /** * Metadata for a conversation, returned by `GET /conversations` (the list * endpoint). The title defaults to the first user message (truncated) and can * be set via `PUT /conversations/:id/title`. `createdAt` is set on first write; * `lastActivityAt` is updated on every append. */ export interface ConversationMeta { readonly id: string; readonly createdAt: number; readonly lastActivityAt: number; readonly title: string; readonly status: ConversationStatus; /** Points to the archive conversation with full pre-compaction history. */ readonly compactedFrom?: string; } // ─── Compaction ────────────────────────────────────────────────────────────── /** * Result of a compaction operation. `summary` is the text the model produced; * `messagesKept` is how many recent messages were retained after the summary; * `messagesSummarized` is how many old messages were replaced by the summary. */ export interface CompactionResult { readonly summary: string; readonly newConversationId: string; readonly messagesSummarized: number; readonly messagesKept: number; } ```