Repository navigation
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements record-based residency transitions (handoff of complete records) where the origin node retains the complete transition image until all named resident nodes have sent a receipt. It introduces new replication protocol commands (HANDOFF_RECEIPT and HANDOFF_RECEIPT_REQUEST), negotiates peer capabilities, and integrates handoff logic into the replication connection. Feedback on the changes includes: (1) converting the retained iterable to an array before iterating to prevent modification issues during iteration, (2) using a shallow copy instead of Object.create(retained) to ensure properties are correctly copied by downstream object spread operators, and (3) awaiting the Promise if getEntry returns one to avoid unnecessary delays in settling handoff receipts.
cb3acb5 to
5052a0f
Compare
…n reconciliation A second review round (the required framing recheck after this rebase forced a full round) traced the fix further: even with residencyId rebound, the redelivered image's own CONTENT is still the original transition's value (e.g. `home: B`), not the current row's. Delivering it to a peer under a claim of that peer's residency, ahead of any core contract for reconciling an older complete image against intervening writes, risks handing that peer stale data under a false claim of completeness -- worse than the existing, already-accepted pin-forever fallback for an unreachable resident. This is the same gap PR #940's decision ledger already flags for human judgment ("redelivery assumes core completes a late image under a newer stub"), not a new one this rebase task can resolve alone. Restored the original round-30 behavior for this path (verified byte-identical to cb3acb5) and replaced the two tests with one documenting the deliberate, known-pinned outcome. The receiptRequestKey collision and BigInt fixes from the same review round are independent of this and are kept. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…n reconciliation A second review round (the required framing recheck after this rebase forced a full round) traced the fix further: even with residencyId rebound, the redelivered image's own CONTENT is still the original transition's value (e.g. `home: B`), not the current row's. Delivering it to a peer under a claim of that peer's residency, ahead of any core contract for reconciling an older complete image against intervening writes, risks handing that peer stale data under a false claim of completeness -- worse than the existing, already-accepted pin-forever fallback for an unreachable resident. This is the same gap PR #940's decision ledger already flags for human judgment ("redelivery assumes core completes a late image under a newer stub"), not a new one this rebase task can resolve alone. Restored the original round-30 behavior for this path (verified byte-identical to cb3acb5) and replaced the two tests with one documenting the deliberate, known-pinned outcome. The receiptRequestKey collision and BigInt fixes from the same review round are independent of this and are kept. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
09cfced to
e8c1cf9
Compare
Release cherry-pick
|
…n reconciliation A second review round (the required framing recheck after this rebase forced a full round) traced the fix further: even with residencyId rebound, the redelivered image's own CONTENT is still the original transition's value (e.g. `home: B`), not the current row's. Delivering it to a peer under a claim of that peer's residency, ahead of any core contract for reconciling an older complete image against intervening writes, risks handing that peer stale data under a false claim of completeness -- worse than the existing, already-accepted pin-forever fallback for an unreachable resident. This is the same gap PR #940's decision ledger already flags for human judgment ("redelivery assumes core completes a late image under a newer stub"), not a new one this rebase task can resolve alone. Restored the original round-30 behavior for this path (verified byte-identical to cb3acb5) and replaced the two tests with one documenting the deliberate, known-pinned outcome. The receiptRequestKey collision and BigInt fixes from the same review round are independent of this and are kept. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
e8c1cf9 to
c5ba6a2
Compare
…n reconciliation A second review round (the required framing recheck after this rebase forced a full round) traced the fix further: even with residencyId rebound, the redelivered image's own CONTENT is still the original transition's value (e.g. `home: B`), not the current row's. Delivering it to a peer under a claim of that peer's residency, ahead of any core contract for reconciling an older complete image against intervening writes, risks handing that peer stale data under a false claim of completeness -- worse than the existing, already-accepted pin-forever fallback for an unreachable resident. This is the same gap PR #940's decision ledger already flags for human judgment ("redelivery assumes core completes a late image under a newer stub"), not a new one this rebase task can resolve alone. Restored the original round-30 behavior for this path (verified byte-identical to cb3acb5) and replaced the two tests with one documenting the deliberate, known-pinned outcome. The receiptRequestKey collision and BigInt fixes from the same review round are independent of this and are kept. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…n reconciliation A second review round (the required framing recheck after this rebase forced a full round) traced the fix further: even with residencyId rebound, the redelivered image's own CONTENT is still the original transition's value (e.g. `home: B`), not the current row's. Delivering it to a peer under a claim of that peer's residency, ahead of any core contract for reconciling an older complete image against intervening writes, risks handing that peer stale data under a false claim of completeness -- worse than the existing, already-accepted pin-forever fallback for an unreachable resident. This is the same gap PR #940's decision ledger already flags for human judgment ("redelivery assumes core completes a late image under a newer stub"), not a new one this rebase task can resolve alone. Restored the original round-30 behavior for this path (verified byte-identical to cb3acb5) and replaced the two tests with one documenting the deliberate, known-pinned outcome. The receiptRequestKey collision and BigInt fixes from the same review round are independent of this and are kept. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
c5ba6a2 to
63dbb6c
Compare
…n reconciliation A second review round (the required framing recheck after this rebase forced a full round) traced the fix further: even with residencyId rebound, the redelivered image's own CONTENT is still the original transition's value (e.g. `home: B`), not the current row's. Delivering it to a peer under a claim of that peer's residency, ahead of any core contract for reconciling an older complete image against intervening writes, risks handing that peer stale data under a false claim of completeness -- worse than the existing, already-accepted pin-forever fallback for an unreachable resident. This is the same gap PR #940's decision ledger already flags for human judgment ("redelivery assumes core completes a late image under a newer stub"), not a new one this rebase task can resolve alone. Restored the original round-30 behavior for this path (verified byte-identical to cb3acb5) and replaced the two tests with one documenting the deliberate, known-pinned outcome. The receiptRequestKey collision and BigInt fixes from the same review round are independent of this and are kept. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…n reconciliation A second review round (the required framing recheck after this rebase forced a full round) traced the fix further: even with residencyId rebound, the redelivered image's own CONTENT is still the original transition's value (e.g. `home: B`), not the current row's. Delivering it to a peer under a claim of that peer's residency, ahead of any core contract for reconciling an older complete image against intervening writes, risks handing that peer stale data under a false claim of completeness -- worse than the existing, already-accepted pin-forever fallback for an unreachable resident. This is the same gap PR #940's decision ledger already flags for human judgment ("redelivery assumes core completes a late image under a newer stub"), not a new one this rebase task can resolve alone. Restored the original round-30 behavior for this path (verified byte-identical to cb3acb5) and replaced the two tests with one documenting the deliberate, known-pinned outcome. The receiptRequestKey collision and BigInt fixes from the same review round are independent of this and are kept. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…n reconciliation A second review round (the required framing recheck after this rebase forced a full round) traced the fix further: even with residencyId rebound, the redelivered image's own CONTENT is still the original transition's value (e.g. `home: B`), not the current row's. Delivering it to a peer under a claim of that peer's residency, ahead of any core contract for reconciling an older complete image against intervening writes, risks handing that peer stale data under a false claim of completeness -- worse than the existing, already-accepted pin-forever fallback for an unreachable resident. This is the same gap PR #940's decision ledger already flags for human judgment ("redelivery assumes core completes a late image under a newer stub"), not a new one this rebase task can resolve alone. Restored the original round-30 behavior for this path (verified byte-identical to cb3acb5) and replaced the two tests with one documenting the deliberate, known-pinned outcome. The receiptRequestKey collision and BigInt fixes from the same review round are independent of this and are kept. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…n reconciliation A second review round (the required framing recheck after this rebase forced a full round) traced the fix further: even with residencyId rebound, the redelivered image's own CONTENT is still the original transition's value (e.g. `home: B`), not the current row's. Delivering it to a peer under a claim of that peer's residency, ahead of any core contract for reconciling an older complete image against intervening writes, risks handing that peer stale data under a false claim of completeness -- worse than the existing, already-accepted pin-forever fallback for an unreachable resident. This is the same gap PR #940's decision ledger already flags for human judgment ("redelivery assumes core completes a late image under a newer stub"), not a new one this rebase task can resolve alone. Restored the original round-30 behavior for this path (verified byte-identical to cb3acb5) and replaced the two tests with one documenting the deliberate, known-pinned outcome. The receiptRequestKey collision and BigInt fixes from the same review round are independent of this and are kept. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
8098359 to
9063963
Compare
…n reconciliation A second review round (the required framing recheck after this rebase forced a full round) traced the fix further: even with residencyId rebound, the redelivered image's own CONTENT is still the original transition's value (e.g. `home: B`), not the current row's. Delivering it to a peer under a claim of that peer's residency, ahead of any core contract for reconciling an older complete image against intervening writes, risks handing that peer stale data under a false claim of completeness -- worse than the existing, already-accepted pin-forever fallback for an unreachable resident. This is the same gap PR #940's decision ledger already flags for human judgment ("redelivery assumes core completes a late image under a newer stub"), not a new one this rebase task can resolve alone. Restored the original round-30 behavior for this path (verified byte-identical to cb3acb5) and replaced the two tests with one documenting the deliberate, known-pinned outcome. The receiptRequestKey collision and BigInt fixes from the same review round are independent of this and are kept. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…p element/length caps The prior element-count (32) and string-length (4096) bounds were both wrong in different directions: they could reject a compound id core would happily store, and a 32-element id of near-max strings still encodes to far more than a legitimate key, while a bare BigInt had no bound at all. Measuring the actual ordered-binary encoded length against LMDB's real 1978-byte limit -- the same technique and constant core itself uses (Table.ts's checkValidId, security/user.ts's keyTooLargeForStore) -- means an id this rejects is one core would reject too, and one it accepts is one core would actually store. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ound comment This filter is stricter than checkValidId in a few pathological corners (e.g. it rejects a top-level Infinity, which checkValidId's NaN-only number check would accept). That only pins an already-vanishingly-unlikely id shape instead of receipting it -- the same safe direction as this file's other pin-over-release choices -- but the comment shouldn't claim exact equivalence it doesn't have. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…raw array element ordered-binary flattens nested arrays at every depth with the same separator it uses between an outer key's own elements, so a compound (array) recordId embedded raw in [marker, tableId, recordId, peerName] could flatten into the exact byte sequence of a different, shorter key -- misattributing one record's receipt to another and releasing or pinning the wrong image. writeKeyId(recordId) already closes this for the in-memory request key (receiptRequestKey); apply the same fix to the durable dbisDB receipt key. The unit fake dbisDB now encodes through real ordered-binary bytes (toBufferKey) instead of JSON, so a regression here is caught at this layer instead of only in production. Also await fixture-residency-handoff's probe read: getEntry can return a Promise on a RocksDB cache miss (the same shape residencyHandoff.ts's resolveLocalEntry already guards against), and an unawaited Promise reads as a present, non-invalidated row with a null version/value -- exactly the "rebuilt resident" scenario these tests exist to check. Drop two exports (pendingTransitionEntries, releaseTransitionEntry) nothing outside the module calls. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…held under 64 bytes ordered-binary's string writer escapes control bytes, including 0x00 (the array separator), only below a 64-character short-string threshold; at or past that length it writes the UTF-8 bytes through raw (index.js's long-string path). writeKeyId's output is itself ordered-binary bytes reinterpreted as a latin1 string, so a sufficiently long or non-ASCII-heavy record id still put an unescaped 0x00 into the receipt key, reopening the exact flattening the previous commit meant to close -- codex's review reproduced it by execution. Hex has no byte that needs escaping at any length, closing it for every id. Tradeoff: an already-near-the-limit id's key now roughly doubles, so it can exceed the store's key-size limit where it previously fit; the existing dbisDB.put catch in the caller already treats that like any other failed receipt (image stays retained, next receipt or resweep retries) -- pinning, not misattribution, the same direction as every other size/shape rejection in this file. Extends the regression test with a long (>64-byte) compound id alongside the short one, and corrects the prior commit's comment, which claimed writeKeyId alone closed this off. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…able's resweep The hex-key fix means an already-maximum-length id's receipt key can exceed the store's key-size limit, and that throw came out of transitionsOwedToPeer's loop uncaught -- the call site only wraps the whole function, so one such entry aborted every other retained entry's processing for that table on every sweep pass, not just its own (reproduced by codex this round by tracing the real end-bound encoder). Wrap the per-entry residency/ receipts read and the two release paths individually: a failure here now skips just that entry (logged, image left exactly where it was) and the loop continues. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ease itself failing
releaseAndClearReceipts releases core's retained entry, then clears that record's receipt
rows. The same oversized-key condition the hex-key fix can hit applies to the cleanup read
too, and letting it propagate made a successful release look identical, to the caller's
error callback, to a release that never happened ("left pinned") -- found by this round's
review. The cleanup step is best-effort: a failure there only leaves harmless, never-read-
again receipt rows behind, since the retained entry is already gone. Swallow it there instead
of surfacing it as a release failure.
Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ught Item 24's "a core without them makes every path below behave as before" is wrong: copyRowDisposition/fetchDisposition withhold an INVALIDATED stub from a resident peer unconditionally (imageMatchesRow is false with no image), independent of core support. The key-layout example still showed a raw recordId instead of hexIdKey(recordId), and "receipts are applied sequentially per connection" was superseded by the bounded-concurrency fix (RECEIPT_APPLY_CONCURRENCY). Item 25's approximate line-number citations had drifted across this branch's many rebases; dropped them to match the rest of the doc's symbol-based convention instead of re-pinning numbers that will drift again. Also reformatted the merged command-constant and cheat-sheet tables with prettier (whitespace only, from the manual rebase-conflict merge). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66
… not just before it settleHandoffReceipts guarded against running while inCopyMode by checking once, synchronously, before calling settleReceiptRequests. But that call awaits a local-row read and a per-blob durability check, and a COPY_START arriving during those awaits flips inCopyMode without the already-in-flight closure seeing it -- so a request this settle answered from a row that becomes a WAL-off copy-applied row mid-read could still be certified and sent. A crash on the receiver after that would lose the record: the origin releases its only image on the strength of a receipt for a row that was never durable. Re-check inCopyMode/outstandingBlobsToFinish/hasBlobGap when the read resolves. If the state changed, defer every settled key -- answered and expired alike -- back to deferredReceiptKeys instead of certifying or clearing them; the existing copy-finish and blob-drain settles already drain that set. Deferring the whole batch (not just the answered subset) is simpler than tracking which keys were answered, and costs only a one-cycle delay on expired-request cleanup in the rare raced case -- no different from the entry guard already postponing all settlement whenever copy/blob state blocks it at the start. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66
…rame, not after sendQueuedData() flushes queued receipt requests before ws.send()'ing the frame that carries the image (the receiver tracks a frame's records for settlement only while a request for them is already waiting -- item 24's Redelivery bullet already said this correctly). The command-table row still said "after putting a complete transition image on the wire", flagged repeatedly by review as stale. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66
…rebase-collided items 24/25 to 26/27 transitionsOwedToPeer's await on a row/receipt read resolves as a microtask on both storage engines' common (synchronous) path, so the loop never actually yielded to the event loop; a backlog sized to a long-down resident (hundreds of thousands of retained images) could hold a worker thread for seconds per sending connection, every HANDOFF_RESWEEP_INTERVAL_MS. One macrotask yield every SWEEP_YIELD_EVERY (64) entries keeps other connections' I/O interleaved. This generation's rebase resolved the recurring DESIGN.md cheat-sheet conflict, but left this branch's own items 24/25 colliding with two new items main independently added at the same numbers (harper-pro#984/#986). Renumbered this branch's items to 26/27 and fixed the one internal cross-reference; main's own 24/25 are left as main defined them. Found by this generation's rebase-forced full pre-push review (domain adjudication: original, in-scope major; Cursor Muse: doc-numbering concern). Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…er timestamps decodeHandoffReceipts accepted any finite positive number (e.g. 2.5), but core's version field is always an integer timestamp, so a non-integer can only be a malformed or forged peer send. Tightened to Number.isSafeInteger, matching the adjacent tableId check. Found by this generation's rebase-forced full pre-push review (Cursor Muse, low-severity suggestion). Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…re integer timestamps" This reverts commit 8be80b5. Wrong: core/resources/recordLock.ts:217 (holderVersionCandidate) mints a fractional LWW tie-break version (`MIN_STEP = 0.000488`) for a cluster-locked write landed by harper-pro#987's round-robin record-lock placement (one of the commits this generation's rebase pulled in). A Number.isSafeInteger check would silently reject a legitimate receipt for any such record, pinning its image forever. Caught by this generation's next review round before push. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…frame, not after Item 26's _Proof._ bullet said "After that put the sender sends HANDOFF_RECEIPT_REQUEST...", but sendQueuedData flushes the request before ws.send (so the receiver never has to track a request for a record it has not yet seen named in a frame) -- the same fact the command-152 cheat-sheet row and the _Redelivery._ bullet already stated correctly. Caught by this generation's second review round as a stale claim surviving the renumbering fix. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ed record's image, withhold an invalidated stub on unresolved residency, and renumber DESIGN.md's rebase-collided item 26 Forced-full review round (this rebase broke the review CLI's ancestor tracking) found four new, real issues beyond this PR's long-standing decision-ledger items: - `flushReceiptRequests`/`settleHandoffReceipts` encoded HANDOFF_RECEIPT(_REQUEST) frames with msgpackr's plain `encode`, which throws RangeError for a BigInt id outside the signed/unsigned 64-bit range — a legal id under isValidReceiptId's own contract. The throw closed the connection before the paired image frame ever sent, and reconnect replays the same record. Fixed with a dedicated `encodeHandoffMessage` (Packr with useBigIntExtension, useRecords: false — `useRecords`'s default initializes `this.structures`, and `.pack` reads `this.lastNamedStructuresLength` directly, which breaks a detached reference like this one unless that path is compiled out, the same reason the module's own default `encode` disables it). The 0x42 extension tag decodes unconditionally on the receiving end's plain `decode()`. - `transitionsOwedToPeer` treated a deleted record (no row, no tombstone — retention expired) the same as a transient read error: both resolve to `row === undefined`, and neither released nor superseded the entry, so it stayed "owed" and kept redelivering forever. Against a legacy peer whose own tombstone has also expired, that resurrects a deleted record. Fixed by distinguishing a confirmed miss (resolveLocalEntry's callback never fired) from a read error (it fired) and treating only the confirmed miss as superseded. - The base copy's `peerIsResident` check collapsed an unresolved residency into "not a resident," and unlike the ordinary live-send gate's equivalent fallback (which forwards the original entry unchanged), the base-copy path always builds a fresh synthetic 'put' frame — so an unresolved residency shipped an invalidated stub as that plain put. Fixed by withholding until residency resolves, same as a resident with no matching image. - Main's per-origin-cursor note and this branch's own pair both landed on DESIGN.md item 26 during this generation's rebase (the third time this exact numbering collision has recurred across this PR's rebases). Renumbered this branch's pair to 28/29 and fixed the one internal cross-reference. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…o a deleted record already fully receipted still releases Delta review round (this PR's own prior commit) found a self-inflicted regression in the confirmed-miss fix: the new check ran before handoffReleasable, so a record deleted right after every other resident had already receipted would be marked superseded and never released -- a permanent pin the ordinary release path was already built to avoid. Moved the check after handoffReleasable's own release attempt, matching the existing residency-move superseded check's position. Added a test for the now-covered case. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ackr's internals Review nit: the comment narrated how Packr's detached .pack reads this internally instead of stating the one fact a reader needs (useRecords: false is required, matching msgpackr's own default encode). Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…s GET_RECORD's sibling check Review finding: HANDOFF_RECEIPT_REQUEST resolved a table purely from tableDecoders (populated by the handshake's TABLE_FIXED_STRUCTURE, independent of replicate:false) and never checked tableReplicates before registering the request and answering from local row state -- letting a peer confirm a replicate:false record's existence and version through the receipt path, the exact boundary GET_RECORD already enforces at its own sibling check (replicationConnection.ts:5777). Added the same two-part check (`!liveTable || !tableReplicates(liveTable)`, in that order: a dropped table has no live entry, and tableReplicates defaults an absent table to "replicates"). Verified the new test fails without the fix (reverted it locally, confirmed the assertion catches the leak, then restored it) before committing. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ale line citation/wrong variable name in the comment, and a blob-backed row making the deny test's timing non-deterministic The comment named a GET_RECORD gate line number that had already drifted (:5777 vs its actual :5783) and a variable (liveTable) that isn't the one this check declares (liveRequestTable) -- replaced the line citation with a by-name description so it can't drift again. The deny test reused the suite's blob-backed 'local' row; without the fix, answering it would also need the async blob-completeness check, which the test's wait budget doesn't account for -- a too-short wait could pass even with the gate missing. Added a non-blob 'local-small' row for this test instead. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ter rebase Rebasing this generation's conflict resolutions left two lines over the column width (a reindented withheldOriginNodeId check and the getTransitionImage property descriptor) and a cheat-sheet table separator one column short. Whitespace only. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…eft misaligned Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…to main's new code exposed Rebasing onto main brought in two independent changes that combined to break tests without any textual merge conflict: - main's table-lifecycle feature (harper#1212) refactored tableDecoders[tableId].getEntry to read this.table instead of closing over a local, so a later rejudge can rebind it. This branch's own receiptRequests.set(...) copies requestDecoder.getEntry as a bare property, which drops that this binding; called later via resolveLocalEntry, it threw "Cannot read properties of undefined (reading 'table')", silently caught and treated as "no entry", so a HANDOFF_RECEIPT_REQUEST for an ordinary replicated table got no answer. Fixed by binding to requestDecoder at copy time. - main's own new sendLoopYield.test.mjs extracts skipAuditRecord's source by string offset between two markers that used to be adjacent; this branch's isHandoffRedelivery skip branch now sits between them, so the extracted chunk included a bare top-level `return` and failed to parse. Narrowed the end marker to stop before that branch. Both confirmed as rebase-introduced (not present pre-rebase) by running the affected tests against the pre-rebase head in a scratch worktree before diagnosing further. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…EST handler
The pre-push review's domain adjudication caught a real gap in the prior commit's getEntry.bind
fix: a refused decoder (TABLE_FIXED_STRUCTURE's dropped-peer-generation branch) is registered as
{ name, refused: true, decoder } with no getEntry at all. If a live table of the same name exists
on this node, the tableReplicates gate doesn't filter it out, and binding undefined throws,
closing the connection on every such request. GET_RECORD already guards this via
peerGenerationRefused; this handler now does too.
Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
180c904 to
d3cebbe
Compare
…n reconciliation A second review round (the required framing recheck after this rebase forced a full round) traced the fix further: even with residencyId rebound, the redelivered image's own CONTENT is still the original transition's value (e.g. `home: B`), not the current row's. Delivering it to a peer under a claim of that peer's residency, ahead of any core contract for reconciling an older complete image against intervening writes, risks handing that peer stale data under a false claim of completeness -- worse than the existing, already-accepted pin-forever fallback for an unreachable resident. This is the same gap PR #940's decision ledger already flags for human judgment ("redelivery assumes core completes a late image under a newer stub"), not a new one this rebase task can resolve alone. Restored the original round-30 behavior for this path (verified byte-identical to cb3acb5) and replaced the two tests with one documenting the deliberate, known-pinned outcome. The receiptRequestKey collision and BigInt fixes from the same review round are independent of this and are kept. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ipts and release harper#3128 keeps the writer in its own record-based residency list, so no write leaves the record's last complete copy only in a transition image, and the image retention, HANDOFF_RECEIPT_REQUEST/HANDOFF_RECEIPT exchange, release-on-receipt, redelivery sweep and residencyHandoffReceipt capability have no job. Removed with residencyHandoff.ts and its unit and cluster tests. Kept, each with a test that fails without it: - the base copy sends an INVALIDATED stub only to a peer its residency excludes (where it becomes an invalidate), never to a resident or on an unresolved residency - GET_RECORD answers a stub as a miss instead of with its bytes - isDurableIdentityTie is not a tie when a complete put arrives over a stub The stub-guard cluster test now builds its stub on a non-resident peer instead of relying on the writer writing itself out, which harper#3128 removes. Dispatch-Task: hp-940-trim-stub-guards Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GfqTiSSdWUyQM9UFyPMap9
…r a stub anyway precedesExistingVersion returns a tie for the same version and origin, and both the copy apply and the live apply discard a tie, so letting a complete put past Pro's fast skip over a local stub changed nothing end to end. It only had an effect alongside the handoff's companion core change, which harper#3128 replaced. Also record the base-copy withhold tradeoff in DESIGN.md and trim the fixture's getEntry comment. Dispatch-Task: hp-940-trim-stub-guards Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GfqTiSSdWUyQM9UFyPMap9
A resident rebuilt from a node that holds only stubs of some records finishes the copy without them; the summary line, next to the withheld-origin one, makes that omission visible. Dispatch-Task: hp-940-trim-stub-guards Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GfqTiSSdWUyQM9UFyPMap9
…n reconciliation A second review round (the required framing recheck after this rebase forced a full round) traced the fix further: even with residencyId rebound, the redelivered image's own CONTENT is still the original transition's value (e.g. `home: B`), not the current row's. Delivering it to a peer under a claim of that peer's residency, ahead of any core contract for reconciling an older complete image against intervening writes, risks handing that peer stale data under a false claim of completeness -- worse than the existing, already-accepted pin-forever fallback for an unreachable resident. This is the same gap PR #940's decision ledger already flags for human judgment ("redelivery assumes core completes a late image under a newer stub"), not a new one this rebase task can resolve alone. Restored the original round-30 behavior for this path (verified byte-identical to cb3acb5) and replaced the two tests with one documenting the deliberate, known-pinned outcome. The receiptRequestKey collision and BigInt fixes from the same review round are independent of this and are kept. Dispatch-Task: pr-maint-0e208afb66ea625b57ed77ba834daf66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
⊙ Problem
A node outside a record's residency holds an INVALIDATED index-only stub: the indexed fields only, with the
INVALIDATEDmetadata flag. Two replication paths sent that stub as if it were the whole record, so a peer could store{ home: "B" }as the complete row:putand strips the low metadata byte.sendAuditRecordrewrites a row to aninvalidateonly for a peer that the record's residency excludes. A resident peer, or any peer when the residency id does not resolve, received the stub as a complete record. Measured with the cluster test below and the guard removed: the rebuilt resident held{"home":"127.0.0.141"}withinvalidated: false.GET_RECORD. The handler answered with the stub's bytes, and the requester stores and serves whatever it receives as the record.This PR used to carry a residency-handoff design. The writer kept a complete transition image, a
HANDOFF_RECEIPT_REQUEST/HANDOFF_RECEIPTexchange confirmed that the new resident held it, and the image was released on receipt. harper#3128 replaced that design: the writer now stays in its own record-based residency list, so the handoff code has no job. Removed:residencyHandoff.ts, message codes 151/152, theresidencyHandoffReceiptcapability, the redelivery sweep, thereplicate: falsegate on the receipt-request handler, and their unit and cluster tests. The PR went from +2435/−102 across 14 files to +345/−21 across 7.💡 Solution
sendAuditRecordturns it into aninvalidateas before. A resident, or any peer when the residency id does not resolve, gets nothing for that row. Look hardest here: the unresolved case includes every invalidated row that has no residency id, such as a caching table'sinvalidate(). Before this PR those rows were copied as completeputs holding only their indexed fields; now they are not copied. The copy logs how many stubs it withheld from the peer, so a resident finishing a copy without those records is visible in the log.GET_RECORD: a stub answers as a miss, the same response as no row.replication/DESIGN.mdnote 29 records the rule, the withhold tradeoff and the tests.No wire, capability or core change, and no
corepointer move.⚖️ Alternatives
invalidatein the base copy instead of withholding it. If a receiver already holds an older complete row, withholding leaves that row in place: stale, but whole. Aninvalidatewould replace it. Not done here because it changes how the receiver applies copy frames (a non-putcopy frame takes the audited apply path, not the WAL-off snapshot path), which is more than a trim should add. Before this PR, the same receiver got the stub as a complete row at the newer version, which is worse.isDurableIdentityTiestub check from the earlier design. Removed. Core'sprecedesExistingVersionreturns a tie for the same version and origin, and both the copy apply and the live apply discard a tie. So letting a completeputpast Pro's fast skip over a stub changed nothing end to end. The check only had an effect alongside the handoff's companion core change, which harper#3128 replaced.✅ Verification
For each guard, its test passes with the guard in place and fails with the guard removed (
distrebuilt between the two runs):residencyStubGuard.test.mjs: 2-node cluster; B writes a record homed on B, A holds the stub, B is rebuilt from nothing{"home":…}as a complete rowGET_RECORDresidencyStubFetch.test.mjs: the real handler over a fake socketunitTests/replication/**: 1250 passing, 0 failing (run before the identity-tie check was removed; that check's own test was removed with it).npm run buildis clean, andoxlintreports no new warnings on the changed files.Refs HarperFast/harper#2257
— Claude Opus 5.5
🤖 Generated with Claude Code
https://claude.ai/code/session_01GfqTiSSdWUyQM9UFyPMap9
Related PRs: #815 overlaps, #995 overlaps, 19 others independent
Review-Coverage: authored=claude; ran=cursor-muse,gemini,codex; adjudicated=domain; declined=cursor-grok,cursor-composer,cursor-kimi; rounds=64; full=16 @ 7ec686a
Review-Attention: study ~20m (critical: replicationConnection.ts; decisions: trim-to-guards, stub-copy-withhold-vs-invalidate, get-record-stub-as-miss, sender-only-guard) @ 7ec686a