Expand description
Transcript resume: the conversation surface as an ordered, role-threaded
projection of the oplog (slice B2 of
docs/proposals/multi-device-sync.md), hardened by kernel review.
§What this closes
docs/solutions/conversation-persistence-removed-in-0.25.md records that
the disk-backed ConversationStore island was removed in 0.25 — dead
code with a latent compaction-vs-store incoherence bug (compaction
summarized the in-memory graph but never wrote the summaries back to the
JSONL store, so a resume would have reloaded stale raw turns). That doc’s
forward path: “an append-only oplog as the source of truth with files/caches
as projections.” B2 is that path, oplog-native — a conversation is a
projection of ordered turn ops, not a resurrected second store.
§The model: a transcript is a projection, not a store
A conversation turn is an crate::oplog::OpRecord on the
crate::oplog::Surface::Conversation surface. The transcript is a pure
fold of those ops: filter by conversation_id, order by the canonical
(hlc, op_id) total order the crate already agrees on
(crate::fold::SyncState::log_entries), project each payload into a typed
Turn. One source of truth; no second store to drift.
§Turn identity: an EVENT STREAM (op_id-keyed), not a content entity
Kernel-review correction (reversed from the first B2 cut). A conversation
turn folds as an event-stream multiset keyed by op_id
(crate::oplog::Surface::is_event_stream returns true for
Conversation), NOT by content. The content-keyed first cut had a
reproduced silent data-loss bug: stable_key hashes the caller’s
payload — {conversation_id, role, content, timestamp} — so two genuine
user “yes” turns stamped at the same payload timestamp (second-granularity
stamps, a cached now(), a rapid double-confirm) collapsed to ONE entry.
The earlier justification (“a repeated utterance differs in timestamp / the
HLC advances”) was wrong: the fold keyed on the payload timestamp, not the
HLC.
The right identity: a turn has exactly one author and propagates by op
replication, so op identity IS turn identity. Keyed by op_id, a resent
op dedups (retransmission), while two distinct authorings never collapse —
even byte-identical ones. This reuses B1’s multiset machinery (the same the
routing observations use). Conversation differs from routing only in that
its entries are independent (no path-dependent EMA replay), so it
tolerates LastN retention where routing forbids any trim — see
crate::oplog::Surface::is_replay_stream.
§Ordering across devices
HLC gives causal order; two devices talking to the same agent concurrently
interleave deterministically by Hlc’s derived Ord
((wall_ms, counter, device_id)), tie-broken on op_id. A turn that
causally follows another (its writer observed it) always sorts after it;
genuinely concurrent turns fall back to the stable device_id tiebreak. So
every device reconstructs a byte-identical transcript from any delivery
order.
But determinism ≠ provider-validity (the second kernel-review defect).
Causal order says nothing about concurrent turns: two devices each replying
to the same user turn yield [user, assistant, assistant] — which Anthropic
400s. So crate::fold::SyncState::resume_messages does not emit the raw
transcript; it runs a repair that guarantees a provider-valid Message
sequence (the “runtime validates” thesis applied to the projection): adjacent
same-role turns are coalesced, an orphan tool_result (one not answering a
preceding assistant tool_call — e.g. a LastN window that cut inside a
tool exchange) is dropped, and a dangling assistant tool_call with no
following tool_result has its calls stripped. See repair.
§Compaction coherence (why the 0.25 bug cannot return)
Conversation retention is B4’s RetentionRule::LastN over the folded
snapshot; the checkpoint keeps the last N turns by timestamp and older raw
turns drop from the read model. The 0.25 incoherence was structural — two
stores on two write paths. B2 has one source of truth (the oplog) and the
transcript is a projection of the same folded state B4’s checkpoint
serializes. apply_retention over the compacted device’s state equals
apply_retention over a fresh full fold (byte- and hash-identical). The
semantic summarization of aged-out turns lives in memgine
(ConversationSummary nodes, a B6 concern); B2 supplies the ordered raw
turns it summarizes and the last-N window, nothing lossy.
§The resume bridge
Turn::to_message builds each turn as the real
car_inference_types::Message (user / assistant {content, tool_calls}
/ tool_result {tool_use_id, content}), and resume_messages returns the
repaired Vec<Message> car-inference’s multi-turn path replays. Because
car-sync depends on the shared car-inference-types crate — not a hand-copied
mirror — a change to Message’s shape is a compile error here, not a
runtime from_value::<Message> break in the daemon. The daemon/memgine
adoption (feeding these into the engine’s multi-turn path) is B6.
Structs§
- Turn
- One folded, ordered transcript turn — the typed projection of a
crate::oplog::Surface::Conversationop.
Enums§
- Role
- A conversation turn’s role. Serializes snake_case (
user/assistant/tool);Turn::to_messagemapsToolonto theMessage::ToolResultrole.
Constants§
- DEFAULT_
CONVERSATION - The conversation a turn belongs to when its payload carries no explicit
conversation_id(e.g. the legacy{speaker, text, timestamp}turns the B4 tests emit). Such turns fold into one unnamed default transcript.
Functions§
- repair
- Repair an ordered transcript into a provider-valid
Messagesequence.