Skip to content

Never send an INVALIDATED residency stub as a complete record from the base copy or GET_RECORD - #940

Draft
kriszyp wants to merge 63 commits into
mainfrom
fix/record-residency-handoff
Draft

kriszyp wants to merge 63 commits into
mainfrom
fix/record-residency-handoff

Conversation

@kriszyp

@kriszyp kriszyp commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

⊙ Problem

A node outside a record's residency holds an INVALIDATED index-only stub: the indexed fields only, with the INVALIDATED metadata 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:

  • Base copy. The copy loop sends every primary-store row as a complete put and strips the low metadata byte. sendAuditRecord rewrites a row to an invalidate only 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"} with invalidated: false.
  • GET_RECORD. The handler answered with the stub's bytes, and the requester stores and serves whatever it receives as the record.

❓ Your call: Is "a stub is never sent as a complete record" the right requirement for this PR? My recommendation: yes, as specified. Both guards hold on this branch's current core and do not depend on harper#3128.

This PR used to carry a residency-handoff design. The writer kept a complete transition image, a HANDOFF_RECEIPT_REQUEST/HANDOFF_RECEIPT exchange 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, the residencyHandoffReceipt capability, the redelivery sweep, the replicate: false gate 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.

❓ Your call: Milestone. This PR is milestoned v5.3, and harper#3128 is v5.4-only. The guards fix behavior that ships in v5.3 today and pass on a core without harper#3128, so they can stay on v5.3 whether or not harper#3128 is backported. I have not changed the milestone.

💡 Solution

  • Base copy: an INVALIDATED row goes only to a peer its residency excludes, where sendAuditRecord turns it into an invalidate as 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's invalidate(). Before this PR those rows were copied as complete puts 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.md note 29 records the rule, the withhold tradeoff and the tests.

No wire, capability or core change, and no core pointer move.

⚖️ Alternatives

  • Send a stub as an invalidate in 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. An invalidate would replace it. Not done here because it changes how the receiver applies copy frames (a non-put copy 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.

    ❓ Your call: Accept the withhold for now and track "copy a stub as an invalidate" as a follow-up? My recommendation: yes.

  • Keep the isDurableIdentityTie stub check from the earlier design. Removed. Core's 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 stub changed nothing end to end. The check only had an effect alongside the handoff's companion core change, which harper#3128 replaced.
  • Keep the handoff design alongside harper#3128. Rejected by the 2026-10-09 decision: once the writer keeps its complete record, no placement relies on a handed-over image.

✅ Verification

For each guard, its test passes with the guard in place and fails with the guard removed (dist rebuilt between the two runs):

Guard Test Without the guard
Base copy residencyStubGuard.test.mjs: 2-node cluster; B writes a record homed on B, A holds the stub, B is rebuilt from nothing fails: rebuilt B holds {"home":…} as a complete row
GET_RECORD residencyStubFetch.test.mjs: the real handler over a fake socket fails: the stub is served with 65524 value bytes
  • The cluster test used to depend on a writer writing itself out of residency, which harper#3128 removes. It now gets its stub on a non-resident peer, so it holds on cores with and without harper#3128. Two consecutive local runs passed.
  • unitTests/replication/**: 1250 passing, 0 failing (run before the identity-tie check was removed; that check's own test was removed with it). npm run build is clean, and oxlint reports 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

@kriszyp kriszyp added this to the v5.3 milestone Sep 29, 2026

@gemini-code-assist gemini-code-assist 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.

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.

Comment thread replication/residencyHandoff.ts Outdated
Comment thread replication/replicationConnection.ts Outdated
Comment thread replication/residencyHandoff.ts Outdated
@kriszyp
kriszyp force-pushed the fix/record-residency-handoff branch 2 times, most recently from cb3acb5 to 5052a0f Compare September 30, 2026 04:52
kriszyp added a commit that referenced this pull request Sep 30, 2026
…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>
kriszyp added a commit that referenced this pull request Oct 6, 2026
…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>
@kriszyp
kriszyp force-pushed the fix/record-residency-handoff branch from 09cfced to e8c1cf9 Compare October 6, 2026 01:39
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Release cherry-pick v5.3: conflict

Cherry-pick onto v5.3 produced conflicts on commit(s): 36c3ad1f393054bcb7e729f9f24e353eb8066a08 c7988e82504685204dde36ceb1450c5b1bf8966e 65877a92540c3d5ec78c71d7650a43d8086e2fa3 d18af42363e73637a906068bc9278c34d8ab2d22 ce1cd0b717d5192661c1731a77ac06f233bc12dd 6358e6e9680fb8ed427559d479c6278d7895bbcb e4bc9294e2f0ed7ad877d20824ef45c2f2801629 f289a90f31f1e1bcf0acea172c06e3fa12ecf01e cc36882bad06ad1a2a56a24ce6b2c070548c3a3d ce2948aaa95cf4221dc61ccc06a3a85081d5c393 7ec686a6c84953982647e30661098a2c0c0a9afb

The conflict markers are committed on branch cherry-pick/v5.3/pr-940.
Integration tests are not running until conflicts are resolved.

@claude please review branch cherry-pick/v5.3/pr-940 and suggest a patch that resolves the conflict markers (<<<<<<< / ======= / >>>>>>>) introduced by cherry-picking PR #940 onto v5.3. Post the suggested patch as a comment on this PR — do not push.

kriszyp added a commit that referenced this pull request Oct 6, 2026
…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>
@kriszyp
kriszyp force-pushed the fix/record-residency-handoff branch from e8c1cf9 to c5ba6a2 Compare October 6, 2026 03:12
github-actions Bot pushed a commit that referenced this pull request Oct 6, 2026
…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>
kriszyp added a commit that referenced this pull request Oct 6, 2026
…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>
@kriszyp
kriszyp force-pushed the fix/record-residency-handoff branch from c5ba6a2 to 63dbb6c Compare October 6, 2026 17:35
github-actions Bot pushed a commit that referenced this pull request Oct 6, 2026
…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>
github-actions Bot pushed a commit that referenced this pull request Oct 6, 2026
…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>
github-actions Bot pushed a commit that referenced this pull request Oct 6, 2026
…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>
kriszyp added a commit that referenced this pull request Oct 7, 2026
…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>
@kriszyp
kriszyp force-pushed the fix/record-residency-handoff branch from 8098359 to 9063963 Compare October 7, 2026 00:29
github-actions Bot pushed a commit that referenced this pull request Oct 7, 2026
…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>
kriszyp and others added 23 commits October 9, 2026 17:10
…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>
@kriszyp
kriszyp force-pushed the fix/record-residency-handoff branch from 180c904 to d3cebbe Compare October 10, 2026 00:37
github-actions Bot pushed a commit that referenced this pull request Oct 10, 2026
…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>
kriszyp and others added 3 commits October 9, 2026 22:28
…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
github-actions Bot pushed a commit that referenced this pull request Oct 10, 2026
…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>
@kriszyp kriszyp changed the title Hand a record-residency transition's complete image to the new resident, release it only on that resident's durable receipt, and never present an INVALIDATED stub as a complete record Never send an INVALIDATED residency stub as a complete record from the base copy or GET_RECORD Oct 10, 2026

This branch has not been deployed

No deployments
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.

1 participant