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 and revisions
Section titled “Items and revisions”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 higherrevision.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.
Every item that starts, terminates
Section titled “Every item that starts, terminates”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.
Snapshots are the same fold, precomputed
Section titled “Snapshots are the same fold, precomputed”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.
Your own in-flight commands
Section titled “Your own in-flight commands”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
commandIdjoin. The entry retires when the server settles the command, and only then — theuserMessageitem carrying the samecommandIdfolds in and replaces the entry at that item’s own transcript position, or acommandIdreplay answers a durable rejection, or a durable no-run settlement folds in:turn/unqueuedcarrying the samecommandIdsays the queued submit was reclaimed (restore its text to the composer), and aturn/completedfor the acked entry’s pre-mintedturnId, with a failed terminal and no priorturn/started, says the launch itself failed — that event carries nocommandId, so its join is theturnIdyour queued ack returned. A fold that waits only for theuserMessagejoin keeps a ghost entry through both. The same join de-duplicates multi-client echo: auserMessagewith no matching local entry is another client’s submission. - Errors that admitted nothing are not settlements.
backpressuredand the envelope errors mean nothing was admitted; the entry stays pending while you retry with the samecommandId. 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
commandIdonce — 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
commandIdonce 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 freshcommandIdwould 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
queuedTurnsorder; an acked entry the snapshot no longer lists is resolved by the samecommandIdreplay.
The paged-read element shape
Section titled “The paged-read element shape”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.
Where this page stops
Section titled “Where this page stops”The resume ladder and gap recovery live on the resume page; the wire framing of these notifications lives in the wire guide.