Skip to main content

Crate car_sync

Crate car_sync 

Source
Expand description

Multi-device sync core for CAR — the oplog + deterministic fold (slice B1 of docs/proposals/multi-device-sync.md).

The proposal’s frame: sync events, not files. Every state-changing operation is appended to a content-addressed, append-only, replica-tagged oplog::OpRecord log; sync is “send me the ops I don’t have”; and each device fold::folds the full op-set into materialized state deterministically — commutative, associative, and idempotent over the op-set (CRDT properties), so two laptops writing simultaneously converge the moment they exchange ops.

What this slice ships (pure, library-only — no network, no daemon wiring):

  • oplogoplog::OpRecord exactly as the proposal specs it (op_id content-derived, hlc {wall_ms, counter, device_id}, scope: Personal | Shared{org}, the eight-variant surface enum, surface-specific payload), plus the per-device seq/prev hash-chain linkage that makes a device’s log order-verifiable (oplog::verify_log) and the oplog::DeviceLog writer that stamps oplog::Hlc values from the hybrid clock (see the B3 note below).
  • [fold] — fold::fold(ops) -> fold::SyncState under the proposal’s per-surface fold rules: grow-only union by stable ID for the log tier, LWW-register per record ordered by HLC for the registry tier, and ordered-observation fold::SyncState::replay for the path-dependent routing tier (“sync the observations, not the result” — the EMA apply is injected, execution stays out of the crate). fold::state_hash is the divergence-detection invariant (“same frontier ⇒ same snapshot hash”), and fold::registry_as_lww projects a folded registry onto car_state::crdt::LwwMap so the fold provably agrees with the shipped crdt_merge primitives where the domains overlap.
  • journal — durable JSONL persistence for the log in the car-eventlog journal idiom: append-only, torn-line tolerant on load, plus B4’s journal::OplogJournal::truncate_to (atomic temp+rename rewrite under the existing advisory lock, stamping a journal::TruncationMarker that fences the naive load+resume path into a runtime error — a truncated tail resumes only through checkpoint::resume_anchored).
  • checkpoint (B4) — checkpoint::Checkpoint: a serialized fold at a frontier — per-device {seq, hlc, head} frontier entries, covered scopes, the fold::SyncState snapshot, fold::state_hash as the divergence invariant, and a whole-record checkpoint_hash (frontier + scopes + state) as the content address / file name — so “same file ⇒ same checkpoint” holds even when two frontiers fold to one deduped state, and a tampered frontier is rejected on load. checkpoint::verify_anchored proves a truncated tail continues the checkpoint’s recorded chain heads (the checkpoint IS the anchored head); checkpoint::resume_anchored resumes a device chain past a truncation without forking. fold::fold_onto is the consumption primitive: fold_onto(checkpoint.state, tail) == fold(full log).
  • compact (B4) — per-surface retention (compact::RetentionPolicy::proposal_default: conversations last-N, runs 50/agent + 30 days, trajectories last-D, knowledge/skills/routing keep-all — every dropped id-bearing entry leaves a minimal tombstone stub so supersedes references resolve even when they arrive after compaction, and event-stream trims are rejected), the monotone-only compact::AckTable fold-frontier bookkeeping (an ack asserts durably-folded state — MUST, binding on B3), and compact::compact_and_truncate enforcing the crash-ordering invariant checkpoint durable FIRST, then truncate — acknowledged data is never lost, and compaction refuses to drop anything above ANY device’s acked frontier.

Determinism discipline (the proposal’s “free property” depends on it): all folded state lives in BTreeMaps — no HashMap iteration order, no wall-clock reads anywhere in the fold/retention path (the age rules’ reference instant defaults to compact::as_of_from_ops, pure over the below-frontier ops).

B3 adds the missing middle — how ops actually travel:

  • oplog::HlcClock — the real hybrid logical clock: {wall_ms, counter} state with the standard send/receive rules (max of local wall and everything witnessed; counter ticks on ties), monotone under clock skew, regression, and same-millisecond bursts. It replaces B1’s pure-Lamport stamp source behind the SAME wire shape, exactly as promised — DeviceLog::new still defaults to the degenerate logical (always-0 wall) mode, and wall readings are injectable (oplog::WallClock; oplog::system_clock is the one opt-in place system time exists in this crate).
  • relay — the relay::Relay trait (push / pull(since seq frontier) → {ops, latest_checkpoint_ptr} / ack / checkpoint_put/get / roster) with two reference implementations: relay::InMemoryRelay and the shared-directory relay::FsRelay loopback (the single-user two-Mac case). The relay admits only ops that continue a device’s relay-held chain (fork = runtime error), computes the stable frontier = min(acked) over non-evicted roster devices, marks a device silent past the horizon H relay::DeviceStatus::Evicted (its ack no longer pins GC; reinstated on a caught-up ack), and GC-drops an op only when it is both at/below the stable frontier AND covered by a stored checkpoint — checkpoints dedup on checkpoint_hash, the whole-record content address, never state_hash (the B4 contract).
  • sessionsession::SyncSession, the device-side pump holding the B1/B4 contracts by construction: append journals (flushed) before an op is pushable (journal-durable before transmit); pulls are verified before folding; folds are journaled before the ack, whose value is derived from journal-held ops only (acking merely-received state is impossible). Retry-safe at every crash point (op_id dedup both ways). Cold bootstrap / straggler re-entry is session::SyncSession::bootstrap/session::SyncSession::rebase: checkpoint_get + pull(since = checkpoint frontier) + checkpoint::resume_anchored — never DeviceLog::resume — with locally-held uncovered ops (a returning straggler’s unpushed writes) carried across the rebase and pushed after.

B5 adds execution lease + fencing — single-leader execution layered on top of the leaderless replication above:

  • lease — the lease::LeaseCoordinator trait: a linearizable compare-and-swap register per agent (exactly one holder at a time; a new acquire after TTL-expiry or release bumps the monotone epoch = the fencing token). It is deliberately separate from relay::Relay — an eventually-consistent relay structurally cannot host a lease (no consensus). lease::InMemoryLeaseCoordinator is the honest in-process reference (Arc<Mutex> CAS is genuinely linearizable in one process); a distributed backend is B6. The lease register holds only non-sensitive metadata, so it never breaches the E2E guarantee on the actual agent data (the proposal’s data/control-plane split).
  • Fencing as a fold property, over two views — the leased oplog::Surface::Intent surface (fold::FoldTier::Leased) carries the epoch, and the fold yields (a) fold::SyncState::committed_run, the fence-independent, keep-all idempotency ORACLE (survives epoch bumps AND compaction — the correct “did this run already execute?” lookup), and (b) fold::SyncState::intent, the “who holds now” view where pending intents are per-agent fenced (a stale zombie’s pending loses deterministically, order-independently, without a wall-clock race — fencing beats HLC) while committed/failed are terminal-immune. Idempotency keys on the B7 car_proto::deterministic_run_id. This converges the ledger and provides the durable oracle; it is NOT exactly-once execution — that is B6’s dispatch fence (a linearizable “still epoch N?” plus the oracle read, before the external effect). See lease and session::SyncSession::record_intent (terminal-guarded).

B2 adds transcript resume — the conversation surface as an ordered, role-threaded projection of the oplog:

  • conversationfold::SyncState::transcript folds the oplog::Surface::Conversation entries for one conversation_id into a causally-ordered Vec<conversation::Turn> (the crate’s canonical (hlc, op_id) order — two devices talking to the same agent concurrently interleave deterministically), and fold::SyncState::resume_messages returns the repaired, provider-valid car_inference_types::Message sequence car-inference’s multi-turn path replays to continue the conversation — the verbatim conversation-resume API docs/solutions/conversation-persistence-removed-in-0.25.md says does not exist today. A conversation turn is an event stream keyed by op_id (op identity IS turn identity — the kernel-review correction: content keying silently dropped two genuine same-timestamp turns), so a resent op dedups but two distinct authorings never collapse; it differs from routing only in being an independent multiset (no path-dependent replay), so it tolerates LastN retention. Because HLC order is deterministic but says nothing about concurrent turns, resume_messages runs a repair (coalesce adjacent same-role turns, drop orphan/dangling tool exchanges) so the Message sequence is never provider-invalid — the “runtime validates” thesis applied to the projection. The 0.25 compaction-vs-store incoherence cannot recur: the oplog is the one source of truth and the transcript is a projection of the same folded state B4’s checkpoint serializes. Built on the shared car-inference-types crate, so a Message shape change is a compile error here, not a runtime break in B6.

Later slices: rerouting today’s file write paths through the oplog and the daemon/memgine adoption of transcript resume (B6), the sync.* WS/FFI surface + E2E encryption + checkpoint/op signing + per-scope streams + the distributed lease coordinator (B6 — the network backend speaks the relay::Relay and lease::LeaseCoordinator contracts).

Re-exports§

pub use checkpoint::resume_anchored;
pub use checkpoint::verify_anchored;
pub use checkpoint::AnchorError;
pub use checkpoint::Checkpoint;
pub use checkpoint::CheckpointError;
pub use checkpoint::FrontierEntry;
pub use compact::apply_retention;
pub use compact::as_of_from_ops;
pub use compact::compact_and_truncate;
pub use compact::is_tombstone;
pub use compact::plan_compaction;
pub use compact::AckTable;
pub use compact::CompactError;
pub use compact::CompactionOutcome;
pub use compact::CompactionPlan;
pub use compact::RetentionPolicy;
pub use compact::RetentionReport;
pub use compact::RetentionRule;
pub use compact::RUNS_MAX_AGE_MS;
pub use compact::RUNS_MAX_PER_AGENT;
pub use conversation::Role;
pub use conversation::Turn;
pub use conversation::DEFAULT_CONVERSATION;
pub use crypto::derive_key;
pub use crypto::encryption_audience;
pub use crypto::CryptoError;
pub use crypto::DerivedKeyProvider;
pub use crypto::Envelope;
pub use crypto::LocalKeyCipher;
pub use crypto::PayloadCipher;
pub use crypto::SyncKeyProvider;
pub use crypto::ALG_CHACHA20POLY1305;
pub use fence::check_dispatch;
pub use fence::FenceDecision;
pub use fold::fold;
pub use fold::fold_onto;
pub use fold::hlc_version;
pub use fold::registry_as_lww;
pub use fold::state_hash;
pub use fold::FoldTier;
pub use fold::FoldedRecord;
pub use fold::IntentAgent;
pub use fold::SyncState;
pub use journal::OplogJournal;
pub use journal::TruncationMarker;
pub use lease::InMemoryLeaseCoordinator;
pub use lease::Intent;
pub use lease::IntentStatus;
pub use lease::Lease;
pub use lease::LeaseCoordinator;
pub use lease::LeaseError;
pub use net_relay::LeaseWire;
pub use net_relay::LoopbackTransport;
pub use net_relay::NetworkLeaseCoordinator;
pub use net_relay::NetworkRelay;
pub use net_relay::SyncTransport;
pub use net_relay::TransportError;
pub use oplog::canonical_json;
pub use oplog::logical_clock;
pub use oplog::system_clock;
pub use oplog::verify_log;
pub use oplog::ChainError;
pub use oplog::DeviceLog;
pub use oplog::Hlc;
pub use oplog::HlcClock;
pub use oplog::OpRecord;
pub use oplog::Scope;
pub use oplog::Surface;
pub use oplog::WallClock;
pub use partition::is_portable;
pub use partition::policy_for;
pub use partition::portable_domains;
pub use partition::SurfacePolicy;
pub use partition::SyncClass;
pub use partition::SURFACE_POLICIES;
pub use relay::checkpoint_frontier;
pub use relay::frontier_of;
pub use relay::AckOutcome;
pub use relay::DeviceStatus;
pub use relay::Frontier;
pub use relay::FsRelay;
pub use relay::GcReport;
pub use relay::InMemoryRelay;
pub use relay::PullResult;
pub use relay::PushOutcome;
pub use relay::Relay;
pub use relay::RelayConfig;
pub use relay::RelayError;
pub use relay::RosterEntry;
pub use session::PumpReport;
pub use session::SessionError;
pub use session::SyncSession;

Modules§

checkpoint
Checkpoints — a serialized fold at a frontier (slice B4 of docs/proposals/multi-device-sync.md, §“Snapshots: bounding the oplog”).
compact
Compaction + oplog GC (slice B4 of docs/proposals/multi-device-sync.md, §“Deep dive: the checkpoint / compaction / GC protocol”).
conversation
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.
crypto
End-to-end payload encryption boundary (slice B6 of docs/proposals/multi-device-sync.md, §“Transport: Parslee-hosted relay, E2E for personal scope”).
fence
The executor dispatch fence (slice B6 of docs/proposals/multi-device-sync.md, §“Layer 2 (safety): the idempotency / fencing gradient” + §“Deep dive: the execution-lease / fencing protocol”).
fold
The deterministic fold: fold(ops) → materialized state.
journal
Durable JSONL persistence for the oplog — the car-eventlog journal idiom: append-only, one record per line, torn-line tolerant on load.
lease
Execution lease + fencing (slice B5 of docs/proposals/multi-device-sync.md, §“Deep dive: the execution-lease / fencing protocol”).
net_relay
Network sync backend — CAR’s client for a remote relay + lease register (the Parslee sync service), behind the same Relay / LeaseCoordinator traits the local [FsRelay] / InMemoryLeaseCoordinator already satisfy.
oplog
The append-only, replica-tagged operation log.
partition
What syncs across a user’s devices, what stays device-local, and how a synced policy is reconciled when the devices run different operating systems.
relay
Relay transport — the account-scoped op-stream devices push/pull against (slice B3 of docs/proposals/multi-device-sync.md, §“Transport” + §“Stragglers and the single coherence knob” + §“Sync protocol surface”).
session
The device-side sync session — the pump that drives a DeviceLog + OplogJournal pair against a Relay (slice B3 of docs/proposals/multi-device-sync.md).