Skip to content
Developer Preview

The item-revision fold model

Your client’s transcript is not sent to you as a document. It arrives as a stream of item events, and you build the transcript by folding them — applying each event to your local state, one at a time, in cursor order. This page is the fold rulebook: what each item event means, how streamed text accumulates, when to replace what you hold, and why the result is guaranteed to converge on the server’s truth.

The one-line invariant behind everything here: client state is a pure function of the events since your cursor. Two clients that fold the same events hold the same state; a snapshot is just the server running the same fold for you.

Items are the transcript entries — user messages, agent messages, reasoning, tool calls, and the other kinds. Every item event carries the full item object with these common fields: itemId (opaque, deterministic), kind (an open enum — render unknown kinds generically, never drop or crash), turnId, revision, status, and recordedAt.

Three lifecycle events carry full item objects, and one carries text appends:

  • item/started — a new item opens on the transcript.
  • item/updated — an open item changed non-terminally in a way appends cannot express; the full item is re-emitted at a higher revision.
  • item/completed — the item’s terminal, authoritative final object.
  • item/delta — a streamed text append to an open item; it bumps no revision.

The replace rule — replace-iff-higher-revision. revision is an integer, at least 1, monotonic per item. When a full item object arrives, replace your stored item if and only if the incoming revision is higher. That single comparison makes the fold safe against re-delivery and snapshot joins: an older or equal revision is never applied backwards. Also accept item/updated and item/completed for an itemId you never saw item/started for — single-record items emit only item/completed, and gap fills re-introduce items the same way.

Deltas accumulate; the committed object is the truth

Section titled “Deltas accumulate; the committed object is the truth”

item/delta is a UTF-8-safe append to one named field of the open item — field defaults to "text", and field paths are dotted for indexed fields: reasoning summary parts stream as summary.0, summary.1, and tool output streams as output. Appends are contiguous and lossless in cursor order, per field. The contract that keeps streaming honest: the concatenation of all deltas for a field path equals that field’s value on the final item/completed object.

Here is that contract on the real wire — two deltas, then the committed final object whose text is exactly their concatenation, at revision 2:

{"dir":"server","raw":"{\"jsonrpc\":\"2.0\",\"method\":\"item/delta\",\"params\":{\"sessionId\":\"0198f0aa-1111-7000-8000-0000000000aa\",\"viewCursor\":\"v:0198f0aa-1111-7000-8000-0000000000aa:7\",\"itemId\":\"0198f0ac-4242-7000-8000-000000000042\",\"field\":\"text\",\"delta\":\"All 214 tests pass\"},\"emittedAtMs\":1754590941100}"}
{"dir":"server","raw":"{\"jsonrpc\":\"2.0\",\"method\":\"item/delta\",\"params\":{\"sessionId\":\"0198f0aa-1111-7000-8000-0000000000aa\",\"viewCursor\":\"v:0198f0aa-1111-7000-8000-0000000000aa:8\",\"itemId\":\"0198f0ac-4242-7000-8000-000000000042\",\"field\":\"text\",\"delta\":\" except two in tbh-agent...\"},\"emittedAtMs\":1754590941200}"}
{"dir":"server","raw":"{\"jsonrpc\":\"2.0\",\"method\":\"item/completed\",\"params\":{\"sessionId\":\"0198f0aa-1111-7000-8000-0000000000aa\",\"viewCursor\":\"v:0198f0aa-1111-7000-8000-0000000000aa:9\",\"sourceRange\":{\"stream\":{\"kind\":\"run\",\"id\":\"018f6a1e-9b3c-7c21-a54a-2f30bd3c9f10\"},\"first\":{\"id\":\"018f9008-71aa-7000-8000-0000000000b9\",\"sequence\":108},\"last\":{\"id\":\"018f9008-71aa-7000-8000-0000000000b9\",\"sequence\":108}},\"item\":{\"itemId\":\"0198f0ac-4242-7000-8000-000000000042\",\"kind\":\"agentMessage\",\"turnId\":\"018f6a1e-9b3c-7c21-a54a-2f30bd3c9f10\",\"revision\":2,\"status\":\"completed\",\"recordedAt\":\"2026-08-07T18:42:31.400Z\",\"text\":\"All 214 tests pass except two in tbh-agent...\"}},\"emittedAtMs\":1754590942000}"}

Deltas are a streaming optimization, never the durable truth. They fold from ephemeral records, carry no sourceRange, may be opted out per-connection, and are never replayed by paged reads — a gap fill reconstructs final item states through item/completed and item/updated, not the keystroke stream. Design your client so losing every delta loses nothing durable: the committed object always restates the whole field. A saturated field stops emitting deltas and the final object carries truncated: true for that surface.

An item that emits item/started is guaranteed a later event with a terminal status — settled exactly once. Your state machine can treat an in-progress item as a promise. The one exception is abnormal host death: the process writes nothing further, and the missing terminals are reconciled on the next resume — folded to host-death-attributed terminals in the snapshot — rather than on the live connection. So never treat a live connection as the only path to a terminal; resume is the path that always works. On an ephemeral-durability host there is no resume, and after abnormal death the client itself must treat still-open items as terminal-unknown — a display state, never a wire status. See durability profiles.

A snapshot is the complete folded state at one cursor: every item at its latest revision, plus the active/queued turns, the pending approvals and user inputs, and the latest session-state values. Snapshot item objects and notification item objects share one schema per kind, which is what makes snapshot-plus-suffix a pure splice: apply the suffix events to the snapshot state and you get state byte-identical to a from-genesis fold for an un-elided snapshot — a conformance-suite property; a budget-elided snapshot matches modulo its declared elisions. How your client chooses between suffix, inline, and snapshot delivery on resume is covered on the resume page.

Everything above folds server events. A client that renders its own submission before the server echoes it back holds exactly one additional, strictly local state family: the pending-command entries. The full fold is state = f(state, server events, pending client events). Pending entries never appear on the wire, and four rules keep the family honest:

  • Anchor at insertion. An entry renders after the last item in your fold at submission time, and multiple entries render in submission order. That anchor is then fixed: server events folding in afterwards must not relocate it.
  • Retire on the commandId join. The entry retires when the server settles the command, and only then — the userMessage item carrying the same commandId folds in and replaces the entry at that item’s own transcript position, or a commandId replay answers a durable rejection, or a durable no-run settlement folds in: turn/unqueued carrying the same commandId says the queued submit was reclaimed (restore its text to the composer), and a turn/completed for the acked entry’s pre-minted turnId, with a failed terminal and no prior turn/started, says the launch itself failed — that event carries no commandId, so its join is the turnId your queued ack returned. A fold that waits only for the userMessage join keeps a ghost entry through both. The same join de-duplicates multi-client echo: a userMessage with no matching local entry is another client’s submission.
  • Errors that admitted nothing are not settlements. backpressured and the envelope errors mean nothing was admitted; the entry stays pending while you retry with the same commandId. If you stop retrying, retire the entry back into your composer rather than leave a durable-looking echo.
  • Never invent a terminal. A pending entry has no terminal of its own: a client must not locally time out, fail, or complete one. Optimistic rendering is sanctioned; an invented outcome is not.

Reconciling pending entries on reconnect. A reconnect resolves the pending family by asking the server, never by guessing:

  • Replay each acked entry’s commandId once — replay is always safe, and the settlement answer retires the entry per the rules above; an answer that is still a pending ack keeps the entry pending.
  • Resolve an unacked entry by resubmitting the same commandId once before retiring it to your composer: the resubmit de-duplicates against an intake the server may have durably written before the connection died, while a fresh commandId would execute the input twice.
  • When the reconnect serves a snapshot, the snapshot is authoritative: kept queued entries re-anchor after the last item of the reconciled snapshot fold, ordered among themselves by the snapshot’s queuedTurns order; an acked entry the snapshot no longer lists is resolved by the same commandId replay.

Each element of a view/page result’s events array is an unframed view notification: a two-member pair — method naming the event type and a params object holding the view notification’s params verbatim. jsonrpc and emittedAtMs are omitted, and viewCursor stays inside params, where every view notification already carries it. The muse exec --json parity lines use the identical nested pair.

The event inside is the same view notification content you receive on the live connection, and it folds by the same rules as the rest of this page.

The resume ladder and gap recovery live on the resume page; the wire framing of these notifications lives in the wire guide.