2026-06-02. Two independent read-only red-team Plan agents attacked
docs/design/durable-global-commit-sequence-redesign.md (D1) from disjoint angles
(intra-session linearization vs cross-restart/recovery/back-compat). Headline: BOTH broke it,
complementarily, at the ARCHITECTURAL level. D1's core idea (a durable global commit-sequence as
the LWW replay key) survives — but D1 made two unstated wrong assumptions that both red-teams falsified.
I (the orchestrator) independently re-verified the single load-bearing finding (RT#1 F1) against the
actual code; it holds. The loop continues: D1 → (broken) → D2 redesign → red-team D2.
F1 — [CRITICAL] CommitSeqMonotone is FALSE for the insert path. CODE-VERIFIED by orchestrator.
Membership-insert has a split linearization point:
lockfree_cas.rs:928 let root_generation = new_root.version(); :930 compare_exchange),try_set_final (:347 → overlay/node.rs:725
flags.fetch_or(IS_FINAL, AcqRel)), which does NOT bump version (contrast as_final:811 /
with_child:766 / with_value:866, which DO bump).is_final() (:1081), so visibility = try_set_final, not the root CAS.build_path_recursive returns Arc::clone(node) of the shared non-final node (:801, the
prefix-reuse arbiter — comment :787 "try_set_final is the SINGLE atomic arbiter"), so the
finalized node can be reachable and shared, not a fresh path-copy.$\Rightarrow$ For inserts, commit_seq order $\ne$ visibility order. Value-ops (upsert :1659, increment :1441)
are SOUND — they stamp at an atomic-final root CAS (build_value_path_recursive bakes
as_final().with_value, :1781) that IS their visibility point. The asymmetry is the bug.
F2 — [CRITICAL] Cross-op-type inversion: insert ‖ increment/upsert on one term. Insert's deferred
try_set_final vs the value-op's atomic root CAS $\Rightarrow$ same-term insert(seq=lo) ‖ increment(seq=hi) can
have visibility order = increment-then-insert while seq order = insert-then-increment. The
"claim-before-CAS $\Rightarrow$ winner has lower claim" bracket assumes ONE linearization point per op; insert has two.
Concrete data-loss trace (orchestrator-constructed, flows through the prefix-reuse path):
"cats" present $\Rightarrow$ "cat" node_A exists non-final. (1) I=insert("cat") claims seq=5, reuses node_A
(:801), root-CAS publishes spine to node_A (still non-final), pauses before try_set_final.
(2) R=remove("cat") claims seq=6, observes node_A non-final $\Rightarrow$ AlreadyAbsent no-op; under the A.5
fix it appends a Remove record + rank(6). (3) I resumes try_set_final(node_A) $\Rightarrow$ "cat" now
visible present. Replay sorts I(5) Insert before R(6) Remove $\Rightarrow$ "cat" absent $\ne$ visible
present = DATA LOSS. (Note: pure-F1 where I's try_set_final lands on a dead node → no loss; the
exploit requires the shared-reachable node_A prefix path + a higher-seq competitor; for the remove
case it also requires the A.5 idempotent-rank. Narrow but real, and it falsifies the theorem.)
F3 — [HIGH] A.6 frontier "no same-term out-orderer" fails with TWO concurrent same-term ops in their
two-append windows simultaneously — both data-durable, both rank-pending $\Rightarrow$ both fall back to
generation_of=lsn (LARGE) and can out-sort a third ranked op or each other.
F4 — [HIGH] Torn multi-op document-tx batch defeats single-rank-at-CommitTx. Order-A + group-commit
does not guarantee atomic all-or-nothing durability of the per-op data appends vs the CommitTx append;
a partial fsync can leave op-k's data missing while CommitTx is present $\Rightarrow$ replay applies a strict
subset of the committed batch.
F5 — [MED/HIGH] C1′ bail-rank strict-inequality not guaranteed: the claim is taken at APPEND, after
the READ (R-1 "decide before append" pushes the claim too late). A superseder can claim a LOWER seq
and be mid-CAS while the bailer's g_read ties/exceeds it. Needs the claim at the read instant.
F6 — [MED] Idempotent-arm A.5 inverse-deletion enabled by F1 (the F1 trace's step 2). A.5 fixes one
resurrection but, with F1's split-LP, introduces the inverse (a no-op AlreadyAbsent deletes a
concurrently-finalized present key). Unique claims fix a tie but not an inversion.
RT#1 verified SOUND: the seed/A.2 fix within one process lifetime; increment commutativity/A.3 among increments; all-paths unification/A.1 structurally (it inherits F1 uniformly — a correctness-of-key bug, not a path-divergence bug).
RT#1 root cause (one line): D1 assumes every op has a single linearization point at which both
visibility and commit_seq are fixed. Membership-insert violates this — seq at the root CAS (:928),
visibility at the later try_set_final (node.rs:725), which does not bump the version.
F1 — [CRITICAL] Post-checkpoint reseed → A.2 reborn. The seed scan reads ONLY the active wal_path
(mmap_ctor.rs:300-321, io_uring_ctor.rs:133), never archives. Checkpoint
(persist.rs:108-165 → rotate_to_archive → wal.truncate() then set_min_lsn(cp+1)) resets the
active WAL to empty. The LSN domain survives via a durable FLOOR (set_min_lsn/header
checkpoint_lsn); commit_seq has NO floor and is NOT in the header's unused reserved[20..64]
(header.rs:22). $\Rightarrow$ After a checkpoint the seed restarts at 0 $\Rightarrow$ a new op gets commit_seq=1 while a
pre-checkpoint same-term op survives on-disk at commit_seq=300 $\Rightarrow$ later reconcile mis-sorts the NEW op
BELOW the old $\Rightarrow$ acked write lost. The exact A.2 class the redesign exists to kill, reborn.
F2 — [CRITICAL] recover_from_archives neither reconciles NOR seeds. mmap_ctor.rs:1137 →
rebuild_from_wal_segments (recovery.rs:1443) replays each segment in raw per-record order
(CommitRank → vec![] at :370), never reconcile_lww $\Rightarrow$ s019 recurs on the archive path; and it
returns only (records, terms) with no channel to report a max_commit_seq $\Rightarrow$ zero seed.
F3 — [HIGH] Mixed v2/v3 file: one header byte, two key-domains, no enforced clean break. A v3 binary
opens & APPENDS to a cf1f80c v2 file (from_bytes accepts [MIN_SUPPORTED=1 ..= VERSION],
header.rs:82; no "refuse v2-for-append" guard). The "v2 root-version vs v3 commit_seq comparator" is a
distinction without a difference — both are u64-ascending sort_by (generation,lsn)
(recovery.rs:273,296); selecting by wal_version cannot disambiguate intra-file mixed ranks. A small
v2 root-version (2) and a small early v3 commit_seq (2) on one term tie-break by lsn, not commit order.
F4 — [HIGH] reconcile replays the unranked frontier and it can WIN. reconcile_lww replays EVERY
in-scope record and assigns unranked records generation_of=lsn (LARGE, :273); it ignores the
watermark (no watermark param). A torn batch where op_A(T) ranked at seq=10 and a later same-term op_B's
data is durable but its rank crashed $\Rightarrow$ op_B replays at $lsn\gg 10$ $\Rightarrow$ op_B wins over op_A. The
watermark-stall argument is about ACK, not REPLAY.
F5 — [HIGH] single-CommitRank-at-CommitTx widens A.6 to whole-batch granularity; tx-gating is
unimplemented. reconcile_lww expands batches unconditionally and treats Begin/Commit/AbortTx as
no-ops (:343-370) — A.4 entirely unfixed. A committed tx whose single CommitRank append crashes $\Rightarrow$ the
ENTIRE batch unranked $\Rightarrow$ every entry mis-sorts.
F6 — [MED] IncrementalRecovery per-window buffering can't preserve cross-window order.
process_record (:932) uses a single pending_ops and BeginTx does pending_ops.clear() (:940)
— tx-unsafe today. Never-checkpoint+v3 $\Rightarrow$ one unbounded window $\Rightarrow$ unbounded buffer (OOM) or fail-closed
(streaming v3 recovery unavailable, not merely restricted).
F7 — [SOUND] Codec sharing does not cross-contaminate the counter — commit_seq is a per-instance
AtomicU64; byte/vocab are separate instances. Verified sound.
RT#2 root structural defect (uniting F1+F2): the LSN domain is reseedable across truncation via a
persisted FLOOR (set_min_lsn/header checkpoint_lsn); D1's commit_seq is reconstructed by SCANNING
CommitRank records that checkpoint/truncate DELETES, and stores no durable floor. commit_seq needs the
same floor-persistence the LSN already has — scanning is insufficient.
D1's core idea (durable global commit-seq = LWW replay key) is RIGHT. It failed on two axes, both with a known principled fix direction:
The membership-insert split-LP is THE crux. Two candidate fixes (D2 must pick + PROVE no prefix-bug
regression — the prefix bug is why try_set_final-as-arbiter exists, fixed Phase-A; vocab is already
single-phase and correct):
try_set_final trick. Then ALL
ops linearize at the root CAS and claim-before-CAS is correct universally. RISK: must not reintroduce
the proper-prefix data-loss bug (the Phase-A fix). This is the cleanest if the rebase-merge is proven.try_set_final (when newly==true, the unique finalizer), append the
CommitRank after. RISK: a node finalized then made unreachable by a concurrent parent-edge remove $\Rightarrow$
phantom rank; needs a reachability/abort argument that is hard in a lock-free tree. Likely inferior.commit_seq floor in the header's unused reserved[20..64]; at checkpoint, set the floor =
max commit_seq subsumed by the checkpoint (mirror set_min_lsn). Seed = max(header floor, scan).recover_from_archives / rebuild_from_wal_segments: (a) route through reconcile_lww, (b) thread a
max_commit_seq out so the rebuilt trie seeds correctly.reconcile_lww must exclude (or down-rank) unranked records above the committed watermark — an
unranked-but-durable data record is genuinely ambiguous (CAS may/may-not have landed); it must NOT win
by generation_of=lsn-is-large. Give unranked-above-watermark records a LOSING rank, or drop them
(they were never confirmed visible). Add the watermark as a reconcile parameter.reconcile_lww honors the tx state machine (drop non-Committed). Per-op ranking inside a committed tx
(not one batch rank) so increments order against non-tx same-term ops. IncrementalRecovery v3:
fail-closed for never-checkpoint (documented availability constraint), else per-checkpoint-window.g_read < any superseder strictly.db7cb2d+cf1f80c remain committed (cf1f80c is a PARTIAL
fix: closes the single-session reconcile_lww path only); F0 hacks remain uncommitted/untouched; the
flip stays PAUSED.Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |