v0.9.0-rc.28
peat-cli round-trip-edit + tombstone-authorship unblocker — Automerge delta API, node-local Lamport clock, cross-restart persistence, and sync-receive wire-up. Four landed PRs (peat-mesh#193, peat-mesh#194, peat-mesh#197, peat-mesh#198) close peat-mesh#187, peat-mesh#192, peat-mesh#195, and peat-mesh#196. Together they replace the peat-cli wall-clock Lamport proxy with a peat-mesh-managed clock, surface the Automerge delta primitive peat-cli needs for peat update --from <PATH>, persist the clock across restarts, and wire the receive-side rule so cross-node Lamport semantics hold without consumer-side threading.
Added — Automerge delta API (peat-mesh#193, closes peat-mesh#187)
AutomergeStore::diff(current, proposed) -> AutomergeDelta— associated function (no store state). WrapsAutomerge::get_changes_added; returns the changes inproposednot incurrentas an opaque delta.AutomergeStore::apply_delta(&self, key, &AutomergeDelta)+apply_delta_with_origin(&self, key, &AutomergeDelta, ChangeOrigin)— read-modify-write under the per-key striped lock, routes the resulting state throughput_innerso observers, the sync coordinator'schange_tx, and the origin-taggedgossip_txall fire identically to aput. TheRemote(peer)origin variant suppresseschange_txper the peat-mesh#115 ping-pong invariant.AutomergeDelta::to_bytes/from_bytes—u32-LE-prefixed framing per change, so the delta crosses a process boundary safely. Truncation surfaces as a named-diagnosticErr.- 8 behavioural pins in
tests/automerge_delta_api.rs: diff/apply roundtrip preserves operation history (ADR-021 invariant), empty-delta no-op, missing-key diagnostic, Local-origin observer fan-out, Remote-originchange_txsuppression, bytes round-trip, truncation rejection.
Unblocks peat-node ADR-001 Phase 4b — peat update --from <PATH> round-trip-edit flow.
Added — node-local Lamport clock (peat-mesh#194, closes peat-mesh#192)
AutomergeBackend::next_lamport() -> u64— read-and-advance the node-local Lamport clock. Strictly monotonic across concurrent callers (fetch_add(Relaxed).wrapping_add(1)).AutomergeBackend::current_lamport() -> u64— non-advancing read for diagnostics.AutomergeBackend::observe_lamport(observed: u64)— Lamport receive-side ruleL := max(L, observed) + 1, implemented via CAS retry loop. Capped atu64::MAX - 1so a hostile or buggy peer'sobserved = u64::MAXcannot saturate the atomic and wrapnext_lamportto0.- Seeded at construction from
SystemTime::now()nanos with a1u64floor. - 6 behavioural pins in
tests/lamport_clock_source.rs: monotonicity, non-advancing read, observe-jumps-clock, observe-never-regresses, uniqueness across 8 × 1024 concurrent calls, seed-above-zero.
Replaces the peat-cli delete wall-clock proxy at the right surface — strict node-local monotonicity under concurrent writes, regardless of NTP drift.
Added — Lamport persistence across restart (peat-mesh#197, closes peat-mesh#195)
AutomergeStore::read_lamport_highwater() -> Result<Option<u64>>+write_lamport_highwater(u64) -> Result<()>— newmetadataredb table; the write is monotonic in-transaction (reads existing, skips write whenexisting >= value) so the periodic flush task and an explicit flush cannot interleave to regress the on-disk high-water. Corrupt entries (wrong length) log a warning and treat asNone, allowing the next write to self-heal.AutomergeBackend::flush_lamport_highwater() -> Result<()>— synchronous explicit flush. Called fromDataSyncBackend::shutdownso the on-disk value reflects every issued value at clean exit (zero worst-case loss); also available for test rigs and explicit shutdown hooks.- Background flush task — 1-second cadence, skips when the atomic hasn't changed since the last flush.
last_flushedseeded frompersisted_highwaterat startup so an idle backend doesn't re-write the same value redundantly. HoldsWeak<AutomergeStore>across the sleep — task exits when the backend drops. - Seed formula —
max(persisted_highwater, SystemTime nanos, 1). Resists wall-clock regression on tactical hardware (battery-less RTC, NTP slew, manual time-set, boot-at-epoch) — the resumed clock stays above any value the prior run could have issued, even when the wall clock has gone backwards. AutomergeBackend::shutdown_and_release(&self) -> Result<()>— async teardown that awaits the iroh Router's shutdown future, so production callers (hot-restart, daemon-rotate, mobile pause/resume) and integration tests can deterministically drop the backend and immediately reopen the samedata_dirin the same process. Idempotent via anAtomicBoolCAS guard against signal-handling races. Bounded byNetworkedIrohBlobStore::shutdown_timeout(5s default).- 4 new pins in
tests/lamport_clock_source.rs: cross-restart persistence (build → shutdown → rebuild → assert resumed seed > prior high-water), monotonic-write race regression, sequential idempotency ofshutdown_and_release, concurrent-CAS-race safety undermulti_threadruntime. Plus an in-module unit pin for corrupt-entry recovery.
Added — observe_lamport sync-receive wire-up (peat-mesh#198, closes peat-mesh#196)
AutomergeSyncCoordinator::set_lamport_clock(Arc<AtomicU64>)— opt-in setter, called fromAutomergeBackend::with_irohso the coordinator's tombstone-receive path advances the clock automatically. Standalone coordinator consumers (peat-mesh-node binary, unit tests) skip the call; the receive-side wire-up no-ops and preserves existing behaviour.handle_incoming_tombstoneobservestombstone.lamportbefore applying.handle_incoming_tombstone_batchobserves the max Lamport across the batch in a single CAS (idempotent, equivalent to per-entry observes but cheaper by N-1 attempts).- Shared
observe_atomichelper atpub(crate)visibility — bothAutomergeBackend::observe_lamport(consumer-facing API) and the coordinator's receive-side dispatch share one CAS retry loop, one memory ordering choice (AcqRel/Acquire), and oneu64::MAX - 1cap. - 3 in-tree dispatch-tier pins in
src/storage/automerge_sync.rs::tests: single-tombstone receive advances wired clock past inbound Lamport; batch receive picks max Lamport across entries; no-op when no clock wired (opt-in contract). Plus au64::MAXcap regression test through the fullhandle_incoming_sync_streampath.
Completes the cross-node half of Lamport partial-order semantics: a locally-authored operation that follows an inbound tombstone receive sorts strictly after the observed remote Lamport in the causal order Lamport's partial order captures.
Out of scope (acknowledged follow-up)
- Lookahead persistence for unclean-shutdown bounds. rc.28 ships persistence with a 1-second periodic flush cadence + clean-shutdown flush; worst-case loss on an unclean crash is up to one periodic interval of issued values. Tightening that (smaller interval at the cost of redb write churn) is a trade-off better made after observing real workloads.
- Backend-level abort of the Lamport flush task on
shutdown_and_release. The current shape relies on the existingWeak<AutomergeStore>-upgrade-fails-on-drop pattern (consistent withobserver_task). The post-shutdown periodic-tick window is harmless because the storage-layer write is monotonic. Carried forward.