fix(ledger,guard): survive parallel reconciles and rewritten history - #1978
Conversation
Two defects that both surfaced during the Phase 0 handoff, both caused by tooling that assumes a single serialized writer. planRequestBatch threw when a cancellation's target had already been applied by another branch's reconciliation. That is a lost race, not corruption, but the throw wedged every consumer at once — check:docs-links, ledger write discipline and reconcile itself — and write discipline forbids deleting the queued request, so there was no legal way out. An already-applied target is now a recorded no-op. It warns loudly, naming the cancellation and its target and saying the correction did not take effect, because silence here would be worse than the crash. A genuinely unknown target still throws, as does cancelling a cancellation or cancelling twice. guardBaseForRange returned the branch's remote tip unconditionally. After a force-push that tip is abandoned history, so every request it carried reads as deleted and the ledger transaction guard can never pass however clean the rebuild is — which is why landing the Phase 0 branch needed SKIP_LEDGER_WRITE_GUARD=1. It now keeps the remote tip only when that tip is an ancestor of the pushed commit, and otherwise falls back to the merge base with origin/main. That is the base CI already uses: ci.yml passes the pull request's base sha as LEDGER_WRITE_BASE_SHA, so the hook and CI were asking different questions. changedFilesForRange follows the same rule, so a discarded commit's files are not reported as deletions introduced by the push. Both fixes are mutation-verified: reverting the cancel tolerance turns 3 ledger tests red and silencing its warning turns 1 red; reverting either half of the guard-push change turns the force-push case red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BKNFogaYfCQBvFVqFQnfRt
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in:48 minutes Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (9)
Comment |
This pull request has been ignored for the connected project Preview Branches by Supabase. |
BigSimmo
commented
Aug 15, 2026
Final Codex review snapshot Head:
The PR was not merged. Final merge is left to the user. |
BigSimmo
commented
Aug 15, 2026
Final review snapshot — PR #1978
The PR was not merged. Final merge is left to the user. |
Uh oh!
There was an error while loading. Please reload this page.
…) (#2044) * docs(db): add coordination handover for multi-chat remediation oversight Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L7umYGe6hbCfq3NmpZ4Gz4 * docs(db): re-baseline the remediation coordination board to main (#316) Carries the coordination handover onto the coordination-chat branch and corrects it against the repository record as of main f5b0932: the tracking anchor is #316 (not #312), Phase 0 is complete (#1938/#1939/#1951/#1978), Phase 1 is partial with 1.2 the only executable next step, Phase 3 is blocked on ten UNCLASSIFIED RPCs, and Phase 4/5 have incident-scope partial evidence. The originating "never executed" verdict is marked superseded by the Phase 1.1 fingerprint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(ledger): record review of the coordination board PR (#2044) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
Summary
Two defects in this repo's own tooling, both found the hard way while landing #1951, both caused by code that assumes a single serialized writer. Parallel reconciles are no longer rare — four ran on 2026-08-14 alone.
planRequestBatchno longer wedges on a cancellation that lost a race (scripts/ledger-inbox.mjs). A cancel whose target was already applied by another branch's reconciliation is now a recorded no-op instead of a throw.guardBaseForRangeno longer compares against abandoned history (scripts/guard-push.mjs). After a force-push it falls back to the merge base withorigin/main— the base CI already uses.Why the ledger one matters more than it looks
It is not a cosmetic error path.
planRequestBatchis reached bycheck-docs-links.mjs,check-ledger-write-discipline.mjsandledger-inbox.mjsreconcile, so a single dangling cancel red-linesdocs:check-links(inverify:cheap,verify:pr-localand CIstatic-pr), the write-discipline gate, and reconcile — simultaneously. And it could not be cleared by deleting the queued file, because write discipline forbids removing a request. That is a deadlock, and it happened:Static PR checkson #1951 failed withafter a parallel reconcile applied
bbf21714while #1951 was open.The new behaviour is deliberately loud, not lenient. An already-applied target warns by name and says the cancellation did not take effect and the row must be corrected with a fresh update — because the cancel's intent did fail, and silence would hide that. A genuinely unknown target still throws; so does cancelling a cancellation, or cancelling the same request twice; and an ineffective cancellation does not resolve a competing-mutation conflict.
Why the guard-push one matters
guardBaseForRangereturnedrange.remoteShaunconditionally. After a force-push that tip is a discarded line of history, so every request it carried reads as "removed without an audit record" and the ledger transaction guard can never pass — no matter how clean the rebuild is. That is exactly why landing #1951 requiredSKIP_LEDGER_WRITE_GUARD=1.The repo already disagreed with itself here:
.github/workflows/ci.ymlpasses the pull request's base sha asLEDGER_WRITE_BASE_SHA, so the local hook and CI were asking different questions of the same diff. The fix keeps the remote tip when it is an ancestor of the pushed commit (ordinary push, unchanged) and otherwise uses the merge base.changedFilesForRangefollows the same rule so a discarded commit's files are not reported as deletions this push introduced.This PR was pushed without any override — the first real exercise of the fix.
Verification
Both fixes are mutation-verified, because a passing test proves nothing on its own:
guardBaseForRangeto the unconditional remote tipchangedFilesForRangeancestry checknpm run verify:pr-localnpx vitest run tests/guard-push.test.ts tests/ledger-inbox-cancellation.test.ts tests/guard-push-no-merge-base.test.ts→Test Files 3 passed (3)/Tests 39 passed (39).node scripts/guard-push.mjs --self-test→[guard-push] self-test passed.Ledger write discipline passed for bf486de406ed..HEAD.anddocs link check passed: 1772 repo path references resolve.unknown[]vsobject[]) and was fixed before commit — noting it because the gate catching it is the point.Not run:
verify:ui(no UI change); provider-backed gates (nothing here touches a provider).RAG impact
Not required: no file under
src/lib/rag/**, nomatch_*RPC, no ranking, selection, eval-harness or fixture surface.check:rag:fixturesran green anyway.Risk and rollout
applied/, i.e. provably a lost race rather than a bad reference — and every other rejection path is unchanged and still covered by tests. The guard-push change only alters behaviour on a non-fast-forward push, which previously could not pass at all.git revertthe single commit; the two fixes are independent in effect, and reverting restores exactly the prior throw/base behaviour.Notes
mainand was removed from that branch before merge, somainis not currently broken — this is a recurrence fix, not an outage fix.check:medication-lexicon-reportinto CI (it currently runs inverify:pr-localbut no CI job —#333's open half), and correcting the remediation plan's stale drift figures.Generated by Claude Code