car_sync/fold.rs
1//! The deterministic fold: `fold(ops) → materialized state`.
2//!
3//! The proposal's convergence contract, verbatim: "each daemon **folds** the
4//! full op-set into local state deterministically. Because the fold is
5//! commutative, associative, and idempotent over the op-set (CRDT
6//! properties), two laptops writing simultaneously converge the moment they
7//! exchange ops."
8//!
9//! Fold rules per surface tier (the proposal's table):
10//! - **Grow-only** (conversations, knowledge, skills, trajectories, runs,
11//! routing observations): union by [`crate::oplog::OpRecord::fold_key`] —
12//! the stable entity key for logical-entity surfaces, the `op_id` for
13//! event-stream surfaces (routing), which fold as a MULTISET: the proposal
14//! replays "the merged **multiset** of observations", so two
15//! byte-identical observations are two events and both survive.
16//! Entities are immutable in this tier (a change is a new op — e.g. a
17//! `Supersedes` fact), so on a key collision with *different* content the
18//! earliest `(hlc, op_id)` writer wins, deterministically.
19//! - **Registry** (declagents, the file registries): LWW-register per
20//! record keyed by id, ordered by HLC — *not per file*. Latest
21//! `(hlc, op_id)` wins; concurrent edits to different records both
22//! survive.
23//! - **Routing**: the fold materializes the hlc-ordered observation stream;
24//! the EMA replay is the caller-injected [`SyncState::replay`] ("sync the
25//! observations, not the result" — same observations + same canonical
26//! order ⇒ bit-identical result on every device).
27//! - **Leased** (`Intent`, B5): LWW-per-run_id (monotone status) with
28//! **per-agent epoch fencing** — a stale-epoch intent from a failed-over
29//! lease holder loses at the fold, order-independently. See [`crate::lease`]
30//! and the [`fold_onto`] `FoldTier::Leased` arm.
31//!
32//! Determinism discipline: all state is `BTreeMap`-backed and nothing here
33//! reads a clock — the proposal calls out "a non-determinism leak
34//! (wall-clock or HashMap iteration order sneaking into a fold)" as the bug
35//! class [`state_hash`] exists to catch.
36
37pub use crate::oplog::FoldTier;
38use crate::oplog::{canonical_json, Hlc, OpRecord};
39use car_state::crdt::{LwwMap, LwwRegister};
40use serde::{Deserialize, Serialize};
41use serde_json::Value;
42use sha2::{Digest, Sha256};
43use std::collections::BTreeMap;
44
45/// One folded entity: the winning op's payload plus the stamp/id it won with.
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47pub struct FoldedRecord {
48 pub op_id: String,
49 pub hlc: Hlc,
50 pub payload: Value,
51}
52
53/// One agent's leased execution-intent ledger — the [`FoldTier::Leased`]
54/// tier's per-agent folded state (B5).
55///
56/// This slice delivers **deterministic ledger convergence plus a durable
57/// idempotency oracle** — it does NOT by itself make execution exactly-once.
58/// The exactly-once *execution* gate is B6's dispatch fence (a linearizable
59/// "am I still epoch N?" check plus the durable non-fenced idempotency read
60/// **before** the external side effect); the fold decides who wins the
61/// *ledger*, not whether the effect happens.
62///
63/// Two distinct views live here, and confusing them causes double-execution:
64///
65/// - **`committed_runs` is the idempotency oracle** — a **fence-INDEPENDENT,
66/// keep-all** map `run_id → committed record`. Once a run commits, it stays
67/// here forever (a commit is a fact; no epoch bump erases it), so
68/// [`SyncState::committed_run`] is the correct "did this run already
69/// execute?" lookup. It is carried in the checkpoint and never trimmed by
70/// retention/compaction (see [`crate::compact`]).
71/// - **`runs` is the "who holds now" view** — per-agent epoch **fencing**
72/// applies to *pending* intents (a stale zombie holder's pending is fenced),
73/// while committed/failed records are **terminal-immune** (never reverted to
74/// pending, never cleared by a fence raise). Read via [`SyncState::intent`].
75/// Do **NOT** use `runs`/`intent()` as the idempotency oracle — a pending
76/// fenced by a later epoch is absent here yet the run may have committed; ask
77/// `committed_runs` / [`SyncState::committed_run`].
78///
79/// **Fencing is per AGENT, not per run** (the proposal's spec, deliberately):
80/// a zombie's *unique* post-failover **pending** — one the new holder never
81/// re-ran — is fenced too (per-run fencing would let it through). `fencing_epoch`
82/// is the agent's max-seen lease epoch. `committed_runs` is what makes that
83/// safe for idempotency: even after prior-epoch pendings drop from `runs`, the
84/// committed fact survives keep-all.
85#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
86pub struct IntentAgent {
87 /// The agent's fencing epoch — the max lease epoch any of its intents
88 /// carried. **Pending** intents below it are fenced (terminals are immune).
89 pub fencing_epoch: u64,
90 /// run_id key (`id:<run_id>`) → the "who holds now" winner (terminal-immune;
91 /// pendings fenced to `fencing_epoch`). NOT the idempotency oracle.
92 pub runs: BTreeMap<String, FoldedRecord>,
93 /// run_id key → the committed record. **Fence-independent, keep-all** — the
94 /// durable idempotency oracle that survives epoch bumps AND compaction.
95 /// Grow-only (highest `(epoch, hlc, op_id)` committed record wins on a
96 /// collision); never cleared by fencing. `#[serde(default)]` so a state
97 /// serialized before this field parses.
98 #[serde(default)]
99 pub committed_runs: BTreeMap<String, FoldedRecord>,
100}
101
102/// The materialized read model a full op-set folds to. On-disk files
103/// (`conversations/*.jsonl`, `declagents.json`, …) are projections of this
104/// (the proposal's "files are projections" reframe); B4's checkpoint is a
105/// serialized `SyncState` at a frontier.
106#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
107pub struct SyncState {
108 /// Grow-only tier: surface tag → stable key → record (union;
109 /// first-writer-wins on a key collision).
110 pub logs: BTreeMap<String, BTreeMap<String, FoldedRecord>>,
111 /// Registry tier: surface tag → record id → LWW winner.
112 pub registries: BTreeMap<String, BTreeMap<String, FoldedRecord>>,
113 /// Leased execution-intent tier (B5): agent_id → its fenced intent
114 /// ledger. Folded from [`crate::oplog::Surface::Intent`] ops with **epoch
115 /// fencing** applied deterministically. `#[serde(default)]` so a pre-B5
116 /// serialized state still parses.
117 #[serde(default)]
118 pub intents: BTreeMap<String, IntentAgent>,
119}
120
121impl SyncState {
122 /// A grow-only surface's entries in canonical `(hlc, op_id)` order — the
123 /// deterministic total order every device agrees on (used by the routing
124 /// replay, and the order B2's transcript materialization will consume).
125 pub fn log_entries(&self, surface_tag: &str) -> Vec<&FoldedRecord> {
126 let mut entries: Vec<&FoldedRecord> = self
127 .logs
128 .get(surface_tag)
129 .map(|m| m.values().collect())
130 .unwrap_or_default();
131 entries.sort_by(|a, b| (&a.hlc, &a.op_id).cmp(&(&b.hlc, &b.op_id)));
132 entries
133 }
134
135 /// Replay an order-sensitive fold (e.g. the routing EMA) over a surface's
136 /// canonically-ordered entries: `fold(routing) =
137 /// observations.sorted_by(hlc).fold(empty_store, apply_ema)`. The apply
138 /// function is injected — execution (and the EMA itself) stays out of
139 /// this crate, like the other pure cores.
140 pub fn replay<T, F>(&self, surface_tag: &str, init: T, apply: F) -> T
141 where
142 F: FnMut(T, &FoldedRecord) -> T,
143 {
144 self.log_entries(surface_tag).into_iter().fold(init, apply)
145 }
146
147 /// The **"who holds now"** leased intent for a run (terminal-immune,
148 /// pending-fenced) — NOT the idempotency oracle. `None` when the agent has
149 /// no such run, or the run is a *pending* fenced by a later, higher-epoch
150 /// holder. A committed run is terminal-immune and stays visible here.
151 ///
152 /// **For "did this run already execute?" use [`SyncState::committed_run`]**
153 /// — `intent()` can return `None`/pending for a run that actually committed
154 /// under a prior epoch, which would cause a double-execution if trusted as
155 /// the idempotency check.
156 pub fn intent(&self, agent_id: &str, run_id: &str) -> Option<&FoldedRecord> {
157 self.intents
158 .get(agent_id)?
159 .runs
160 .get(&format!("id:{run_id}"))
161 }
162
163 /// The idempotency oracle (B5): the committed record for a run, if it has
164 /// **ever** committed for this agent. **Fence-independent and keep-all** —
165 /// unaffected by epoch bumps and by compaction — so this is the correct
166 /// "did `run_id` already run?" lookup before dispatching a side effect.
167 /// `None` iff no committed intent for `(agent_id, run_id)` exists.
168 pub fn committed_run(&self, agent_id: &str, run_id: &str) -> Option<&FoldedRecord> {
169 self.intents
170 .get(agent_id)?
171 .committed_runs
172 .get(&format!("id:{run_id}"))
173 }
174
175 /// Every run_id this agent has committed (the keep-all oracle's keys, with
176 /// the `id:` prefix stripped) — for a failover executor scanning "what has
177 /// already run".
178 pub fn committed_run_ids(&self, agent_id: &str) -> Vec<&str> {
179 self.intents
180 .get(agent_id)
181 .map(|a| {
182 a.committed_runs
183 .keys()
184 .filter_map(|k| k.strip_prefix("id:"))
185 .collect()
186 })
187 .unwrap_or_default()
188 }
189
190 /// The agent's current fencing epoch — the max lease epoch its intents
191 /// carry — or `None` if it has none. Pending intents below this are fenced.
192 pub fn fencing_epoch(&self, agent_id: &str) -> Option<u64> {
193 self.intents.get(agent_id).map(|a| a.fencing_epoch)
194 }
195}
196
197/// Is this folded intent record terminal (committed/failed)? Terminals are
198/// immune to fencing; only pending intents are fenced.
199fn intent_is_terminal(rec: &FoldedRecord) -> bool {
200 crate::lease::intent_status_rank(&rec.payload) == 1
201}
202
203/// Total priority for the leased tier's winner selection: terminal-flag (a
204/// terminal always outranks a pending — terminal-immunity), then `epoch`
205/// (fencing / latest-holder), then `(hlc, op_id)`. `max` under this key is
206/// order-independent.
207fn intent_priority(rec: &FoldedRecord) -> (u8, u64, &Hlc, &str) {
208 (
209 crate::lease::intent_status_rank(&rec.payload),
210 crate::lease::intent_epoch(&rec.payload),
211 &rec.hlc,
212 rec.op_id.as_str(),
213 )
214}
215
216/// Fold an op-set into its materialized state. Order-independent (per-key
217/// winner selection under a total order), idempotent (ops dedup on `op_id`
218/// first), and pure.
219///
220/// # Input contract: verify before folding untrusted input
221///
222/// `fold` does NOT verify ids or chains — that is the explicit, separate
223/// [`crate::oplog::verify_log`] pass, and any caller feeding ops from a
224/// remote/untrusted source (the B3 relay pull path) MUST run it first.
225/// `fold` stays order-independent even on invalid input (two records forging
226/// the *same* claimed `op_id` with *different* content dedup by a
227/// content-deterministic tiebreak, not arrival order), but which forged
228/// record wins is meaningless — verification is what makes the answer mean
229/// something.
230pub fn fold(ops: &[OpRecord]) -> SyncState {
231 fold_onto(&SyncState::default(), ops)
232}
233
234/// Fold additional ops **onto an already-folded base state** — the B4
235/// checkpoint-consumption primitive: a truncated device reconstructs
236/// `fold(full log)` as `fold_onto(checkpoint.state, retained tail)`.
237///
238/// Uses the same per-key winner selection as [`fold`] (grow-only earliest
239/// `(hlc, op_id)` wins; registry latest wins; event streams keyed by
240/// `op_id`), so `fold_onto(fold(prefix), suffix) == fold(prefix ∪ suffix)`
241/// exactly — the equivalence that makes compaction safe, and the invariant
242/// the lib-level B4 tests pin per surface. Re-delivering an op already in
243/// the base is idempotent (equal `(hlc, op_id)` never displaces the slot).
244///
245/// Same input contract as [`fold`]: verify (via
246/// [`crate::oplog::verify_log`] / [`crate::checkpoint::verify_anchored`])
247/// before folding untrusted input. One caveat unique to invalid input: the
248/// base keeps only `FoldedRecord`s, so a forged op colliding with a
249/// *base* record's `op_id` cannot use the full-record content tiebreak
250/// [`fold`] applies within one op-set — verification is what makes the
251/// answer mean something.
252pub fn fold_onto(base: &SyncState, ops: &[OpRecord]) -> SyncState {
253 // Idempotence: a retransmitted op (same op_id) folds once. On an id
254 // collision with DIFFERENT content (invalid input — verify_log rejects
255 // it) the tiebreak must not depend on arrival order, so the
256 // lexicographically-smaller canonical serialization wins.
257 let canonical_record = |op: &OpRecord| -> String {
258 canonical_json(&serde_json::to_value(op).expect("OpRecord serializes"))
259 };
260 let mut unique: BTreeMap<&str, &OpRecord> = BTreeMap::new();
261 for op in ops {
262 unique
263 .entry(&op.op_id)
264 .and_modify(|existing| {
265 if *existing != op && canonical_record(op) < canonical_record(existing) {
266 *existing = op;
267 }
268 })
269 .or_insert(op);
270 }
271
272 let mut state = base.clone();
273
274 // Leased-tier pre-pass (B5): establish the FINAL per-agent `fencing_epoch`
275 // (max over the base and every new intent op) BEFORE the main loop, and
276 // evict base *pending* records that the raised fence makes stale — terminals
277 // (committed/failed) are immune and kept. Deciding pending-fencing against
278 // the final fence (not an intermediate one built up mid-loop) is what keeps
279 // the leased fold order-independent and base-composable
280 // (`fold_onto(checkpoint, tail) == fold(full)`).
281 {
282 let mut agent_max: BTreeMap<String, u64> = BTreeMap::new();
283 for op in unique.values() {
284 if op.surface.fold_tier() == FoldTier::Leased {
285 let slot = agent_max
286 .entry(crate::lease::intent_agent(&op.payload).to_string())
287 .or_insert(0);
288 *slot = (*slot).max(crate::lease::intent_epoch(&op.payload));
289 }
290 }
291 for (agent, max_epoch) in agent_max {
292 let entry = state.intents.entry(agent).or_default();
293 if max_epoch > entry.fencing_epoch {
294 entry.fencing_epoch = max_epoch;
295 entry.runs.retain(|_, r| intent_is_terminal(r)); // keep terminals, fence pendings
296 }
297 }
298 }
299
300 for op in unique.values() {
301 let record = FoldedRecord {
302 op_id: op.op_id.clone(),
303 hlc: op.hlc.clone(),
304 payload: op.payload.clone(),
305 };
306 match op.surface.fold_tier() {
307 FoldTier::GrowOnly => {
308 let slot = state
309 .logs
310 .entry(op.surface.tag())
311 .or_default()
312 .entry(op.fold_key());
313 slot.and_modify(|existing| {
314 // Immutable-entity union: earliest (hlc, op_id) wins.
315 // (Unreachable for event-stream surfaces — their fold_key
316 // IS the op_id, so a collision is the same op.)
317 if (&record.hlc, &record.op_id) < (&existing.hlc, &existing.op_id) {
318 *existing = record.clone();
319 }
320 })
321 .or_insert(record);
322 }
323 FoldTier::Registry => {
324 let slot = state
325 .registries
326 .entry(op.surface.tag())
327 .or_default()
328 .entry(op.fold_key());
329 slot.and_modify(|existing| {
330 // LWW: latest (hlc, op_id) wins.
331 if (&record.hlc, &record.op_id) > (&existing.hlc, &existing.op_id) {
332 *existing = record.clone();
333 }
334 })
335 .or_insert(record);
336 }
337 FoldTier::Leased => {
338 // `fencing_epoch` is already final for this agent (pre-pass).
339 let agent = crate::lease::intent_agent(&op.payload).to_string();
340 let epoch = crate::lease::intent_epoch(&op.payload);
341 let key = op.fold_key();
342 let is_terminal = crate::lease::intent_status_rank(&op.payload) == 1;
343 let is_committed = crate::lease::intent_is_committed(&op.payload);
344 let entry = state.intents.entry(agent).or_default();
345
346 // (A) The idempotency ORACLE: grow-only, fence-INDEPENDENT,
347 // keep-all. A commit is a permanent fact — recorded whatever
348 // its epoch, never cleared by the fence. Highest
349 // `(epoch, hlc, op_id)` committed record wins a collision.
350 if is_committed {
351 let better = entry.committed_runs.get(&key).is_none_or(|existing| {
352 intent_priority(&record) > intent_priority(existing)
353 });
354 if better {
355 entry.committed_runs.insert(key.clone(), record.clone());
356 }
357 }
358
359 // (B) The "who holds now" view: terminals are immune, pendings
360 // are fenced to `fencing_epoch`. Eligible = terminal (always)
361 // OR a live pending at the fence; below-fence pendings drop.
362 // The winner is `max` under `intent_priority`, so a terminal
363 // never reverts to a pending (terminal-immunity) and a
364 // stale-epoch pending never wins.
365 let eligible = is_terminal || epoch == entry.fencing_epoch;
366 if eligible {
367 let better = entry.runs.get(&key).is_none_or(|existing| {
368 intent_priority(&record) > intent_priority(existing)
369 });
370 if better {
371 entry.runs.insert(key, record);
372 }
373 }
374 }
375 }
376 }
377 state
378}
379
380/// Deterministic content hash of a folded state — the proposal's built-in
381/// divergence invariant: "Same frontier ⇒ same snapshot hash,
382/// deterministically. A mismatch is a fold bug or a non-determinism leak."
383/// B4's checkpoint hash is this value at a frontier.
384pub fn state_hash(state: &SyncState) -> String {
385 let value = serde_json::to_value(state).expect("SyncState serializes");
386 let mut hasher = Sha256::new();
387 hasher.update(canonical_json(&value).as_bytes());
388 let digest = hasher.finalize();
389 let hex: String = digest.iter().take(16).map(|b| format!("{b:02x}")).collect();
390 format!("state-{hex}")
391}
392
393/// Encode an [`Hlc`] as a single `u64` version that preserves the
394/// `(wall_ms, counter)` order — the bridge onto `car_state::crdt`'s
395/// `(version, replica)` total order. 44 bits of wall-clock milliseconds
396/// (good past year 2500) and 20 bits of counter; a counter ≥ 2^20 within one
397/// millisecond is outside the HLC's operating range (B3's clock guarantees
398/// far less) and would break the order-preservation, so it is debug-asserted.
399pub fn hlc_version(hlc: &Hlc) -> u64 {
400 debug_assert!(
401 hlc.counter < (1 << 20),
402 "HLC counter exceeds encoding range"
403 );
404 debug_assert!(
405 hlc.wall_ms < (1 << 44),
406 "HLC wall_ms exceeds encoding range (the << 20 would drop high bits in release)"
407 );
408 (hlc.wall_ms << 20) | (u64::from(hlc.counter) & 0xF_FFFF)
409}
410
411/// Project a folded registry surface onto the shipped
412/// [`car_state::crdt::LwwMap`], so the oplog fold composes with (and is
413/// testably equivalent to) `crdt_merge`/`crdt_export` where the domains
414/// overlap: `fold(union of ops)` ≡ `merge_maps(per-device exports)`.
415pub fn registry_as_lww(state: &SyncState, surface_tag: &str) -> LwwMap {
416 state
417 .registries
418 .get(surface_tag)
419 .map(|records| {
420 records
421 .iter()
422 .map(|(key, rec)| {
423 (
424 key.clone(),
425 LwwRegister::new(
426 rec.payload.clone(),
427 hlc_version(&rec.hlc),
428 rec.hlc.device_id.clone(),
429 ),
430 )
431 })
432 .collect()
433 })
434 .unwrap_or_default()
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440 use crate::lease::{Intent, IntentStatus};
441 use crate::oplog::{DeviceLog, Scope, Surface};
442 use serde_json::json;
443
444 /// Append a leased execution-intent op through a device log.
445 fn intent_op(
446 dev: &mut DeviceLog,
447 agent: &str,
448 run: &str,
449 epoch: u64,
450 status: IntentStatus,
451 ) -> OpRecord {
452 dev.append(
453 Scope::Personal,
454 Surface::Intent,
455 Intent::new(agent, run, epoch, status).payload(),
456 )
457 }
458
459 #[test]
460 fn grow_only_unions_by_stable_key() {
461 let mut a = DeviceLog::new("a");
462 let mut b = DeviceLog::new("b");
463 let ops = vec![
464 a.append(
465 Scope::Personal,
466 Surface::Knowledge,
467 json!({"id": "f1", "v": 1}),
468 ),
469 b.append(
470 Scope::Personal,
471 Surface::Knowledge,
472 json!({"id": "f2", "v": 2}),
473 ),
474 ];
475 let state = fold(&ops);
476 let knowledge = &state.logs[&Surface::Knowledge.tag()];
477 assert_eq!(knowledge.len(), 2);
478 assert_eq!(knowledge["id:f1"].payload["v"], json!(1));
479 assert_eq!(knowledge["id:f2"].payload["v"], json!(2));
480 }
481
482 #[test]
483 fn grow_only_key_collision_resolves_to_earliest_deterministically() {
484 // Two devices emit different content under one stable id — an
485 // anomaly for the immutable tier, resolved first-writer-wins.
486 let mut a = DeviceLog::new("a");
487 let mut b = DeviceLog::new("b");
488 let oa = a.append(
489 Scope::Personal,
490 Surface::Knowledge,
491 json!({"id": "f", "v": "a"}),
492 );
493 b.observe(&oa.hlc); // b writes causally later
494 let ob = b.append(
495 Scope::Personal,
496 Surface::Knowledge,
497 json!({"id": "f", "v": "b"}),
498 );
499 let fwd = fold(&[oa.clone(), ob.clone()]);
500 let rev = fold(&[ob, oa]);
501 assert_eq!(fwd, rev);
502 assert_eq!(
503 fwd.logs[&Surface::Knowledge.tag()]["id:f"].payload["v"],
504 json!("a")
505 );
506 }
507
508 #[test]
509 fn registry_is_lww_per_record_not_per_file() {
510 let mut a = DeviceLog::new("a");
511 let mut b = DeviceLog::new("b");
512 // Concurrent edits to DIFFERENT agents both survive.
513 let oa = a.append(
514 Scope::Personal,
515 Surface::Declagent,
516 json!({"id": "x", "owner": "a"}),
517 );
518 let ob = b.append(
519 Scope::Personal,
520 Surface::Declagent,
521 json!({"id": "y", "owner": "b"}),
522 );
523 // Concurrent edits to the SAME agent resolve by HLC.
524 b.observe(&oa.hlc);
525 let ob2 = b.append(
526 Scope::Personal,
527 Surface::Declagent,
528 json!({"id": "x", "owner": "b"}),
529 );
530 let state = fold(&[oa, ob, ob2]);
531 let reg = &state.registries[&Surface::Declagent.tag()];
532 assert_eq!(reg.len(), 2, "both records survive");
533 assert_eq!(
534 reg["id:x"].payload["owner"],
535 json!("b"),
536 "later HLC wins the shared record"
537 );
538 assert_eq!(reg["id:y"].payload["owner"], json!("b"));
539 }
540
541 #[test]
542 fn registry_concurrent_tie_breaks_on_device_deterministically() {
543 // Same lamport stamp on two devices (true concurrency): the HLC's
544 // device_id component breaks the tie, both fold orders agree.
545 let mut a = DeviceLog::new("a");
546 let mut b = DeviceLog::new("b");
547 let oa = a.append(
548 Scope::Personal,
549 Surface::Declagent,
550 json!({"id": "x", "owner": "a"}),
551 );
552 let ob = b.append(
553 Scope::Personal,
554 Surface::Declagent,
555 json!({"id": "x", "owner": "b"}),
556 );
557 assert_eq!(oa.hlc.wall_ms, ob.hlc.wall_ms);
558 let fwd = fold(&[oa.clone(), ob.clone()]);
559 let rev = fold(&[ob, oa]);
560 assert_eq!(fwd, rev);
561 // "b" > "a" in the device tiebreak — matches crdt's replica tiebreak.
562 assert_eq!(
563 fwd.registries[&Surface::Declagent.tag()]["id:x"].payload["owner"],
564 json!("b")
565 );
566 }
567
568 #[test]
569 fn state_hash_detects_divergence_and_agrees_on_convergence() {
570 let mut a = DeviceLog::new("a");
571 let o1 = a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f1"}));
572 let o2 = a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f2"}));
573 let h_full = state_hash(&fold(&[o1.clone(), o2.clone()]));
574 let h_full_again = state_hash(&fold(&[o2.clone(), o1.clone()]));
575 assert_eq!(h_full, h_full_again, "same op-set → same hash");
576 let h_partial = state_hash(&fold(&[o1]));
577 assert_ne!(h_full, h_partial, "different op-set → different hash");
578 assert!(h_full.starts_with("state-"));
579 }
580
581 #[test]
582 fn hlc_version_preserves_order() {
583 let stamps = [
584 Hlc {
585 wall_ms: 1,
586 counter: 0,
587 device_id: "a".into(),
588 },
589 Hlc {
590 wall_ms: 1,
591 counter: 1,
592 device_id: "a".into(),
593 },
594 Hlc {
595 wall_ms: 2,
596 counter: 0,
597 device_id: "a".into(),
598 },
599 ];
600 for w in stamps.windows(2) {
601 assert!(hlc_version(&w[0]) < hlc_version(&w[1]));
602 }
603 }
604
605 #[test]
606 fn registry_as_lww_matches_crdt_merge_including_export_shape() {
607 // The equivalence the proposal leans on: per-device exports merged
608 // with the shipped crdt primitives == the fold of the op union.
609 let mut a = DeviceLog::new("dev-a");
610 let mut b = DeviceLog::new("dev-b");
611 let oa1 = a.append(
612 Scope::Personal,
613 Surface::Registry {
614 kind: "agents".into(),
615 },
616 json!({"id": "r1", "v": "a"}),
617 );
618 let oa2 = a.append(
619 Scope::Personal,
620 Surface::Registry {
621 kind: "agents".into(),
622 },
623 json!({"id": "r2", "v": "a"}),
624 );
625 b.observe(&oa1.hlc);
626 b.observe(&oa2.hlc);
627 let ob1 = b.append(
628 Scope::Personal,
629 Surface::Registry {
630 kind: "agents".into(),
631 },
632 json!({"id": "r1", "v": "b"}),
633 );
634
635 let tag = Surface::Registry {
636 kind: "agents".into(),
637 }
638 .tag();
639 let union = registry_as_lww(&fold(&[oa1.clone(), oa2.clone(), ob1.clone()]), &tag);
640 let export_a = registry_as_lww(&fold(&[oa1, oa2]), &tag);
641 let export_b = registry_as_lww(&fold(&[ob1]), &tag);
642
643 assert_eq!(car_state::crdt::merge_maps(&export_a, &export_b), union);
644 assert_eq!(car_state::crdt::merge_many(&[export_b, export_a]), union);
645 let plain = car_state::crdt::materialize(&union);
646 assert_eq!(plain["id:r1"]["v"], json!("b"));
647 assert_eq!(plain["id:r2"]["v"], json!("a"));
648 }
649
650 #[test]
651 fn log_entries_are_hlc_ordered() {
652 let mut a = DeviceLog::new("a");
653 let mut b = DeviceLog::new("b");
654 let o1 = a.append(
655 Scope::Personal,
656 Surface::Conversation,
657 json!({"t": "first"}),
658 );
659 b.observe(&o1.hlc);
660 let o2 = b.append(
661 Scope::Personal,
662 Surface::Conversation,
663 json!({"t": "second"}),
664 );
665 a.observe(&o2.hlc); // a's next write causally follows b's
666 let o3 = a.append(
667 Scope::Personal,
668 Surface::Conversation,
669 json!({"t": "third"}),
670 );
671 // Deliver out of order; the view is canonical.
672 let state = fold(&[o3, o1, o2]);
673 let texts: Vec<&Value> = state
674 .log_entries(&Surface::Conversation.tag())
675 .iter()
676 .map(|r| &r.payload["t"])
677 .collect();
678 assert_eq!(
679 texts,
680 vec![&json!("first"), &json!("second"), &json!("third")]
681 );
682 }
683
684 #[test]
685 fn routing_observations_fold_as_a_multiset() {
686 // The demonstrated kernel-review defect: "agent x succeeded" twice is
687 // TWO observations. Under content-keyed dedup the second collapsed
688 // into the first (1 entry, EMA 0.65); the proposal requires the
689 // merged MULTISET (2 entries, EMA 0.755).
690 let mut dev = DeviceLog::new("dev-a");
691 let ops = vec![
692 dev.append(Scope::Personal, Surface::Routing, json!({"sample": 1.0})),
693 dev.append(Scope::Personal, Surface::Routing, json!({"sample": 1.0})),
694 ];
695 let state = fold(&ops);
696 assert_eq!(
697 state.log_entries(&Surface::Routing.tag()).len(),
698 2,
699 "two byte-identical observations are two events"
700 );
701 let ema =
702 |s: f64, rec: &FoldedRecord| 0.7 * s + 0.3 * rec.payload["sample"].as_f64().unwrap();
703 let value = state.replay(&Surface::Routing.tag(), 0.5_f64, ema);
704 assert!(
705 (value - 0.755).abs() < 1e-12,
706 "EMA over both events: got {value}"
707 );
708 }
709
710 #[test]
711 fn logical_entity_surfaces_dedup_identical_content() {
712 // Content-keyed dedup is scoped to logical-ENTITY surfaces (knowledge,
713 // skills, …): the same fact emitted identically by two devices is ONE
714 // entity. (Conversation is NOT one of these — it's an event stream
715 // keyed by op_id — see the conversation module's CRIT-2 tests.)
716 let mut a = DeviceLog::new("a");
717 let mut b = DeviceLog::new("b");
718 let fact = json!({"kind": "note", "body": "the sky is blue"});
719 let oa = a.append(Scope::Personal, Surface::Knowledge, fact.clone());
720 let ob = b.append(Scope::Personal, Surface::Knowledge, fact);
721 let state = fold(&[oa, ob]);
722 assert_eq!(state.log_entries(&Surface::Knowledge.tag()).len(), 1);
723 }
724
725 #[test]
726 fn forged_colliding_op_id_dedups_order_independently() {
727 // Invalid input (verify_log rejects it), but the fold must stay
728 // order-independent: two records claiming the SAME op_id with
729 // DIFFERENT content tiebreak on content, not arrival order.
730 let mut dev = DeviceLog::new("d1");
731 let genuine = dev.append(
732 Scope::Personal,
733 Surface::Knowledge,
734 json!({"id": "f", "v": 1}),
735 );
736 let mut forged = genuine.clone();
737 forged.payload = json!({"id": "f", "v": 2}); // op_id NOT recomputed
738 assert!(crate::oplog::verify_log(&[forged.clone()]).is_err());
739
740 let ab = fold(&[genuine.clone(), forged.clone()]);
741 let ba = fold(&[forged, genuine]);
742 assert_eq!(
743 ab, ba,
744 "colliding-id dedup must not depend on arrival order"
745 );
746 assert_eq!(state_hash(&ab), state_hash(&ba));
747 }
748
749 #[test]
750 fn fold_onto_prefix_fold_equals_full_fold() {
751 // The B4 primitive: fold(prefix) then fold_onto(., suffix) must be
752 // byte-identical to fold(prefix ∪ suffix) — for every fold rule at
753 // once, including a grow-only collision and an LWW overwrite that
754 // CROSS the split point.
755 let mut a = DeviceLog::new("a");
756 let mut b = DeviceLog::new("b");
757 let prefix = vec![
758 a.append(
759 Scope::Personal,
760 Surface::Knowledge,
761 json!({"id": "f", "v": "old"}),
762 ),
763 a.append(
764 Scope::Personal,
765 Surface::Declagent,
766 json!({"id": "x", "owner": "a"}),
767 ),
768 a.append(Scope::Personal, Surface::Routing, json!({"sample": 1.0})),
769 ];
770 for op in &prefix {
771 b.observe(&op.hlc);
772 }
773 let suffix = vec![
774 // Grow-only collision across the split: earliest wins → "old".
775 b.append(
776 Scope::Personal,
777 Surface::Knowledge,
778 json!({"id": "f", "v": "new"}),
779 ),
780 // LWW across the split: latest wins → owner "b".
781 b.append(
782 Scope::Personal,
783 Surface::Declagent,
784 json!({"id": "x", "owner": "b"}),
785 ),
786 // Event stream across the split: both observations survive.
787 b.append(Scope::Personal, Surface::Routing, json!({"sample": 0.0})),
788 ];
789 let mut full = prefix.clone();
790 full.extend(suffix.iter().cloned());
791
792 let via_base = fold_onto(&fold(&prefix), &suffix);
793 assert_eq!(via_base, fold(&full));
794 assert_eq!(state_hash(&via_base), state_hash(&fold(&full)));
795 assert_eq!(
796 via_base.logs[&Surface::Knowledge.tag()]["id:f"].payload["v"],
797 json!("old")
798 );
799 assert_eq!(
800 via_base.registries[&Surface::Declagent.tag()]["id:x"].payload["owner"],
801 json!("b")
802 );
803 assert_eq!(via_base.log_entries(&Surface::Routing.tag()).len(), 2);
804
805 // Idempotent re-delivery: folding an op already in the base changes
806 // nothing.
807 assert_eq!(fold_onto(&via_base, &prefix), via_base);
808 }
809
810 #[test]
811 fn empty_fold_is_empty_and_stable() {
812 let state = fold(&[]);
813 assert_eq!(state, SyncState::default());
814 assert_eq!(state_hash(&state), state_hash(&fold(&[])));
815 assert!(state.log_entries("conversation").is_empty());
816 assert!(registry_as_lww(&state, "declagent").is_empty());
817 assert!(state.intent("milo", "R").is_none());
818 assert!(state.fencing_epoch("milo").is_none());
819 }
820
821 // ------------------------------------------------------------------
822 // B5: leased execution-intent fencing as a deterministic fold property.
823 // ------------------------------------------------------------------
824
825 #[test]
826 fn intent_fold_fences_stale_epoch_order_independently() {
827 // Failover: dev-a held epoch 1, dev-b stole epoch 2. Both fire the
828 // SAME run R — a=zombie, b=legit holder. The fold must pick epoch 2
829 // (b) in ANY delivery order and fence a's epoch-1 writes, leaving one
830 // ledger record — no double-commit.
831 let mut a = DeviceLog::new("dev-a");
832 let mut b = DeviceLog::new("dev-b");
833 let ops = vec![
834 intent_op(&mut a, "milo", "R", 1, IntentStatus::Pending),
835 intent_op(&mut a, "milo", "R", 1, IntentStatus::Committed),
836 intent_op(&mut b, "milo", "R", 2, IntentStatus::Pending),
837 intent_op(&mut b, "milo", "R", 2, IntentStatus::Committed),
838 ];
839 let b_commit = ops[3].clone();
840
841 let baseline = fold(&ops);
842 assert_eq!(baseline.fencing_epoch("milo"), Some(2));
843 assert_eq!(
844 baseline.intents["milo"].runs.len(),
845 1,
846 "single record — no double-commit"
847 );
848 let winner = baseline.intent("milo", "R").expect("R survives");
849 let decoded = Intent::from_payload(&winner.payload).unwrap();
850 assert_eq!(
851 (decoded.epoch, decoded.status),
852 (2, IntentStatus::Committed)
853 );
854 assert_eq!(
855 winner.op_id, b_commit.op_id,
856 "the current holder's commit wins"
857 );
858
859 // Order-independence: several explicit permutations agree exactly.
860 for order in [
861 vec![
862 ops[3].clone(),
863 ops[2].clone(),
864 ops[1].clone(),
865 ops[0].clone(),
866 ],
867 vec![
868 ops[2].clone(),
869 ops[0].clone(),
870 ops[3].clone(),
871 ops[1].clone(),
872 ],
873 vec![
874 ops[1].clone(),
875 ops[3].clone(),
876 ops[0].clone(),
877 ops[2].clone(),
878 ],
879 ] {
880 assert_eq!(fold(&order), baseline);
881 assert_eq!(state_hash(&fold(&order)), state_hash(&baseline));
882 }
883 }
884
885 #[test]
886 fn intent_per_agent_pending_fencing_with_committed_immunity() {
887 // Per-AGENT fencing applies to PENDINGS: dev-a (epoch 1) has an
888 // unshared PENDING run S that dev-b (epoch 2) never touched → S's
889 // pending is fenced (a stale holder's intent-to-do is silenced). But a
890 // COMMITTED run is terminal-immune — a commit is a fact, not fenced —
891 // so the zombie's unshared committed run K survives (the C1 fix: an
892 // unrelated higher-epoch run must not evict it).
893 let mut a = DeviceLog::new("dev-a");
894 let mut b = DeviceLog::new("dev-b");
895 let ops = vec![
896 intent_op(&mut a, "milo", "S", 1, IntentStatus::Pending), // unshared zombie pending
897 intent_op(&mut a, "milo", "K", 1, IntentStatus::Committed), // unshared zombie commit
898 intent_op(&mut b, "milo", "T", 2, IntentStatus::Committed), // new holder, unrelated run
899 ];
900 let state = fold(&ops);
901 assert_eq!(state.fencing_epoch("milo"), Some(2));
902 // The unshared PENDING is fenced; it never committed.
903 assert!(
904 state.intent("milo", "S").is_none(),
905 "unshared zombie pending is fenced"
906 );
907 assert!(
908 state.committed_run("milo", "S").is_none(),
909 "S never committed"
910 );
911 // The unshared COMMITTED run survives the unrelated epoch bump (C1).
912 assert!(
913 state.committed_run("milo", "K").is_some(),
914 "committed run survives an unrelated epoch bump (idempotency oracle)"
915 );
916 assert!(
917 state.intent("milo", "K").is_some(),
918 "committed is terminal-immune in runs too"
919 );
920 assert!(state.committed_run("milo", "T").is_some());
921 // A different agent is a different fencing group.
922 let mut c = DeviceLog::new("dev-c");
923 let mixed = {
924 let mut v = ops.clone();
925 v.push(intent_op(&mut c, "other", "U", 1, IntentStatus::Committed));
926 v
927 };
928 assert!(
929 fold(&mixed).committed_run("other", "U").is_some(),
930 "fencing does not cross agents"
931 );
932 }
933
934 #[test]
935 fn intent_fold_onto_equals_full_fold_across_an_epoch_bump() {
936 // The compaction-safety equivalence for the leased tier across an epoch
937 // bump: a checkpoint captured the agent at epoch 1 (COMMITTED run R). A
938 // later, higher-epoch tail op (run S @ 2) raises the fence — and R,
939 // being committed, is terminal-immune and SURVIVES (the C1/C3 fix; a
940 // commit is a durable fact, not evicted by an unrelated bump). The
941 // fold_onto == fold equivalence still holds exactly.
942 let mut a = DeviceLog::new("dev-a");
943 let mut b = DeviceLog::new("dev-b");
944 let prefix = vec![intent_op(&mut a, "milo", "R", 1, IntentStatus::Committed)];
945 for op in &prefix {
946 b.observe(&op.hlc);
947 }
948 let tail = vec![intent_op(&mut b, "milo", "S", 2, IntentStatus::Committed)];
949 let full = {
950 let mut v = prefix.clone();
951 v.extend(tail.iter().cloned());
952 v
953 };
954
955 let base = fold(&prefix); // the "checkpoint" state: epoch 1, R committed
956 assert_eq!(base.fencing_epoch("milo"), Some(1));
957 assert!(base.committed_run("milo", "R").is_some());
958
959 let via_base = fold_onto(&base, &tail);
960 assert_eq!(
961 via_base,
962 fold(&full),
963 "fold_onto == fold across the epoch bump"
964 );
965 assert_eq!(state_hash(&via_base), state_hash(&fold(&full)));
966 assert_eq!(via_base.fencing_epoch("milo"), Some(2));
967 // R committed@1 SURVIVES the bump in both views (terminal-immune / oracle).
968 assert!(
969 via_base.committed_run("milo", "R").is_some(),
970 "committed R survives the epoch bump in the idempotency oracle"
971 );
972 assert!(
973 via_base.intent("milo", "R").is_some(),
974 "committed R is terminal-immune in runs"
975 );
976 assert!(via_base.committed_run("milo", "S").is_some());
977 // Idempotent re-delivery of the tail changes nothing.
978 assert_eq!(fold_onto(&via_base, &tail), via_base);
979 }
980
981 #[test]
982 fn intent_fencing_beats_a_later_hlc() {
983 // Safety is by EPOCH, not wall clock: a zombie op with a LATER hlc but
984 // a LOWER epoch still loses to the higher-epoch op — no wall-clock race.
985 let mut cloud = DeviceLog::new("cloud");
986 let mut zombie = DeviceLog::new("laptop");
987 let c = intent_op(&mut cloud, "milo", "R", 2, IntentStatus::Committed);
988 zombie.observe(&c.hlc); // the zombie's later write stamps a HIGHER hlc
989 let z = intent_op(&mut zombie, "milo", "R", 1, IntentStatus::Committed);
990 assert!(z.hlc > c.hlc, "the zombie op is later in HLC");
991
992 let state = fold(&[c.clone(), z]);
993 let winner = state.intent("milo", "R").unwrap();
994 assert_eq!(winner.op_id, c.op_id, "higher epoch wins despite lower HLC");
995 assert_eq!(state.fencing_epoch("milo"), Some(2));
996 }
997
998 #[test]
999 fn idempotent_run_under_failover_uses_the_same_deterministic_run_id() {
1000 // B7 tie-in: two sites computing the same scheduled occurrence derive
1001 // the SAME run_id, so a failed-over holder and a zombie collapse to ONE
1002 // ledger record; epoch fencing then picks the legit (epoch-2) winner.
1003 let run_id = car_proto::deterministic_run_id("milo", "3am digest", "2026-07-02T03:00");
1004 assert_eq!(
1005 run_id,
1006 car_proto::deterministic_run_id("milo", "3am digest", "2026-07-02T03:00"),
1007 "same occurrence → same run_id"
1008 );
1009 let mut zombie = DeviceLog::new("laptop");
1010 let mut cloud = DeviceLog::new("cloud");
1011 let z = intent_op(&mut zombie, "milo", &run_id, 1, IntentStatus::Committed);
1012 let c = intent_op(&mut cloud, "milo", &run_id, 2, IntentStatus::Committed);
1013
1014 let state = fold(&[z, c.clone()]);
1015 assert_eq!(
1016 state.intents["milo"].runs.len(),
1017 1,
1018 "exactly one execution record"
1019 );
1020 assert_eq!(
1021 state.intent("milo", &run_id).unwrap().op_id,
1022 c.op_id,
1023 "the epoch-2 holder's run wins; the zombie is a no-op"
1024 );
1025 }
1026
1027 #[test]
1028 fn intent_fold_is_order_independent_over_every_permutation() {
1029 // Brute-force the redesigned leased fold (pre-pass + terminal-immunity
1030 // + committed oracle): a 5-op set mixing committed/pending across two
1031 // epochs and three runs must fold IDENTICALLY in all 120 orders.
1032 fn permutations<T: Clone>(items: &[T]) -> Vec<Vec<T>> {
1033 fn heap<T: Clone>(k: usize, arr: &mut Vec<T>, out: &mut Vec<Vec<T>>) {
1034 if k == 1 {
1035 out.push(arr.clone());
1036 return;
1037 }
1038 for i in 0..k {
1039 heap(k - 1, arr, out);
1040 if k.is_multiple_of(2) {
1041 arr.swap(i, k - 1);
1042 } else {
1043 arr.swap(0, k - 1);
1044 }
1045 }
1046 }
1047 let mut arr = items.to_vec();
1048 let mut out = Vec::new();
1049 heap(arr.len(), &mut arr, &mut out);
1050 out
1051 }
1052
1053 let mut a = DeviceLog::new("a");
1054 let mut b = DeviceLog::new("b");
1055 let ops = vec![
1056 intent_op(&mut a, "milo", "R", 1, IntentStatus::Committed), // terminal-immune across bump
1057 intent_op(&mut a, "milo", "S", 1, IntentStatus::Pending), // unshared pending → fenced
1058 intent_op(&mut a, "milo", "T", 1, IntentStatus::Committed), // unshared committed → survives
1059 intent_op(&mut b, "milo", "R", 2, IntentStatus::Pending), // C3: must not revert R
1060 intent_op(&mut b, "milo", "S", 2, IntentStatus::Committed), // S commits at the higher epoch
1061 ];
1062 let baseline = fold(&ops);
1063 // Expected steady state.
1064 assert_eq!(baseline.fencing_epoch("milo"), Some(2));
1065 let mut committed = baseline.committed_run_ids("milo");
1066 committed.sort();
1067 assert_eq!(
1068 committed,
1069 vec!["R", "S", "T"],
1070 "the oracle keeps every committed run"
1071 );
1072 assert!(baseline
1073 .intent("milo", "S")
1074 .map(|r| r.op_id.clone())
1075 .is_some_and(|_| {
1076 Intent::from_payload(&baseline.intent("milo", "S").unwrap().payload)
1077 .unwrap()
1078 .status
1079 == IntentStatus::Committed
1080 }));
1081 assert_eq!(
1082 Intent::from_payload(&baseline.intent("milo", "R").unwrap().payload)
1083 .unwrap()
1084 .status,
1085 IntentStatus::Committed,
1086 "R is not reverted to pending"
1087 );
1088
1089 for perm in permutations(&ops) {
1090 assert_eq!(
1091 fold(&perm),
1092 baseline,
1093 "leased fold must be order-independent"
1094 );
1095 assert_eq!(state_hash(&fold(&perm)), state_hash(&baseline));
1096 }
1097 }
1098
1099 #[test]
1100 fn c1_committed_run_survives_an_unrelated_higher_epoch_run() {
1101 // C1 REPRO: run R commits at epoch 1; later an UNRELATED run T lands at
1102 // epoch 2 for the same agent (no concurrency). The old fold cleared
1103 // runs on the bump, so intent(R) → None → the idempotency check said
1104 // "not run" → double-execution. FIX: the fence-INDEPENDENT
1105 // committed_run oracle answers correctly regardless of the bump.
1106 let mut a = DeviceLog::new("dev-a");
1107 let mut b = DeviceLog::new("dev-b");
1108 let r_commit = intent_op(&mut a, "milo", "R", 1, IntentStatus::Committed);
1109 b.observe(&r_commit.hlc);
1110 let t_pending = intent_op(&mut b, "milo", "T", 2, IntentStatus::Pending);
1111
1112 let state = fold(&[r_commit.clone(), t_pending]);
1113 assert_eq!(
1114 state.fencing_epoch("milo"),
1115 Some(2),
1116 "the unrelated run bumped the fence"
1117 );
1118 // The oracle still says R committed — the correct idempotency answer.
1119 assert_eq!(
1120 state.committed_run("milo", "R").unwrap().op_id,
1121 r_commit.op_id,
1122 "committed_run(R) survives the unrelated epoch bump (C1 fixed)"
1123 );
1124 assert_eq!(state.committed_run_ids("milo"), vec!["R"]);
1125 }
1126
1127 #[test]
1128 fn c3_committed_then_pending_across_a_bump_stays_committed() {
1129 // C3 REPRO: R commits at epoch 1; a failed-over holder writes R PENDING
1130 // at epoch 2 (before checking). The old Greater arm unconditionally
1131 // cleared, reverting R to pending → looked un-run → double-execute.
1132 // FIX: terminal-immunity — the fold keeps committed for R in BOTH views,
1133 // in any delivery order.
1134 let mut orig = DeviceLog::new("orig");
1135 let mut failover = DeviceLog::new("failover");
1136 let committed = intent_op(&mut orig, "milo", "R", 1, IntentStatus::Committed);
1137 failover.observe(&committed.hlc);
1138 let late_pending = intent_op(&mut failover, "milo", "R", 2, IntentStatus::Pending);
1139 assert!(
1140 late_pending.hlc > committed.hlc,
1141 "the pending is even later in HLC"
1142 );
1143
1144 for order in [
1145 vec![committed.clone(), late_pending.clone()],
1146 vec![late_pending.clone(), committed.clone()],
1147 ] {
1148 let state = fold(&order);
1149 // Oracle: committed, unconditionally.
1150 assert_eq!(
1151 state.committed_run("milo", "R").unwrap().op_id,
1152 committed.op_id,
1153 "committed stays committed across the bump (oracle)"
1154 );
1155 // who-holds view: terminal-immune, still committed (not reverted).
1156 let decoded =
1157 Intent::from_payload(&state.intent("milo", "R").unwrap().payload).unwrap();
1158 assert_eq!(
1159 decoded.status,
1160 IntentStatus::Committed,
1161 "runs view is not reverted to pending"
1162 );
1163 }
1164 }
1165}