Skip to main content

Module conversation

Module conversation 

Source
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::Conversation op.

Enums§

Role
A conversation turn’s role. Serializes snake_case (user/assistant/ tool); Turn::to_message maps Tool onto the Message::ToolResult role.

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 Message sequence.