You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
From the local-store durability study (three-lens fable analysis, 2026-07-25). Decision recorded in ADR-0014 (#610).
Decision
The local store is per-call committed durability, not per-event atomic: each set/delete is an individually fsync-durable redb transaction, program-order, read-your-writes, and a trap (fault, panic, OutOfFuel, deadline, crash) freezes state at the last completed call and never rewinds it. Per-event atomicity is rejected: it would roll back the keeper Journal's RESERVED marker on exactly the trap it exists to survive, breaking the reserve/commit/reconcile design (#583-#587, #574), and it defends against nothing (dispatch is single-actor serialised, redb single-process). The sanctioned atomicity scope is a single opt-in batch verb.
API surface: no new host verbs beyond apply
The host primitive is complete: get/set/delete/list-keys/count/contains/len plus apply(set|delete). set is already an upsert (insert or overwrite). No compare-and-swap, atomic increment, or prefix-delete host verb is warranted, because every module is key-isolated by a keccak prefix (ADR-0003) and dispatch is single-actor serialised, so no two writers ever touch the same key: get-then-apply is effectively atomic, and prefix-delete composes as list-keys + apply(deletes). WIT variants stay additively extensible, so a conditional write-op can be added later if a concrete need appears; not now. Enrich the SDK, not the seam: every WIT addition is a versioned cross-repo change after the cleave, so a minimal host seam is itself the low-effort path.
Build
Host (Wave 1, done)
apply WIT verb + ModuleStore::apply (host: atomic apply batch verb for local-store #611): additive apply(list<write-op>) with write-op = set(key,value) | delete(key), one begin_write/commit, whole-batch net quota projection, op-count and byte caps, all-or-nothing. Landed.
SDK ergonomics (Wave 2, after #611 merges, needs apply in the bindings)
The pain is hand-serialising every list<u8> and rewriting get-modify-set and batch patterns; all pure SDK sugar over the primitives, zero host surface. A module should write typed, intention-revealing code and never touch raw bytes or hand-roll a batch.
TypedCell<T>: a borsh-typed key (cell.get/cell.set). The single biggest win; kills manual serialisation.
Typed keyed collection: list-by-prefix plus typed values, for sets of things (watches, orders), so modules stop hand-rolling indexes.
Counter / accumulator helper (get-inc-set, safe under single-actor dispatch).
WriteBatch builder over apply (stage in guest memory, flush in one host call; a pre-flush trap discards for free) and a clear_prefix helper (list-keys + apply-deletes, atomic).
Journal guard combinator: reserve, run the submit closure, then commit or park, enforcing reserve-before-effect ordering by API shape. The Journal's single-key markers stay on plain set (they span an await, so they structurally cannot ride one apply).
Test kit (Wave 1, done)
Trap-injection harness (sdk: trap-injection harness proving reconcile converges from every torn write prefix #612): a mock local store that traps after the Nth write, sweeping every torn prefix of the keeper reserve/commit flow and asserting reconcile converges exactly-once. Test-only. Plus the review rule: no in-store invariant may span two set calls unless the intermediate state is self-healing or the writes use apply.
Constraints
apply is an additive nexum:host@0.1.0 verb landed pre-cleave (cheap in-monorepo fold, expensive cross-repo ripple after the carve). The Journal and the reserve/commit/reconcile code stay untouched; the SDK helpers add no host surface.
Acceptance
apply lands additive, quota-projected, capped, all-or-nothing (done, #611); the trap-injection harness proves crash convergence from every torn prefix (done, #612); ADR-0014 and the docs state the per-call contract with no rollback claim (#610, #606); the Wave 2 SDK helpers give module authors typed cells, collections, counters, atomic batches and a Journal guard, so no module reimplements store atomicity or serialisation by hand.
Decision
The local store is per-call committed durability, not per-event atomic: each
set/deleteis an individually fsync-durable redb transaction, program-order, read-your-writes, and a trap (fault, panic, OutOfFuel, deadline, crash) freezes state at the last completed call and never rewinds it. Per-event atomicity is rejected: it would roll back the keeper Journal'sRESERVEDmarker on exactly the trap it exists to survive, breaking the reserve/commit/reconcile design (#583-#587, #574), and it defends against nothing (dispatch is single-actor serialised, redb single-process). The sanctioned atomicity scope is a single opt-in batch verb.API surface: no new host verbs beyond apply
The host primitive is complete:
get/set/delete/list-keys/count/contains/lenplusapply(set|delete).setis already an upsert (insert or overwrite). No compare-and-swap, atomic increment, or prefix-delete host verb is warranted, because every module is key-isolated by a keccak prefix (ADR-0003) and dispatch is single-actor serialised, so no two writers ever touch the same key:get-then-applyis effectively atomic, and prefix-delete composes aslist-keys+apply(deletes). WIT variants stay additively extensible, so a conditionalwrite-opcan be added later if a concrete need appears; not now. Enrich the SDK, not the seam: every WIT addition is a versioned cross-repo change after the cleave, so a minimal host seam is itself the low-effort path.Build
Host (Wave 1, done)
applyWIT verb +ModuleStore::apply(host: atomic apply batch verb for local-store #611): additiveapply(list<write-op>)withwrite-op = set(key,value) | delete(key), onebegin_write/commit, whole-batch net quota projection, op-count and byte caps, all-or-nothing. Landed.SDK ergonomics (Wave 2, after #611 merges, needs
applyin the bindings)The pain is hand-serialising every
list<u8>and rewriting get-modify-set and batch patterns; all pure SDK sugar over the primitives, zero host surface. A module should write typed, intention-revealing code and never touch raw bytes or hand-roll a batch.TypedCell<T>: a borsh-typed key (cell.get/cell.set). The single biggest win; kills manual serialisation.WriteBatchbuilder overapply(stage in guest memory, flush in one host call; a pre-flush trap discards for free) and aclear_prefixhelper (list-keys+apply-deletes, atomic).set(they span an await, so they structurally cannot ride oneapply).Test kit (Wave 1, done)
setcalls unless the intermediate state is self-healing or the writes useapply.Constraints
applyis an additivenexum:host@0.1.0verb landed pre-cleave (cheap in-monorepo fold, expensive cross-repo ripple after the carve). The Journal and the reserve/commit/reconcile code stay untouched; the SDK helpers add no host surface.Acceptance
applylands additive, quota-projected, capped, all-or-nothing (done, #611); the trap-injection harness proves crash convergence from every torn prefix (done, #612); ADR-0014 and the docs state the per-call contract with no rollback claim (#610, #606); the Wave 2 SDK helpers give module authors typed cells, collections, counters, atomic batches and a Journal guard, so no module reimplements store atomicity or serialisation by hand.