Skip to content

L2-optimism follow-up ledger

L2-optimism follow-up ledger

The refactor that made the replay engine atomic and pure landed green (unit, integration, backend lib, send-path gate, live Stalwart 4/4, fmt --check, clippy -D warnings). The review found the architecture sound and the four contract slices intact; the items below are debt it surfaced — tracked here, prioritized, with locations and fix sketches.

Progress (2026-07-18): ALL ITEMS LANDED. F7/F8/F10 (dd1589b9), F4–F5+F11 (b83ba529), F9 (f8dd8ba0), F6 (this change). The ledger is now history — kept so the original findings stay traceable.

P1 — phantom-prevention core

These two sit inside the “words are never silently dropped” guarantee and are the highest priority.

F6. ✅ LANDED — the 5000-op cap is removed

OUTBOX_LIST_SAFETY_LIMIT capped the fold’s in-txn log read (and the pending / unsettled / settled listings) at 5000 ops, silently dropping the newest ops from the fold when an account exceeded it — a phantom inside the atomicity core. The cap is deleted; collect_operations no longer takes a limit, and all five reads (list_pending_operations, list_unsettled_operations, list_settled_operations, derive_overlay, remove_op_and_derive) read the full account log. Chosen over paging/backpressure for simplicity and correctness; the O(rows × ops) cost during sync is the accepted trade-off (realistic backlogs are tiny; a pathologically large outbox is a root-cause problem — stuck flusher / no sync — to fix separately, not by truncating the fold).

F7. ✅ LANDED — content-kind list single-sourced

The SQL fragment and the Rust predicate now both derive from OperationKind::CONTENT_KINDS; the compiler keeps them in lockstep (see commit dd1589b9). Kept here for history.

The “failed content ops stay foldable” SQL fragment is copy-pasted at outbox.rs:317, overlay.rs:130, overlay.rs:192, plus the migration at schema.rs:514/661, and must agree with the Rust predicates is_replayable/is_content_op (replay.rs). Adding a new content-op kind (a future draft shape) requires changing all of them in lockstep — a miss-site silently breaks the “parked content stays visible” rule.

Fix: one source of truth — a is_content_op_kind(kind) used to build the SQL fragment (or a stored set), and the Rust predicates calling the same function. The three fold-SQL sites should share a single const/fragment.

P2 — maintainability / dead surface

F8. ✅ LANDED — apply-mutation match extracted

apply_overlay_mutation_tx (prod) and apply_mutation_to_overlay_map (test) share the mutation→write mapping; a new OverlayMutation variant lands in one place per plane (commit dd1589b9). Kept here for history.

match mutation { Upsert(record) => upsert_overlay_tx(…), Tombstone => …, Remove => …, Keep => {} } plus the now_visible derivation appears in both derive_overlay (overlay.rs:153-163) and remove_op_and_derive (overlay.rs:213-223), and twice more in the test store (store_sync_cache_impls.rs). A new OverlayMutation variant must be added at every site.

Fix: extract apply_overlay_mutation_tx(tx, account_id, row_id, mutation) -> bool (returns now_visible) shared by both derives; the test store gets a matching helper.

F9. ✅ LANDED — replay_account_overrides wired to the post-repair path

It was load-bearing, not dead: a quarantined-and-rebuilt store salvages the op log and draft_alias map but starts with an EMPTY overlay, and the only thing that re-derived parked content (an unsent draft, a provisional Sent row with no base successor) was the post-sync sweep — which runs only at a completed cycle’s end. AppState::assemble now replays each account’s overrides when open_with_repair reports a repair, before starting the supervisor, so the derived view reappears immediately. Tested with a disabled-account continuity test (the supervisor skips a disabled account, so no sync/flush runs — isolating the wiring).

Fix: either wire it to the quarantine-and-rebuild path the model specifies (L2-optimism “Durability classes”), or delete it and document that the per-row replay_base_write + sweep is the only recovery. Do not leave it ambiguous.

F10. ✅ LANDED — design-history comments rewritten

The three comments now state invariants directly (commit dd1589b9). Kept here for history.

tests/replay.rs:1415 and :1505 (and one more) explain the new behavior by contrast with the retired non-atomic / clock-based fold. The house rule is that comments describe the current design, not its history — these age into folklore and mislead once the “old” code is forgotten.

Fix: rewrite them to state the invariant directly (“the fold is a pure function of (log, base); a Pending send with an elapsed but undispatched hold stays held…”) without the comparative frame.

P3 — echo / TOCTOU (self-healing, plumbing exists)

F4–F5. ✅ LANDED — truncate echo uses effective visibility

DeriveDiff now carries both retired() (overlay-only — for a provisional id with no base successor, e.g. adoption) and effectively_retired() (base ∪ overlay — for an id that may have a base successor, e.g. truncation). The truncate site uses effectively_retired(), so a base-backed content row no longer emits a spurious/double deleted echo. Adopt keeps retired() (no-base provisional id — equivalent). See commit 3491fab4. A dedicated base-caught- up content-op truncate test is still wanted (the TestStore’s global base pretense makes a precise one subtle).

The truncate/adopt echo now uses DeriveDiff::retired() (overlay-only), not the previous effective-visibility (get_message_summary before/after). For content-op rows (no base) the two agree; for base-backed rows they can differ — a content-op row whose base exists should NOT emit deleted when its overlay retires (base shows through), and retired() alone would. Audit found two sites where this delta is observable.

Fix: extend DeriveDiff with base_present and an effectively_retired() = (base || was) && !(base || now); use it for the echo. Computed in-txn, so it also closes the TOCTOU. (This is the same plumbing the deferred item below needs.)

F11. ✅ LANDED — echo visibility-decision sites use the in-txn DeriveDiff

The five echo sites that used get_message_summary for a VISIBILITY decision (the TOCTOU reads) now use refresh_message_overlay(...).effectively_retired() from the in-txn fold: the three before/after TOCTOU sites (cancel-draft-save, unwind-send-row, held-send-consumed in queue.rs) and the two after-only retire sites (rotation old-key, settle old-id in draft.rs). Closes the race and drops ~7 effective-view reads.

The projection-echo sites KEEP a single get_message_summary read — they serialize the MessageSummary as the event’s projection, and that type carries joined account/conversation/version fields the overlay-plane derive cannot produce. delete_draft keeps its before-read for the deleted echo’s mailbox scope. These are content reads, not visibility decisions — no TOCTOU.

refresh_message_overlay already returned DeriveDiff (every caller ignored it); the echo sites now use it, so no port change was needed. Replaces the old “Deferred” item.

These three (outbox/queue.rs, outbox/draft.rs) were not converted to DeriveDiff in the refactor — they read the effective view before and after a refresh, a TOCTOU under concurrency (self-healing on the next replay, but real). The plumbing exists: refresh_message_overlay returns DeriveDiff.

Fix: convert the three sites to refresh_message_overlay(...).await?.retired() (or effectively_retired() once F4–F5 lands), deleting the duplicated before/after reads. This is the last echo-cleanup site.