Skip to content

D-MBX-A6-P3d: durable-witness reshape (crash-durable paired move) + temporal.rs layer-1 causal deinterlacing - #878

Open
AdaWorldAPI wants to merge 8 commits into
mainfrom
claude/medcare-rs-continue-ufsazd
Open

D-MBX-A6-P3d: durable-witness reshape (crash-durable paired move) + temporal.rs layer-1 causal deinterlacing#878
AdaWorldAPI wants to merge 8 commits into
mainfrom
claude/medcare-rs-continue-ufsazd

Conversation

@AdaWorldAPI

@AdaWorldAPI AdaWorldAPI commented Aug 1, 2026

Copy link
Copy Markdown
Owner

What & why

The POST-write half of the D-MBX-A6 persistence sink, reshaped to close a
crash-durability gap the operator named, plus the missing layer-1 of
temporal.rs. Both are mandated before any concrete LanceShardSink
this PR builds no production sink.

The gap

At the prior split-phase shape, persist_cast appended only the payload; the
paired KanbanMove lived solely in the in-memory DurableReceipt. Then:

WAL append lands (durable) → process dies before apply_durable_step → the
receipt evaporates → the paired move is lost → the KanbanStep never fires —
even though the durable SoA state moved.

Silent: storage advanced, lifecycle did not. An in-memory receipt is not a
durable record.

The reshape (persist_sink)

  • DurableWitness{owner, cast_id, cycle, paired_move} is CO-LOCATED with the
    SoA payload in ONE persistence generation
    DurableWrite::append(&witness, &payload) lands both atomically. The witness is the crash-durable copy of the
    move, never in-memory-only.
  • DurableReceipt now merely references that durable material (via its
    DurableCoordinate); it is not the only copy.
  • DurableWrite::scan_witnesses — replay seam; reads the co-located witnesses
    back (a read of what durable storage holds, not a separate ack ledger —
    E-ACK-ELIMINATED-1 stands).
  • recover_and_apply(owner, witnesses) — crash recovery: runs temporal layer-1
    to find the owner's pending tail and replays in cast order; stale moves
    (already reflected in the recovered state) are skipped (idempotent).
  • apply_durable_step gains a from == phase guard (PersistError::StalePhase)
    so a stale/out-of-order apply on the synchronous path is surfaced loudly.
  • Durability proof stays the DurableCoordinate (shard/epoch/wal-position),
    never a LanceVersion — the write is durable before any base manifest
    version attaches.

temporal.rs layer-1 — CAUSAL deinterlacing

New LocalCausalRow{owner, cast_seq} + local_trajectories /
local_trajectory_of split a globally-interleaved durable log into per-owner
local causal chains, ordered by cast_seq: A@s0, C@s0, B@s0, A@s1
owner A's chain [A@s0, A@s1], with the interleaved owners removed from A's
timeline. This composes with the existing layer-2 epistemic projection
(classify / deinterlace): layer-1 rebuilds the chain, layer-2 filters it by
the reader's horizon. DurableWitness implements LocalCausalRow.

Falsifiers (the operator's five integration falsifiers — all green)

  1. layer-1 deinterlaces the interleaved global log into an owner-local chain
    (+ vacuity guard: other owners' rows dropped, not reordered)
  2. one owner's batch replays in cast order (over a deliberately
    seq-descending log)
  3. WAL succeeds + crash (receipt dropped) → recovery reconstructs + applies the
    move from the durable witness; idempotent re-run applies nothing
  4. WAL fails → no witness lands → nothing to replay → no move, no step
  5. WAL-visible-before-manifest → recovery identifies latest local state from
    cast order; no dataset version exists

Gates

  • 343 lance-graph-planner lib tests pass; cargo clippy -p lance-graph-planner
    • cargo fmt -p clean.
  • Builds no concrete LanceShardSink (deferred per operator ruling — gated
    on these crash falsifiers going green).
  • No dependency changes (Cargo.lock untouched).

Deliberately NOT done

Concrete LanceShardSink (ShardWriter::put, enable_memtable + durable_write);
confirmation ledger / replay queue / per-thought ack / custom WAL / ractor
hot-path callback.

Finding: E-THE-PAIRED-MOVE-MUST-BE-DURABLE-CO-LOCATED-NOT-IN-MEMORY-ONLY-1.

🤖 Generated with Claude Code


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Added cycle-level durable persistence with atomic commits and version tracking.
    • Added deterministic ordering and coalescing for changes within each cycle.
    • Added recovery and replay based on persisted progress markers.
    • Added per-mailbox trajectory views for interleaved activity.
  • Bug Fixes

    • Prevented incomplete cycles from appearing in sealed reads.
    • Prevented duplicate recovery application during retries.
    • Added safeguards against stale phases and ownership mismatches.
    • Ensured durable changes remain correctly ordered during recovery.

claude added 4 commits August 1, 2026 22:28
…nbanstep)

The POST-write half of the fire-and-forget flow (pre-write half = owner_adapter, #877).
Runs on the independent persistence path — the thinker already cast and moved on.

persist_then_step(owner, write, paired, bytes): durable write FIRST, then — and only on
a successful write — apply the cast's PAIRED move via the owner's checked try_advance_phase.

Ordering invariant proven by 5 lance-free falsifiable probes:
- no successful write => no step (owner untouched);
- successful write => the PAIRED move applied exactly once;
- applies paired.to NOT next_phases().first() — from Evaluation the generic successor is
  Commit, but a paired Evaluation->Prune veto (free-won't) is applied instead (key falsifier);
- a landed write with no paired move is a durable no-step;
- an illegal paired edge is surfaced (PersistError::Illegal), owner untouched.

The DurableWrite seam is the durable-append surface; its concrete impl (next slice, a
lance-having crate) wires lance's OFFICIAL MemWAL (dataset::mem_wal::WalAppender::append,
Copyright The Lance Authors) over Arrow RecordBatch — hand-rolling no WAL, inventing nothing.
Keeping it a trait leaves the ordering core lance-free / protoc-free (probe-first).

Co-Authored-By: Claude <noreply@anthropic.com>
…(operator ruling)

A monolithic persist_then_step(&mut owner, ..., bytes).await would hold &mut owner AND
owner-borrowed bytes across the WAL I/O await — immobilizing the owner during object-store
latency and defeating fire-and-forget. Split into:

- persist_cast(sink, PersistCast) -> DurableReceipt   (async; NO owner in the signature —
  never borrowed across the await; the owner stays free while durability runs);
- apply_durable_step(&mut owner, DurableReceipt)      (sync; NO await; the exclusive owner
  borrow is held only here, outside the storage-latency window).

Durability type corrected: a WAL append yields a DurableCoordinate (shard + writer_epoch +
wal_entry_position), NOT a DatasetVersion (that arrives later via MemTable flush + manifest
commit). DurableReceipt carries the coordinate + the paired move; a receipt is refused for a
foreign owner (OwnerMismatch).

7 probes green: success->receipt, failure->no receipt (=> no step reachable), paired-move
applied, paired!=generic (Evaluation->Prune veto not Commit), no-move no-step, illegal edge
surfaced, foreign-owner refused. Concrete impl (next): lance 7 ShardWriter::put
(enable_memtable + durable_write) over an Arrow RecordBatch with shared-buffer ownership.

Co-Authored-By: Claude <noreply@anthropic.com>
…terlacing

Closes the crash-durability gap the operator named on the persistence sink,
and wires temporal.rs layer-1 — both mandated BEFORE any concrete LanceShardSink.

## The gap (persist_sink)

At the prior split-phase shape, persist_cast appended only the payload; the
paired move rode solely in the in-memory DurableReceipt. WAL append lands →
process dies before apply_durable_step → the receipt evaporates → the paired
move is lost → the KanbanStep never fires, though the durable SoA state moved.
Silent: storage advanced, lifecycle did not.

## The reshape

- DurableWitness (owner, cast_id, cycle, paired_move) is CO-LOCATED with the
  SoA payload in ONE persistence generation: DurableWrite::append(&witness,
  &payload). The witness is the crash-durable copy of the move, never
  in-memory-only.
- DurableReceipt now merely REFERENCES that durable material (via its
  DurableCoordinate); it is not the only copy of the move.
- DurableWrite::scan_witnesses replay seam reads the co-located witnesses back
  — a read of what durable storage already holds, NOT a separate ack ledger.
- recover_and_apply(owner, witnesses): crash recovery reads the witnesses, runs
  temporal layer-1 to find the owner's pending tail, and replays in cast order;
  stale moves (already reflected in the recovered state) are skipped (idempotent).
- apply_durable_step gains a from==phase guard (PersistError::StalePhase) so a
  stale/out-of-order apply on the synchronous path is surfaced loudly.

Durability proof stays the DurableCoordinate (shard/epoch/wal position), never a
LanceVersion — the write is durable before any base manifest version attaches.

## temporal.rs layer-1 — CAUSAL deinterlacing

New LocalCausalRow trait (owner + cast_seq) + local_trajectories /
local_trajectory_of split a globally-interleaved durable log into per-owner
LOCAL causal chains, ordered by cast_seq. A@s0,C@s0,B@s0,A@s1 → owner A's chain
[A@s0,A@s1]; the interleaved owners are REMOVED from A's timeline. This is the
layer-1 counterpart to the existing layer-2 epistemic projection (classify /
deinterlace); the two compose (layer-1 reconstructs the chain, layer-2 filters
it by the reader's horizon). DurableWitness implements LocalCausalRow.

## Falsifiers (all green — the operator's five integration falsifiers)

1. layer-1 deinterlaces interleaved global log → owner-local chain (+ vacuity
   guard: other owners' rows dropped, not reordered)
2. one owner's batch replays in cast order (deliberately seq-descending log)
3. WAL succeeds + crash (receipt dropped) → recovery reconstructs+applies the
   move from the durable witness; idempotent re-run applies nothing
4. WAL fails → no witness lands → nothing to replay → no move, no step
5. WAL-visible-before-manifest → recovery identifies latest local state from
   cast order, no dataset version exists

343 planner lib tests pass; clippy clean; fmt clean. Builds NO concrete sink.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCGhDYoQBXs3poaR7sFuqp
Board hygiene for the persist_sink / temporal changes (same logical unit):
- EPIPHANIES: E-THE-PAIRED-MOVE-MUST-BE-DURABLE-CO-LOCATED-NOT-IN-MEMORY-ONLY-1
  (the crash gap, the co-location rule, temporal's missing layer-1, 5 falsifiers)
- STATUS_BOARD: D-MBX-A6-P3d row (persistence ordering core + reshape + layer-1)
- LATEST_STATE: persist_sink + temporal layer-1 contract-inventory entries

Concrete LanceShardSink remains deferred per operator ruling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCGhDYoQBXs3poaR7sFuqp
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@AdaWorldAPI, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 921cc55a-91b2-499b-ba74-8fbd575ac993

📥 Commits

Reviewing files that changed from the base of the PR and between b6f904c and 7e7928a.

📒 Files selected for processing (2)
  • .claude/board/LATEST_STATE.md
  • crates/lance-graph-planner/src/persist_sink.rs
📝 Walkthrough

Walkthrough

Added a public cycle-level persistence sink. It stably orders and coalesces cycle landings, commits each cycle with one WAL append, exposes sealed reads, and supports watermark-based owner recovery. Added causal per-owner trajectory helpers and updated contract records.

Changes

Cycle persistence and causal recovery

Layer / File(s) Summary
Cycle contracts and atomic commit
crates/lance-graph-planner/src/lib.rs, crates/lance-graph-planner/src/persist_sink.rs
Added cycle persistence types, stable stream ordering, row coalescing, sealed-read interfaces, and one-append cycle commits.
Sealed reads and owner recovery
crates/lance-graph-planner/src/persist_sink.rs
Recovery filters by owner and watermark, applies paired moves in stored order, and rejects owner, phase, and transition violations.
Causal trajectories and contract records
crates/lance-graph-planner/src/temporal.rs, .claude/board/*
Added owner-local trajectory reconstruction and tests. Updated board records for cycle persistence, recovery, and validation status.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: claude

Poem

A rabbit orders cycles in line,
Folds each row and seals the sign.
WAL keeps one version bright,
Watermarks guide replay right.
Local trails sort clean and fine.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately references temporal causal deinterlacing and the persistence redesign, although it uses the retired durable-witness terminology.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c7424cd. Configure here.

fn cast_seq(&self) -> u64 {
self.cast_id.0
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Witness replay ignores writer epoch

High Severity

Crash recovery orders an owner’s witnesses only by cast_id.0, but DurableWitness does not carry writer_epoch or WAL position even though DurableCoordinate does. BatchWriter assigns CastId from a counter that resets on restart, so scan_witnesses can return multiple rows with the same cast_seq from different writer lifetimes. Stable sort then depends on scan order, and recover_and_apply may apply the wrong moves or skip pending steps.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c7424cd. Configure here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c7424cd986

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +330 to +334
if mv.from != owner.phase() {
// Stale: already reflected in the recovered durable state. Skip
// (recovery is idempotent), do not surface — unlike the synchronous
// single-receipt path, a stale move here is expected, not an error.
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Track replay progress instead of inferring it from the phase

When a recovered owner has completed the legal Evaluation → Plan → Planning loop, its phase is Planning again, so the first historical Planning → CognitiveWork witness matches and this function replays the entire old cycle. The owner returns to Planning, meaning every subsequent recovery repeats the cycle, increments its cycle counter, and emits duplicate steps despite the claimed idempotence. Select the pending tail using a durable cast/cycle watermark rather than treating every phase mismatch as proof that a witness was already applied.

Useful? React with 👍 / 👎.

Comment on lines +289 to +293
if mv.from != owner.phase() {
return Err(PersistError::StalePhase {
owner_phase: owner.phase(),
move_from: mv.from,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Order concurrent receipts before consuming them

When two stacked casts for one owner are persisted concurrently, the later append can complete first. Applying that receipt while the owner is still at the earlier phase returns StalePhase, but the receipt is consumed by value and the error does not return it; after the earlier receipt succeeds, the owner remains stuck until a crash-time full scan happens. Since DurableWrite explicitly supports concurrent drains, normal completion reordering must be buffered by cast_id or otherwise remain retryable instead of discarding the receipt.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (6)
crates/lance-graph-planner/src/temporal.rs (1)

397-424: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Document the tie-break for a duplicated cast_seq.

sort_by_key is a stable sort, so two rows of one owner that share a cast_seq keep their global durable-log order. recover_and_apply replays in exactly this order, which means a duplicated cast_seq makes replay order depend on log arrival.

The cast_seq doc calls the value "monotonic", but nothing in the trait or in local_trajectories enforces uniqueness, and DurableWitness::cast_seq just returns CastId.0. A retried append or an overlapping writer epoch would produce a duplicate.

State the precondition and the tie-break on the trait method, so a future implementor knows that uniqueness per owner is required and that ties fall back to log order.

📝 Proposed doc change
     /// The owner-local monotonic cast sequence (e.g. `CastId.0`) — the ordering
     /// key WITHIN one owner's trajectory. Cross-owner values are never compared;
     /// only rows sharing an `owner()` are ordered against each other.
+    ///
+    /// PRECONDITION: unique per owner. The layer-1 sort is stable, so rows of one
+    /// owner sharing a `cast_seq` keep their global durable-log order — a
+    /// tie-break that makes replay order depend on log arrival. Implementors must
+    /// not reuse a `cast_seq` within one owner.
     fn cast_seq(&self) -> u64;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/lance-graph-planner/src/temporal.rs` around lines 397 - 424, Update
the documentation for LocalCausalRow::cast_seq to state that cast_seq must be
unique and monotonic within each owner; if duplicate values occur,
local_trajectories preserves their original global durable-log order as the
tie-break because the stable sort is used.
crates/lance-graph-planner/src/persist_sink.rs (5)

406-419: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

The test double cannot exercise the documented concurrency claim.

FakeSink uses Cell and RefCell, so it is !Sync. The DurableWrite doc at lines 136-137 states that &self is shared because "many casts drain concurrently". No test drains two casts concurrently, and this double cannot be shared across tasks to do so.

If you want the concurrency claim covered before LanceShardSink lands, switch landed to Mutex<Vec<_>> and calls to AtomicU32, then add one #[tokio::test(flavor = "multi_thread")] that joins two persist_cast futures and asserts both witnesses landed. Otherwise the claim rests on the concrete sink alone.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/lance-graph-planner/src/persist_sink.rs` around lines 406 - 419, The
FakeSink test double cannot be shared across concurrent persist_cast calls
because it uses Cell and RefCell, leaving the documented DurableWrite
concurrency behavior untested. Update FakeSink to use AtomicU32 for calls and
Mutex<Vec<DurableWitness>> for landed, then add a multi-threaded tokio test that
joins two persist_cast futures and verifies both witnesses were recorded.

694-723: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add recovery coverage for a witness that carries no paired move.

recover_and_apply line 329 skips a witness whose paired_move is None. No test reaches that branch. Every helper in the recovery tests (cast_of) always sets Some(...).

Extend this test with a durable no-step cast interleaved into owner 42's chain. The assertion should show that the no-step witness is skipped and the following move still applies in cast order. This pins the interaction between durable no-steps and cast-ordered replay.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/lance-graph-planner/src/persist_sink.rs` around lines 694 - 723,
Extend
recovery_replays_one_owners_batch_in_cast_order_ignoring_interleaved_owners with
a durable witness for owner 42 whose paired_move is None, interleaved between
its two cast_of moves. Update the expected applied destinations and final phase
assertions to confirm the no-step witness is skipped while the subsequent move
still replays in cast order.

83-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Adopt the snafu error pattern for the public error types.

WriteFailed and PersistError are plain types. They do not implement Display or std::error::Error. Callers outside the crate cannot propagate them into a boxed error or print a message. The crate guideline asks for snafu error patterns.

As per coding guidelines: "reuse snafu error patterns".

♻️ Sketch of the snafu shape
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct WriteFailed(pub String);
+#[derive(Debug, Clone, PartialEq, Eq, snafu::Snafu)]
+#[snafu(display("durable write did not land: {reason}"))]
+pub struct WriteFailed {
+    pub reason: String,
+}

Apply the same treatment to PersistError with one #[snafu(display(...))] per variant.

Also applies to: 209-233

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/lance-graph-planner/src/persist_sink.rs` around lines 83 - 85, Adopt
the established snafu error pattern for the public `WriteFailed` and
`PersistError` types in the persist sink module. Add the required snafu derives
and one `#[snafu(display(...))]` message per error variant, preserving each
type’s existing payloads and semantics so both implement `Display` and
`std::error::Error` for external callers.

Source: Coding guidelines


336-341: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Report the partially applied moves when recovery aborts.

If try_advance_phase fails mid-chain, recover_and_apply returns Err(PersistError::Illegal(_)) and drops applied. The owner is already mutated by every earlier move in the chain. The caller therefore cannot tell how far recovery progressed, and a retry restarts from an unknown position.

Either carry the applied prefix in the error variant, or document on the function that the owner is left mid-chain on Illegal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/lance-graph-planner/src/persist_sink.rs` around lines 336 - 341, The
recover_and_apply flow must preserve information about moves already applied
when owner.try_advance_phase fails. Update PersistError::Illegal or the
recover_and_apply contract so callers receive the applied prefix alongside the
failure, or explicitly document that Illegal leaves the owner mid-chain and
exposes the partial progress; ensure earlier applied moves are not silently
discarded.

155-160: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider bounding the replay scan before the concrete sink lands.

scan_witnesses returns every witness the durable log ever held. Two costs follow:

  • Memory grows with the full log length, not with the pending tail.
  • Recovering k owners calls recover_and_apply k times, and each call scans and clones the whole slice in local_trajectory_of. That is O(k × total_log).

The trait is the right place to fix this, because the seam shape is fixed once LanceShardSink implements it. Two options:

  • Add a from: Option<DurableCoordinate> argument so recovery reads only the tail after the last applied coordinate.
  • For multi-owner recovery, call temporal::local_trajectories once and apply each owner's chain, instead of calling local_trajectory_of per owner.

The module states the concrete sink is deferred, so this is cheap to change now.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/lance-graph-planner/src/persist_sink.rs` around lines 155 - 160,
Update the scan_witnesses trait seam to support bounded replay by accepting an
optional DurableCoordinate lower bound and returning only witnesses after that
coordinate, so recovery does not load the entire durable log. Propagate this
change through the recovery flow, and ensure multi-owner recovery derives local
trajectories once via temporal::local_trajectories rather than repeatedly
scanning and cloning the full witness slice through local_trajectory_of.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/board/STATUS_BOARD.md:
- Line 966: Preserve the append-only history in STATUS_BOARD.md by moving the
new D-MBX-A6-P3d governance row to the beginning of the board entries rather
than inserting it between existing historical rows. Keep all existing rows in
their current order; only use an in-place insertion if repository guidance
explicitly documents this file as a snapshot table.

In `@crates/lance-graph-planner/src/persist_sink.rs`:
- Around line 246-251: Validate that cast.paired_move.mailbox matches cast.owner
before constructing DurableWitness in persist_cast, rejecting mismatches so
cross-owner moves are never persisted. Also add the same invariant check in
apply_durable_step if the existing defense-in-depth pattern requires it.
- Around line 328-340: Update recover_and_apply to use the owner’s durable
last-applied cast_id as the idempotence watermark: skip witnesses whose cast_id
is at or below that watermark, and retain the mv.from versus owner.phase() check
only as a corruption guard. Ensure the watermark is persisted with the recovered
state so replaying a complete cyclic lap after recovery remains idempotent;
otherwise document the limitation and add a regression test covering the full
cycle.

---

Nitpick comments:
In `@crates/lance-graph-planner/src/persist_sink.rs`:
- Around line 406-419: The FakeSink test double cannot be shared across
concurrent persist_cast calls because it uses Cell and RefCell, leaving the
documented DurableWrite concurrency behavior untested. Update FakeSink to use
AtomicU32 for calls and Mutex<Vec<DurableWitness>> for landed, then add a
multi-threaded tokio test that joins two persist_cast futures and verifies both
witnesses were recorded.
- Around line 694-723: Extend
recovery_replays_one_owners_batch_in_cast_order_ignoring_interleaved_owners with
a durable witness for owner 42 whose paired_move is None, interleaved between
its two cast_of moves. Update the expected applied destinations and final phase
assertions to confirm the no-step witness is skipped while the subsequent move
still replays in cast order.
- Around line 83-85: Adopt the established snafu error pattern for the public
`WriteFailed` and `PersistError` types in the persist sink module. Add the
required snafu derives and one `#[snafu(display(...))]` message per error
variant, preserving each type’s existing payloads and semantics so both
implement `Display` and `std::error::Error` for external callers.
- Around line 336-341: The recover_and_apply flow must preserve information
about moves already applied when owner.try_advance_phase fails. Update
PersistError::Illegal or the recover_and_apply contract so callers receive the
applied prefix alongside the failure, or explicitly document that Illegal leaves
the owner mid-chain and exposes the partial progress; ensure earlier applied
moves are not silently discarded.
- Around line 155-160: Update the scan_witnesses trait seam to support bounded
replay by accepting an optional DurableCoordinate lower bound and returning only
witnesses after that coordinate, so recovery does not load the entire durable
log. Propagate this change through the recovery flow, and ensure multi-owner
recovery derives local trajectories once via temporal::local_trajectories rather
than repeatedly scanning and cloning the full witness slice through
local_trajectory_of.

In `@crates/lance-graph-planner/src/temporal.rs`:
- Around line 397-424: Update the documentation for LocalCausalRow::cast_seq to
state that cast_seq must be unique and monotonic within each owner; if duplicate
values occur, local_trajectories preserves their original global durable-log
order as the tie-break because the stable sort is used.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 789afb32-68d7-495c-8b71-98cbb38d35c9

📥 Commits

Reviewing files that changed from the base of the PR and between fa3ce5d and c7424cd.

📒 Files selected for processing (6)
  • .claude/board/EPIPHANIES.md
  • .claude/board/LATEST_STATE.md
  • .claude/board/STATUS_BOARD.md
  • crates/lance-graph-planner/src/lib.rs
  • crates/lance-graph-planner/src/persist_sink.rs
  • crates/lance-graph-planner/src/temporal.rs

Comment thread .claude/board/STATUS_BOARD.md Outdated
| D-MBX-A6-P3a | StyleStrategy: thinking-style -> cluster -> mechanism -> recipe_kernels Tactic selection (planning substrate; carries tau JIT addr) | lance-graph-planner | 130 | LOW | **In PR** | #439; first cut of A6-P3 consumer wiring; planner now consumes contract recipes/styles; deferred: i4-32D decode, Outcome->Candidate, tau->JIT, membrane commit |
| D-MBX-A6-P3b | output overhaul: `StrategyOutcome{reliability, intended_move: Option<KanbanMove>}` carrier on `PlanInput.outcome`; StyleStrategy retires the dead-store `_reliability`, SURFACES reliability + a bootstrap intended move (Planning→CognitiveWork, owner 0, warden-BOOTSTRAP-OK) — plan still pure | lance-graph-planner | 130 | LOW | **In progress** | additive Option field (6 in-crate literals); UNBLOCKED (no mint, not OQ-11.7); deferred: compose thread-out + contract-promote + owner-consume; E-STRATEGY-OUTCOME-CARRIER-1 |
| D-MBX-A6-P3c | owner-consume: `lance_graph_planner::owner_adapter` = the `Outcome → KanbanMove` bootstrap-rebind + ahead-cast adapter. `rebind_bootstrap` (mailbox 0/cycle 0 sentinel → live owner; refuses an already-owned move = no ownership theft) + `emit_bootstrap_intent` → `BatchWriter::cast(on_behalf = owner)`. Fire-and-forget (no ack/ledger/WAL/arbitration/callback); the move is the pre-write "parcel address", the lifecycle STEP stays post-write. Completes P3b's deferred `owner-consume`. | lance-graph-planner | 90 | LOW | **In PR** | 5 falsifiable probes (rebind 0→live anti-vacuity + no-theft + on-behalf cast + non-vacuous no-op silence); lance-free, builds without protoc. Persistence sink (drain→Lance 7 `mem_wal::WalAppender::append`) verified-but-gated (protoc missing + disk); knowledge doc `.claude/v3/knowledge/d-mbx-a6-owner-consume-and-persistence.md`; `E-KANBANMOVE-IS-THE-PARCEL-ADDRESS-STEP-IS-THE-DELIVERY-SCAN-1` |
| D-MBX-A6-P3d | persistence sink ORDERING CORE + durable-witness reshape + temporal layer-1 (the POST-write half). `lance_graph_planner::persist_sink`: two clock domains (async `persist_cast` no-owner-borrow → `DurableReceipt`; sync `apply_durable_step` no-await → `try_advance_phase`). Crash-durability: `DurableWitness{owner,cast_id,cycle,paired_move}` CO-LOCATED with the SoA payload in one generation via `DurableWrite::append(&witness,&payload)`; `scan_witnesses` replay seam; `recover_and_apply` replays the pending tail in cast order (idempotent, stale-skip); `StalePhase` from-guard on the sync path. `temporal::{LocalCausalRow, local_trajectories, local_trajectory_of}` = layer-1 CAUSAL deinterlacing (global interleaved log → per-owner local chain), composing with the existing layer-2 epistemic projection. Durability proof = `DurableCoordinate`, never `LanceVersion`. | lance-graph-planner | 130 | LOW | **In progress** | 5 operator falsifiers green (deinterlace+vacuity, cast-order replay, crash→reconstruct+apply, WAL-fail→no-step, WAL-before-manifest); 343 planner lib tests, clippy+fmt clean; builds NO concrete `LanceShardSink` (deferred per operator — gated on these falsifiers); `E-THE-PAIRED-MOVE-MUST-BE-DURABLE-CO-LOCATED-NOT-IN-MEMORY-ONLY-1` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Preserve append-only board history.

Line 966 inserts a new governance row between existing historical rows. Prepend new entries in newest-first order instead of inserting them into the existing history. If STATUS_BOARD.md is intended to be a snapshot table, document that exception in the repository guidance.

As per coding guidelines, governance entries in .claude/board/*.md must be append-only. Based on learnings, new board entries must be prepended without reordering existing history.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/board/STATUS_BOARD.md at line 966, Preserve the append-only history
in STATUS_BOARD.md by moving the new D-MBX-A6-P3d governance row to the
beginning of the board entries rather than inserting it between existing
historical rows. Keep all existing rows in their current order; only use an
in-place insertion if repository guidance explicitly documents this file as a
snapshot table.

Sources: Coding guidelines, Learnings

Comment thread crates/lance-graph-planner/src/persist_sink.rs Outdated
Comment thread crates/lance-graph-planner/src/persist_sink.rs Outdated
#878 review)

Three confirmed correctness fixes from Bugbot/Codex/CodeRabbit review, all in the
crash-recovery path the operator mandated:

- **cast_id resets across restarts (Bugbot High).** BatchWriter::next_id resets
  to 0 on restart, so witnesses from different writer lifetimes collide on
  cast_id — ordering replay by it is unstable. Replay now orders by the durable
  DurableCoordinate::log_order (WAL entry position, monotonic across restarts,
  never reset). scan_witnesses returns LandedWitness{coordinate, witness}, which
  is the LocalCausalRow implementor; cast_id survives as provenance only.

- **phase equality is unsound idempotence on a cyclic lifecycle (Codex +
  CodeRabbit Critical).** Planning→CognitiveWork→Evaluation→Plan→Planning is a
  real lap; after it the owner is back at Planning, so a phase-only "already
  applied" check replays the whole lap. recover_and_apply now takes a durable
  watermark (applied_through): skip ≤ watermark; above it a non-matching `from`
  is genuine corruption (StalePhase), not a benign skip. Returns
  Recovered{applied, watermark} to persist WITH the SoA phase. New falsifier
  proves cyclic idempotence WITH the watermark and, as a negative control,
  reproduces the double-lap WITHOUT it (watermark is load-bearing).

- **cross-owner move could become durable (CodeRabbit Major).** persist_cast now
  rejects paired_move.mailbox != owner before the append; apply_durable_step adds
  the same defense-in-depth check.

Also: scan_witnesses(from) is bounded (tail read); WriteFailed/PersistError impl
Display+Error (no snafu dep added); FakeSink is Sync (Mutex+Atomic) with a
multi-thread concurrent-drain test; a durable-no-step recovery case; the layer-1
cast_seq doc states the monotonic-across-restarts precondition. StalePhase on the
sync path documented safe-to-drop (the durable witness replays).

348 planner lib tests pass; clippy + fmt clean. No Cargo.lock change. Still builds
NO concrete LanceShardSink (operator-deferred). Board entries (EPIPHANIES
Correction, LATEST_STATE, STATUS_BOARD) updated to the corrected design.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCGhDYoQBXs3poaR7sFuqp
@AdaWorldAPI

Copy link
Copy Markdown
Owner Author

Review round addressed in 08fa9f3

Thanks to Cursor Bugbot, Codex, and CodeRabbit — the three converged on one real cluster (crash-recovery ordering/idempotence), and it was a genuine gap. Summary of what landed:

Fixed (correctness):

  • Replay order (Bugbot High — "witness replay ignores writer epoch"). BatchWriter::next_id resets to 0 on restart, so cast_id collides across writer lifetimes. Replay now orders by the durable DurableCoordinate::log_order (WAL entry position, monotonic across restarts, never reset). scan_witnesses returns LandedWitness{coordinate, witness}, which is the LocalCausalRow implementor; cast_id survives as provenance only. New falsifier recovery_orders_by_durable_position_not_the_resettable_cast_id (two lifetimes, colliding cast_id 0, distinct WAL positions).
  • Cyclic idempotence (Codex + CodeRabbit Critical). Confirmed: Planning→CognitiveWork→Evaluation→Plan→Planning is a real lap, and phase-equality replays it. recover_and_apply now takes a durable watermark (applied_through): skip ≤ watermark; above it a non-matching from is genuine corruption (StalePhase), not a benign skip. Returns Recovered{applied, watermark} to persist WITH the SoA phase. New falsifier cyclic_recovery_is_idempotent_only_with_the_durable_watermark proves idempotence with the watermark and, as a negative control, reproduces the double-lap without it (watermark is load-bearing).
  • Cross-owner move (CodeRabbit Major). persist_cast now rejects paired_move.mailbox != owner before the append; apply_durable_step has the same defense-in-depth check.

Addressed (nitpicks): scan_witnesses(from) bounded tail read; WriteFailed/PersistError now impl Display + Error; FakeSink made Sync with a multi-thread concurrent-drain test; durable-no-step recovery case added; layer-1 cast_seq doc states the monotonic-across-restarts precondition; recover_and_apply documents that the owner is left mid-chain on Illegal.

Handled by decision, not the literal suggestion:

  • snafu (CodeRabbit). snafu is not currently a dependency of lance-graph-planner; rather than add a workspace dep for two error types, I added manual Display + std::error::Error impls — same external-propagation ergonomics, no new dep.
  • Codex "order concurrent receipts before consuming them." A reordered/stale receipt dropped on the sync path loses nothing: the move is durably co-located, and recover_and_apply (or a re-drive re-reading the durable tail) replays it in durable order. Documented on apply_durable_step as safe-to-drop rather than adding sync-path buffering.
  • STATUS_BOARD append-only (CodeRabbit). STATUS_BOARD.md is a thematic snapshot dashboard (per the repo CLAUDE.md), not a chronological append-log like EPIPHANIES.md/PR_ARC_INVENTORY.md. The D-MBX-A6-* rows form a numbered sub-series (P1→P2→P3a→P3b→P3c→P3d→P3-M1); the new row sits in its sub-series position adjacent to P3c and reorders no existing row. Kept it grouped with its siblings; the append-only chronological logs (EPIPHANIES) were prepended/appended per convention.

348 planner lib tests pass; clippy + fmt clean; no Cargo.lock change. Still builds no concrete LanceShardSink (operator-deferred, gated on these falsifiers).


Generated by Claude Code

claude added 2 commits August 2, 2026 00:13
…t framing (PR #878 review 2)

ChatGPT review (taken with a grain of salt — it reviewed the pre-fix c7424cd;
findings 1/2/4 were already fixed in 08fa9f3). Three further points held up:

- **Reordered receipts must stay retryable, not dropped (Codex/ChatGPT).** A
  stale receipt dropped on the sync/happy path would only be re-applied on a
  crash+restart — recovery is NOT the backstop for ordinary concurrent-completion
  reordering. apply_durable_step now BORROWS &DurableReceipt; on StalePhase the
  caller keeps it and retries once the missing prefix lands. New falsifier
  a_reordered_receipt_is_retryable_not_lost (later receipt applied before its
  predecessor → neither lost, both fire in order, no crash needed). The earlier
  "safe to drop" doc claim was wrong and is corrected.

- **API-honest durable coordinate (ChatGPT/prior note).** ShardWriter::put returns
  a batch position, not WalAppendResult::entry_position, so the field
  wal_entry_position over-promised. Renamed to an opaque backend-defined `seq`
  (the sink maps whichever position its API yields); no field names a WAL offset
  the chosen API does not return.

- **Honest test/claims framing (ChatGPT; the Ladybug compile≠storage lesson).**
  The FakeSink tests are labelled ordering/recovery CONTRACT probes, not
  crash/WAL integration tests (no real MemWAL, restart, atomic RecordBatch
  co-location, or fencing). The fake now STORES the payload (was ignored) and a
  probe asserts it landed alongside the witness. Module doc + board wording
  downgraded from "crash-durable / high confidence" to "contract-probed; real
  durability unproven until the concrete sink".

Taken with salt, NOT actioned as blocking: the per-cast-vs-generation persistence
SEAM SHAPE (finding 5) is a real architectural fork but not a correctness bug
(the operator's own bb5fcdd used per-cast) — surfaced to the operator for
decision, not unilaterally reshaped.

349 planner lib tests pass; clippy + fmt clean; no Cargo.lock change. Still builds
NO concrete LanceShardSink.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCGhDYoQBXs3poaR7sFuqp
…er cast (PR #878)

The operator ruled the seam-shape fork surfaced in Correction 2(c) of
E-THE-PAIRED-MOVE-...-1: the durable unit is the cycle/sweep, not the cast.
64k concurrent thoughts stage into an owned Vec<SweepSlot>; persist_cycle
folds+freezes them; WalSink::commit_cycle does exactly ONE atomic append ->
one DatasetVersion. Modelling durability per-cast paid WAL header +
durability-wait + version-metadata 64k times; this amortizes it.

The layer's ONLY concerns are storage + no physical/epistemic race:
- Physical race: order_cycle_stably (generic over the caller's key) stable-
  orders casts by their EXISTING stream_position in DetachedCycleBatch::freeze
  BEFORE the single append. scan_sealed returns stored order and NEVER sorts.
  Write-side ordering lives here, NOT in temporal.rs (which owns query-time
  reading); the deinterlace_write additions made there this session are
  reverted (temporal.rs untouched).
- Epistemic race: sealed read horizon (read Vn / write Vn+1); an uncommitted
  cycle is invisible to scan_sealed.
- Coalescing: real per-row fold (row -> last payload in stream order), not
  last-chunk-wins.

No semantic/witness/ancestry/awareness/branch/rung/temporal-policy type is
minted in this layer. CycleFrame is storage-only {cycle, base_version};
SweepSlot is a boring landing (no basis). Vocabulary reused, not re-minted:
DatasetVersion, KanbanMove/KanbanColumn, MailboxId/MailboxSoaOwner,
stream_position. recover_and_apply + the durable watermark idempotence +
StalePhase/OwnerMismatch guards survive unchanged.

Retired: DurableWitness, DurableReceipt, DurableCoordinate, DurableWrite,
persist_cast, apply_durable_step, scan_witnesses, LandedWitness.

Tests: 13 storage/race CONTRACT probes over an in-process FakeWalSink,
honestly labelled (compile+test green != storage proven, the Ladybug
lesson). No concrete Lance sink built (deferred, gated on crash falsifiers).
lance-graph-planner: 344 lib tests green, clippy+fmt clean.

Board: EPIPHANIES E-THE-DURABLE-UNIT-IS-THE-CYCLE-...-1 + LATEST_STATE
D-MBX-A6-P3e (prepended, same commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCGhDYoQBXs3poaR7sFuqp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
crates/lance-graph-planner/src/persist_sink.rs (4)

828-891: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for the no-step watermark branch.

recover_and_apply advances the watermark for a landing whose paired_move is None (line 357). No test drives that branch through recover_and_apply. The None landings in this file appear only in the ordering and coalescing tests, which never call recovery.

The branch matters: if it did not advance, a no-step landing would be re-scanned forever and would block the watermark behind every following step.

Add a case with a mixed chain, for example a step at position 0, a no-step landing at position 1, and a step at position 2. Assert that rec.watermark == Some(2) and that a second pass with that watermark applies nothing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/lance-graph-planner/src/persist_sink.rs` around lines 828 - 891, Add a
recovery test alongside cyclic_recovery_is_idempotent_only_with_the_watermark
that persists a mixed chain containing a step at position 0, a no-step landing
at position 1, and a step at position 2, then calls recover_and_apply. Assert
the first result advances rec.watermark to Some(2), and a second
recover_and_apply call using that watermark applies nothing.

225-249: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider snafu for these error types, or at least implement source().

The repository guideline asks Rust crates to reuse snafu error patterns. WriteFailed and PersistError use hand-written Display and empty Error impls instead. The concrete cost is a broken error chain: PersistError::Write and PersistError::Illegal wrap inner errors, but Error::source returns None, so callers cannot walk to the cause.

If you keep the manual impls, implement source() for the wrapping variants.

♻️ Minimal alternative without adding `snafu`
-impl std::error::Error for PersistError {}
+impl std::error::Error for PersistError {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        match self {
+            Self::Write(e) => Some(e),
+            _ => None,
+        }
+    }
+}

As per coding guidelines: "reuse snafu error patterns".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/lance-graph-planner/src/persist_sink.rs` around lines 225 - 249,
Update PersistError’s std::error::Error implementation to expose wrapped causes
through source(), returning the underlying error for Write and Illegal and None
for OwnerMismatch and StalePhase. Preserve the existing Display messages and
avoid unrelated refactoring; apply the same error-chain treatment to WriteFailed
if it is defined nearby and manually implements Error.

Source: Coding guidelines


737-758: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The sealed-predecessor assertion is tautological.

Line 750 asserts f2.base_version == s1, but line 749 constructs f2 with s1 as that exact argument. The assertion restates the constructor input and exercises no sink behaviour. The test name claims the stronger property that a cycle cannot read an in-flight sibling.

FakeWalSink::commit_cycle ignores its base parameter entirely, so no test proves that a commit against a stale base_version is fenced. Make the fake compare base against the current sealed head and reject a mismatch, then assert that rejection here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/lance-graph-planner/src/persist_sink.rs` around lines 737 - 758,
Strengthen the test by updating FakeWalSink::commit_cycle to compare its base
argument with the current sealed head and reject mismatches, thereby fencing
stale or in-flight predecessors. In
a_cycle_reads_the_sealed_predecessor_not_an_in_flight_sibling, remove the
tautological f2.base_version assertion and assert that committing with a stale
sibling base is rejected, while preserving the successful sealed-predecessor
commit and resulting DatasetVersion(2) checks.

260-286: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the non-Send WalSink methods or make the returned futures Send.

WalSink is public and persist_cycle<WalSink> returns its async methods, but async trait methods still produce non-Send futures. Do not tokio::spawn calls outside this crate, or change commit_cycle, scan_sealed, and versions to return explicit impl Future<Output = ...> + Send futures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/lance-graph-planner/src/persist_sink.rs` around lines 260 - 286,
Update the public WalSink contract and persist_cycle usage to explicitly
document that commit_cycle, scan_sealed, and versions return non-Send futures;
do not introduce tokio::spawn calls outside the crate. Alternatively, if these
operations must be spawnable, change all three methods to return explicit Send
futures while preserving their existing result types and behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/board/EPIPHANIES.md:
- Around line 35-37: Preserve the original 2026-08-01 board entry unchanged,
except for permitted Status or Confidence updates. Move the correction text
currently embedded in that entry into a new dated entry at the top, keeping
.claude/board entries append-only and ordered newest-first.

In @.claude/board/LATEST_STATE.md:
- Line 4: Update the temporal.rs statement in LATEST_STATE.md to clarify that
only the write-side deinterlace_write additions were reverted, while the
existing local_trajectories and local_trajectory_of changes remain included. Do
not state that temporal.rs is entirely untouched.

In `@crates/lance-graph-planner/src/persist_sink.rs`:
- Around line 343-380: Update recover_and_apply to preserve and return the
accumulated Recovered value when a later landing fails: include the current
applied steps and watermark alongside errors from OwnerMismatch, StalePhase, and
try_advance_phase’s Illegal mapping. Ensure callers can persist the watermark
for the successfully processed prefix, and add a mid-chain failure test
asserting that the returned partial watermark covers that prefix.
- Around line 298-313: Add a dedicated permanent error variant for cast/frame
cycle mismatches alongside the existing OwnerMismatch variant, then update the
cycle check in the casts validation loop to return it instead of
PersistError::Write. Preserve the mismatch details in the new error so retry
logic can distinguish this caller error from retryable seal failures.
- Around line 122-143: Document that SweepSlot::stream_position is a globally
strictly monotonic per-owner ordering and recovery key across sealed cycles, and
clarify that Recovered::watermark uses the same cross-cycle scope. Add a
multi-cycle recovery test covering landings from later cycles, verifying
recovery does not skip positions at or below an earlier cycle’s watermark.

---

Nitpick comments:
In `@crates/lance-graph-planner/src/persist_sink.rs`:
- Around line 828-891: Add a recovery test alongside
cyclic_recovery_is_idempotent_only_with_the_watermark that persists a mixed
chain containing a step at position 0, a no-step landing at position 1, and a
step at position 2, then calls recover_and_apply. Assert the first result
advances rec.watermark to Some(2), and a second recover_and_apply call using
that watermark applies nothing.
- Around line 225-249: Update PersistError’s std::error::Error implementation to
expose wrapped causes through source(), returning the underlying error for Write
and Illegal and None for OwnerMismatch and StalePhase. Preserve the existing
Display messages and avoid unrelated refactoring; apply the same error-chain
treatment to WriteFailed if it is defined nearby and manually implements Error.
- Around line 737-758: Strengthen the test by updating FakeWalSink::commit_cycle
to compare its base argument with the current sealed head and reject mismatches,
thereby fencing stale or in-flight predecessors. In
a_cycle_reads_the_sealed_predecessor_not_an_in_flight_sibling, remove the
tautological f2.base_version assertion and assert that committing with a stale
sibling base is rejected, while preserving the successful sealed-predecessor
commit and resulting DatasetVersion(2) checks.
- Around line 260-286: Update the public WalSink contract and persist_cycle
usage to explicitly document that commit_cycle, scan_sealed, and versions return
non-Send futures; do not introduce tokio::spawn calls outside the crate.
Alternatively, if these operations must be spawnable, change all three methods
to return explicit Send futures while preserving their existing result types and
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 935b11c2-30cc-400c-83f7-de6b193b7461

📥 Commits

Reviewing files that changed from the base of the PR and between c7424cd and b6f904c.

📒 Files selected for processing (5)
  • .claude/board/EPIPHANIES.md
  • .claude/board/LATEST_STATE.md
  • .claude/board/STATUS_BOARD.md
  • crates/lance-graph-planner/src/persist_sink.rs
  • crates/lance-graph-planner/src/temporal.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • .claude/board/STATUS_BOARD.md
  • crates/lance-graph-planner/src/temporal.rs

Comment thread .claude/board/EPIPHANIES.md
Comment thread .claude/board/LATEST_STATE.md Outdated
Comment thread crates/lance-graph-planner/src/persist_sink.rs
Comment thread crates/lance-graph-planner/src/persist_sink.rs
Comment thread crates/lance-graph-planner/src/persist_sink.rs
…hape (PR #878)

Five valid findings + nitpicks, verified against the code and fixed:

- recover_and_apply now returns Result<Recovered, (Recovered, PersistError)>
  (Major): it mutates the owner as it walks the chain, so a mid-chain failure
  had already advanced the owner's phase for the applied prefix but dropped the
  accumulated Recovered on Err — the caller could not persist the watermark it
  earned, replaying applied moves against an advanced owner (permanent
  StalePhase stall acyclic / double-apply cyclic). Each error path now returns
  the partial Recovered. New falsifier a_mid_chain_failure_returns_the_applied_
  prefix_watermark.

- PersistError::CycleMismatch (Minor): a cast whose cycle != frame.cycle is a
  permanent caller error, not the RETRYABLE Write class (retry would loop). New
  variant + a_cast_for_the_wrong_cycle_is_a_permanent_cycle_mismatch_not_write.

- stream_position cross-cycle monotonicity (Major, doc gap): documented on
  SweepSlot::stream_position + Recovered::watermark that the key is monotonic
  per owner ACROSS cycles (recover_and_apply compares it over the multi-cycle
  scan_sealed stream). New falsifier recovery_watermark_spans_multiple_sealed_
  cycles.

- PersistError::source() (nitpick): wrapping variants (Write/Illegal) now
  forward to their inner error so callers can walk the chain.

- Sealed-predecessor test was tautological (nitpick): FakeWalSink::commit_cycle
  now fences on a stale base (optimistic-concurrency: base must equal the
  sealed head), and the test asserts a stale-base commit is REJECTED instead of
  restating the constructor input — the epistemic horizon is now proven, not
  assumed.

- No-step watermark branch (nitpick): new falsifier a_no_step_landing_advances_
  the_watermark_and_is_not_re_scanned (mixed step/no-step chain).

- WalSink futures documented as non-Send (nitpick).

- LATEST_STATE wording corrected: only the write-side deinterlace_write
  additions were reverted; temporal.rs's existing query-time causal helpers
  (LocalCausalRow/local_trajectories) landed in earlier PR commits and remain
  — the reshape commit leaves temporal.rs untouched, NOT the whole PR.

Not actioned (false positive): the "EPIPHANIES append-only" flag on lines
35-37 — those are the unchanged 2026-08-01 entry's Corrections; the new entry
is prepended at the top (lines 1-17). The diff mis-attributes because
prepending shifts line numbers.

lance-graph-planner: 348 lib tests green (+4), clippy + fmt clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCGhDYoQBXs3poaR7sFuqp
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants