Repository navigation
Stop out-of-order patches on audit:false tables from resurrecting deleted records or applying increments twice - #3506
Merged
Conversation
Contributor
There was a problem hiding this comment.
Code Review
This pull request updates the out-of-order patch handling on tables with auditing disabled (audit: false). It ensures that older patches do not resurrect deleted records or materialize records over newer publishes, correctly handles program-order ties when earlier writes restamped the head without staging, and tracks merged write identities in additionalAuditRefs to recognize re-delivered patches. Corresponding documentation in DESIGN.md and comprehensive unit tests in noAuditOutOfOrderPatch.test.js have been added. I have no feedback to provide as there are no review comments to assess.
kriszyp
force-pushed
the
fix/noaudit-patch-tie-null-dup-guards
branch
from
October 23, 2026 21:29
785f921 to
3b57fd0
Compare
kriszyp
force-pushed
the
fix/noaudit-patch-tie-null-dup-guards
branch
from
October 24, 2026 05:36
3b57fd0 to
65493d3
Compare
This was referenced Oct 24, 2026
kriszyp
force-pushed
the
fix/noaudit-patch-tie-null-dup-guards
branch
from
October 27, 2026 04:17
65493d3 to
88b4a76
Compare
kriszyp
force-pushed
the
fix/noaudit-patch-tie-null-dup-guards
branch
2 times, most recently
from
October 27, 2026 16:56
9759ff4 to
f8428ef
Compare
…eted records or re-applying increments On an audit:false table, a patch older than (or tied with) the stored record went through remainingAgainstRecord with no further checks, so: - a patch older than a delete tombstone or a publish to an absent key recreated the record (indexed, with its blobs kept); - a re-delivery at the head's own version re-applied its `add` ops; - a re-delivered out-of-order patch re-applied its ops, because the record's additionalAuditRefs were neither consulted nor carried forward. The branch now skips a tie, a head holding no record, and a write whose (txnTime, nodeId) the head already lists; a merge carries the head's refs forward and adds its own identity. resolveAuditHead ignores refs on an unaudited record, since they address no log entry. LMDB still stores a merge at the merging write's older version (#3388): its entry version is also the ifVersion token LMDBTransaction commits on, so keeping the head's version lets a write staged against the pre-merge entry overwrite the merge. A test pins that. Fixes #3385 Refs #3388 Dispatch-Task: harper-noaudit-patch-tie-null-dup-guards Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s by the received log key A publish or relocate restamps the head without staging it, so on RocksDB a patch that follows one in the same transaction ties the head (or meets a publish's null value) and the new guards dropped it. They now apply only when every earlier write to the key in the transaction was skipped, as the audited head-tie check does. The identity folded into an unaudited record is the received log key — the origin's log key for an apply, txnTime for a local write — so two applied writes sharing a record version stay distinct. resolveAuditHead no longer ignores refs on unaudited tables: a publish stores real log-key refs there. Dispatch-Task: harper-noaudit-patch-tie-null-dup-guards Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ogram order A publish or relocate restamps the head without staging it, so on RocksDB a later write to the key in the same transaction ties the head. On an audit:false table that tie sent a put to the "loses" branch and a patch through remainingAgainstRecord, dropping the put and every plain field of the patch. Such a tie now applies as an in-order write; the audited walk already ordered it by log key. Dispatch-Task: harper-noaudit-patch-tie-null-dup-guards Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ive op A re-delivered out-of-order patch can only double-apply its commutative ops; the plain keys it merged are held by the record afterwards and remainingAgainstRecord drops them. Receipts are walked as log keys once auditing is turned on (a subscribe does that), each a transaction-log miss of 0.3-1 ms on a 60k-entry log here, so they are kept to the merges they protect. Dispatch-Task: harper-noaudit-patch-tie-null-dup-guards Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dispatch-Task: harper-noaudit-patch-tie-null-dup-guards Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
arms still use it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Dispatch-Task: pr-maint-563ceb51d0967c6c19dbcab60690aaa4
…ocksDB Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Dispatch-Task: pr-maint-563ceb51d0967c6c19dbcab60690aaa4
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Dispatch-Task: pr-maint-563ceb51d0967c6c19dbcab60690aaa4
…log-key list Dispatch-Task: pr-maint-563ceb51d0967c6c19dbcab60690aaa4 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Dispatch-Task: pr-maint-563ceb51d0967c6c19dbcab60690aaa4 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…es the key after it A source-applied patch is deferred to commit, while a publish or relocate of the same key saves when it is added and restamps the head at the origin's id and version. The patch then tied that head with no earlier unskipped write, so the no-audit branch dropped it as a re-delivery (count 10 instead of 14). The program-order check now also applies the tie when the committed head (identityBase) does not tie; a re-delivered transaction still ties it and skips. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: pr-maint-563ceb51d0967c6c19dbcab60690aaa4
…t away from it The previous commit applied the tie in program order whenever the committed head did not tie, including a committed head that is strictly newer: an older replicated patch then overwrote its plain fields. Compare once against the committed head (identityBase) and carry the result: older head applies in program order, newer head takes the ordinary out-of-order merge, a tie skips. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: pr-maint-563ceb51d0967c6c19dbcab60690aaa4
…ocksDB A publish or relocate before a replicated patch is an unskipped predecessor, so the tie went straight to program order and an older source transaction overwrote a newer head's plain fields; a re-delivered [publish, patch] also re-applied its increment. On RocksDB the tie is now always decided against identityBase's committed head, wherever the publish sits. LMDB commits pass no committedBy, so identityBase cannot see past the restamp there and LMDB keeps the predecessor exception. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: pr-maint-563ceb51d0967c6c19dbcab60690aaa4
#3529 added the same predecessor walk under that name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: pr-maint-563ceb51d0967c6c19dbcab60690aaa4
kriszyp
force-pushed
the
fix/noaudit-patch-tie-null-dup-guards
branch
from
October 27, 2026 19:46
f8428ef to
a6212d0
Compare
kriszyp
marked this pull request as ready for review
October 28, 2026 14:51
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
⊙ Problem
On
audit: falsetables, a patch older than or tied with the stored record skipped the checks the audited path makes. A patch older than a delete or a publish brought the record back, indexed. A re-delivered patch applied itsaddagain. On RocksDB, aputorpatchafter apublishto the same key in one transaction was dropped.🤔 Is the requirement right as specified?
Mostly. The task also asked LMDB to keep the head's version after a merge (#3388). That version is also LMDB's optimistic-lock token, so keeping it loses concurrent writes.
💡 Solution
For an older or tied write on an
audit: falsetable:Fixes #3385. Refs #3388.
⚖️ Alternatives
Rejected alternatives
Math.max):LMDBTransaction.commitchecksifVersion(key, entry.version), so keeping the version lets a write staged before the merge commit over it.nullfromremainingAgainstRecordfor a missing record: the delete rule is Table policy (already inbelowAuditFloor), and the helper cannot detect duplicates.🤔 Should a same-origin tie with a different log key count as a duplicate?
Yes. An
audit: falsehead stores no log key, so a re-delivery and a distinct same-version write look identical.🤔 Should an older patch over a publish-only head be dropped?
Here it is: without a log, a publish-only head looks like delete-then-publish. The cost: a patch arriving first creates the record.
🤔 Should receipts get their own record field?
They reuse
receivedIdentityRefs, avoiding a format change; old receipts can age out of the bounded list.🤔 Should the out-of-order arm avoid its extra walk and allocation?
No: the walk is bounded by one transaction's same-key writes, and the allocation is on the rare merge path.
🔧 Changes
resources/Table.ts,_writeUpdate's out-of-order block: the committed-head ordering for a tie a same-transaction publish or relocate made, the in-order branch, thenreceivedLogKeyand recorded only when the merge folds in a commutative op.resources/DESIGN.md: receipts on unaudited records and their best-effort limits, why LMDB still rewinds, the skip inventory, the program-order rule.unitTests/resources/noAuditOutOfOrderPatch.test.js: 23 new cases on both engines (first new case).✅ Verification
noAuditOutOfOrderPatch.test.js: 29/29 on RocksDB, including an older patch after a delete, tie re-delivery, out-of-order re-delivery and a write staged before a merge. On LMDB 17 pass, 12 skipped.npm run test:unit:resources:core: 4533 passing on RocksDB.New test cases
addapplies once.Math.maxchange).🤖 Generated with Claude Code
🤖 Generated by Anthropic Claude (Opus 5.5); posted via @kriszyp.
Release target: v5.4 — extends #3411's audit:false branch, which is on v5.4 only
Related PRs: #3469 overlaps (adjacent full-update branch; expect a textual conflict), #3499 overlaps (merged; same-transaction publish/relocate tie, audited path; rebased onto it), #3411 overlaps (merged; this extends its audit:false branch), #3476 overlaps (merged; committed-head duplicate skip, audited path), #3510 overlaps (merged; minted local log keys and the received-identity list the receipts now use; rebased onto it), #3423 overlaps (merged; replication confirmation note in DESIGN.md; rebased onto it), #3576 overlaps (merged; distinct origins never tie, so the tie skip is same-origin only; rebased onto it), #3529 overlaps (merged; added
followsAppliedWrite, reused here; rebased onto it), 20 others independentComplexity: complicated
Framing-Verdict: chosen-approach-sound
Deferred findings
e88fb92adebeea8bminor data-integrity —resources/Table.ts— chained same-txn out-of-order plain-field patch dropped against prior staged record — NOT FILED367176ab4120fb41minor data-integrity —resources/Table.ts— in-order write on audit:false drops receivedIdentityRefs, so re-delivery of an earlier commutative merge double-applies — NOT FILEDe06173778072dd87minor data-integrity —unitTests/resources/noAuditOutOfOrderPatch.test.js— LMDB drops patch before same-txn publish; new tests skip LMDB — NOT FILED9cef1ac0381dadf8minor data-integrity —resources/Table.ts— an older replicated publish rewinds the head version below a newer committed write — NOT FILED3001c44f2fdb2aa6minor data-integrity —resources/Table.ts— re-delivered [publish K, patch K] re-applies the patch's commutative ops on LMDB — NOT FILEDde38970ada68f5aaminor data-integrity —unitTests/resources/noAuditOutOfOrderPatch.test.js— dedup and publish arms skipped on LMDB — NOT FILEDc2574982c64bc159minor data-integrity —resources/Table.ts— LMDB merge stores the older version; a later older-than-head write compares in-order — NOT FILEDOrigin — the dispatch brief this PR was written from
Fixes #3385 and #3388 plus two untracked siblings, all in the audit:false patch branch of Table._writeUpdate (resources/Table.ts ~5535-5547), which after #3411 is remainingAgainstRecord(...), skip if nothing remains, set write.superseded, with no tie, null-head or duplicate guard (the audited and belowAuditFloor branches have all three, e.g. ~5518-5528). remainingAgainstRecord returns the whole update for a null record and always keeps op keys, so: (D1, #3385) an old patch past a delete tombstone recreates an
id: undefinedrecord in primary and secondary indexes and leaks blobs (also via Table.publish null entries on trackDeletes:false tables); (D2) an exact-timestamp re-delivery double-applies add; (D3) an out-of-order duplicate re-applies because existingEntry.additionalAuditRefs is never consulted, and writeCommit (~5714) wipes the list; (D4, #3388, LMDB) writeCommit uses Math.max(txnTime, existing version) only on RocksDB (~5694), so LMDB rewinds the head version. Skip the write (write.skipped = true; return writeCommit(false)) when the txn ties the existing version, the existing record is null, or additionalAuditRefs already holds this (txnTime, nodeId); when a partial merge applies, carry additionalAuditRefs forward and add this write's ref as the belowAuditFloor branch does; use the Math.max version rule on LMDB too. Draft PR #3469 edits the adjacent audited block; expect a textual conflict.Acceptance
Dual-engine tests on an audit:false table: old patch after delete does not recreate the row (no index entry, no leaked blob); tie re-delivery of an add applies once; out-of-order duplicate add applies once; LMDB head version never decreases. Controls: in-order and genuine out-of-order patches still merge. Shown failing on origin/main.
Dispatch: task
harper-noaudit-patch-tie-null-dup-guards· queued by finding-triage/wave-39 · ran by claude/opus/xhigh · worker kzyp-xps-1Late Grok findings
11 surviving (highest major) — Grok reviewed 9759ff4
resources/Table.ts:5847— Source-appliedaudit: falsetransaction[patch K count+=4, message K](seed count 10 at v50, frame at v100): the message saves eagerly and restamps K to v100; the deferred patch reloads that entry, ties with it, and the tie exception checks only earlier writes (priorWritesSkipped), not later oresources/Table.ts:5862—existingRecord == nulltreats a publish-only head like a delete tombstone. In-order delivery (patch T50, then publish T100) keeps the record; out-of-order delivery leaves no record; the audited walk creates it. Peers end up with different data, and the test at `unitTests/resources/noAuditOutOfOrderesources/Table.ts:5861—precedesExisting === 0skips on record-version equality alone. A distinct same-origin write at version 100 / log key 110 against a head at version 100 / log key 100 loses itscount += 4resources/Table.ts:5995— In-order writes onaudit: falsecarry no receipts forward. So: T50 out-of-order add (receipt stored), then T150 in-order patch, then re-delivery of T50 applies the add a second time (count 15 → 18)resources/Table.ts:10319—priorWritesSkippedwalks the whole predecessor chain, and every skipped same-key write calls it again (resources/Table.ts:5847,resources/Table.ts:5864). Re-delivering N same-key writes costs N(N−1)/2 inspectionsresources/Table.ts:5880—commutativeOpsOfbuilds a full projection whose only use is a truthiness test, and the receipts are sliced before anyone knows a push will happen. Both are per-patch allocations on the out-of-orderaudit: falsebranchresources/Table.ts:8372— A re-delivered[publish K, patch K]transaction applies the patch's commutative ops again (deferred:noaudit-publish-then-patch-redelivery-double-apply)resources/Table.ts:5880— On LMDB, a chained same-transaction patch rebuilds receipts from the pre-transaction entry and drops the receipt the earlier patch staged. No double-apply follows today because the LMDB merge stores the record at txnTimeresources/Table.ts:5847— The emptyelse ifbranch only works becauseincrementalUpdateToApplystays unset; an explicit assignment would survive later editsresources/DESIGN.md:146— The receipt note does not say what happens to unaudited receipts once auditing is turned on (the audited check reads them only underidentityIsNotKey), or mention the encoder-budget limitunitTests/resources/noAuditOutOfOrderPatch.test.js:16— Comment nits, all in one: "as on main" at test line 16 is addressed to the reviewer, and the null-guard narration atresources/Table.ts:5858-5859restates the condition below it. TheapplyFromcomment at test line 132 is borderlineNot standing: 2 separate-issue. Grok ran 15m (15m from launch to report); the fold adjudication (
cursor-grok@9759ff40a9c6-9aee4a61, claude) took 2m.Late Grok findings
1 surviving (highest minor) — Grok reviewed e58cfa9
resources/Table.ts:5892— on anaudit: falsetable, the next in-order write throws away the receipt from a commutative out-of-order merge. A re-delivery of that merge then applies its ops a second time (count 10 → 13 → 16)Not standing: 1 separate-issue. Grok ran 12m (12m from launch to report); the fold adjudication (
cursor-grok@e58cfa93ee41-9901dfce, claude) took 1m.Review-Coverage: authored=claude; ran=cursor-composer,gemini,codex,cursor-grok; late=cursor-grok:adjudicated; adjudicated=domain; blocked=claude(fallback)(out-of-budget); declined=muse; rounds=20; full=7 @ a6212d0
Review-Attention: study ~15m (critical: Table.ts; decisions: noaudit-tie-is-duplicate, null-head-skip-includes-publish, receipts-on-record-best-effort, lmdb-arms-deferred) @ a6212d0