Repository navigation
Replication certifies origin floors for subscription checkpoints: level-2 certificates, floor-only resume, revocation on downgrade - #1092
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements checkpoint-grade certificates, floor-only resume, and sticky revocation for per-origin subscription checkpoints (harper-pro#922 items 3–4). It introduces level 2 originFloors capability, allowing nodes to resume exactly below a stored floor and carry origin generation and full coverage details, with comprehensive integration and unit tests added to verify these replication behaviors. Feedback on the changes suggests explicitly checking for undefined in replication/replicationConnection.ts when updating the certified start position, rather than comparing a potentially nullish value using the <= operator.
09da15d to
bfcdfae
Compare
|
Converted to draft so this does not merge before the round-7 pre-push review findings are fixed. The two new majors:
Two earlier majors are still open as well: — Claude Opus 5.5 |
e763b58 to
bbeec7b
Compare
…overage persistence Group (a) fixes: - Committed copy-bound coverage is cached per pass and re-read after a settle, so a pass never trusts a reservation another pass's settle dropped. - A settled pass writes no more reservations, so its settle stays in effect. - A resumed pass keeps its reservation when it settles: an earlier attempt may have applied rows past its saved cursor that the source no longer sends. - Bound loads skip when the origin-state epoch has not moved. - DESIGN wording, a duplicate tuple JSDoc and an unused reader kind. Route-coverage persistence ([routeCoverage, peer]) is reverted: it extends widening detection beyond this PR's invariant and is filed separately. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xoBA9XmjPA2mM5joANzNm Dispatch-Task: harper-pro-1092-row-ownership
originFloors level 2 carries { origin: [floor, generation, fullCoverage] | 0 },
advertised only by a build whose engine syncs the floor record before it is
advertised. A level-2 route resumes exactly below a stored floor and records
where its complete prefix begins (certifiedFrom), marks holes and widened
routes, and revokes a direct peer's floor when it stops certifying at level 2.
Core's subscription checkpoints consume these qualifiers.
harper-pro#922 items 3–4. Submodule core -> 2871b8481d9c76f3a92daa706a829e42f76ad303.
Dispatch-Task: hp-922-subscription-checkpoints-resume
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JecFqDDkuL5xZoe9sAQLto
… downgrade, whole-route floor-only resume From the pre-push review: marks and revocations ride the copy-apply sequence update too and a flush of this node's own right after a subscription request, since a downgraded peer sends none; a revocation is accepted during a copy and drops the origin's held certificate; floor-only resume needs a whole-route row; a widening is detected from the persisted coverage across restarts; the capability ceiling is a parameter rather than module state. Integration test: a peer that stops certifying at level 2 is revoked on its receiver and one hop beyond. Submodule core -> c45b3d227153b7e7cd902b2af288f299db976213. Dispatch-Task: hp-922-subscription-checkpoints-resume Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JecFqDDkuL5xZoe9sAQLto
The flush after a subscription request awaited the placeholder with socket intake paused, which the handshake must never do (item 13): the DB_SCHEMA frame that resolves it could not arrive. It now sends when the subscription is resolved and retries from a timer otherwise; the next frame carries the marks in any case. Submodule core -> 2e31248342ce5a2a9b44002e438bf9944476086f. Dispatch-Task: hp-922-subscription-checkpoints-resume Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JecFqDDkuL5xZoe9sAQLto
…row; flush never inside a frame From the second pre-push round: marks and revocations stay pending until the update that carries them commits; the post-request flush respects an open frame and the blob clamp; a floor-only start is never raised by a recorded transaction time; a downgrade revokes, and a widening marks, every origin on the peer's row; a relay forwards no level-2 fields from a row with a hole or partial history, nor once the origin's socket dropped a level; a new generation replaces a held certificate and a revocation is keyed by generation; the copy-complete mark rides the copy's own durable update; dead level-1 helpers removed. Submodule core -> afcdc83c2662ec59d3bc595ea54a34d4c902ff17. Dispatch-Task: hp-922-subscription-checkpoints-resume Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JecFqDDkuL5xZoe9sAQLto
…idening marked under a resumed copy From the third pre-push round: marks and revocations are kept for the socket's lifetime and attached to every sequence update rather than settled on commit; a flush deferred by an open frame drains with the blob clamp on its own flag; a widening is marked under a resumed copy too and only a copy begun after the mark clears it; the direct peer's own row decides a persisted partial route; a copy's request records no certifiedFrom. Submodule core -> c8cc9a7e7be0cd1ca17a02e950e800615b9d0da3. Dispatch-Task: hp-922-subscription-checkpoints-resume Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JecFqDDkuL5xZoe9sAQLto
… request records no prefix From the fourth pre-push round: marks and revocations carry the origin generation they were derived under and are settled when the update that carries them commits, instead of living for the socket; a full copy with no saved cursor records no certifiedFrom; a fresh copy resets the resumed-copy flag and clears every row marked partial, persisted ones included. Submodule core -> cda2cd5fde6214f8fd83d6795c4ab966898f9152. Dispatch-Task: hp-922-subscription-checkpoints-resume Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JecFqDDkuL5xZoe9sAQLto
…deferred drains run From the fifth pre-push round: the resumed-copy flag is combined across the nodes of one request rather than decided by the last; a copy that clears partial marks forgets them; a route-mark drain deferred behind a frame runs even when the blob drain was deferred too. Submodule core -> 4d93ab3e20723a85b49e6212e870cc771ba23597. Dispatch-Task: hp-922-subscription-checkpoints-resume Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JecFqDDkuL5xZoe9sAQLto
… sync the WAL Level 2 is advertised only by an engine with syncWal(); on 2.12.0 every case waited out its 120 s deadline on CI. Dispatch-Task: hp-922-subscription-checkpoints-resume Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JecFqDDkuL5xZoe9sAQLto
…n floors
- Reconcile PendingRouteState marks and revocations against [seq, peer] before dropping them, coalescing marks by originId per connection and ordering partialHistory clear before partialHistory=false.
- Preserve level-2 tuple fields in PendingOriginFloors when merging a level-1 update for the same origin.
- Default buildLocalCapabilities({ durableFloors }) to false so call sites without WAL sync never advertise level 2.
- Trigger a fresh base copy on partial-to-full coverage widening (prevPartialHistoryForPeer), persist widened on the copy cursor across restarts, and clear partialHistory on COPY_END across all known origins.
- Update replication/DESIGN.md and add unit and integration coverage.
Dispatch-Task: pr-maint-2b964fca1b68d933fd0647c5cf664df4
Co-Authored-By: Gemini <noreply@google.com>
…state settlement Address review findings on level-2 origin floor certificates, route state marks, durable copy-pass tracking, and integration test coverage: - Require certifiedFrom <= cursorBelowFloor(closedFloor) (and optionally <= requestedStart) before relaying checkpoint-grade qualifiers from a direct route row, and do not certify copyStartTime on base-copy completion before floor-only resume. - Guard floor-only resume against base-copy watermark overlap via maxCopiedVersionByOrigin. - Harden PendingOriginFloors and PendingRouteState settlement against commit failure, coalesced marks, generation changes, bare-floor replacement, and partialHistory/certifiedFrom ordering. - Persist durable copy-pass state across COPY_START and COPY_END with deferred cursor removal and reconnect restoration, and clear widened/partial-coverage flags on copy restart. - Fix SetRouteCoverage/ClearSeq fixture and subscriptionCheckpointResume integration suite, providing a test-only standinSyncWal preload when native syncWal is absent. Dispatch-Task: pr-maint-2b964fca1b68d933fd0647c5cf664df4 Co-Authored-By: Gemini <noreply@google.com>
…lds and re-assert unpersisted route marks - Gate copy-batch persistCopyBounds() on crossing another peer's persisted certifiedFrom threshold (snapshot at COPY_START in otherPeerCertifiedFroms) and supportsCheckpointFloors(), avoiding per-batch dbisDB range scans when no other route has certifiedFrom or when level-2 floors are inactive. - Narrow persistMergedCopyBounds try/catch to the getRange scan so dbisDB.put rejections propagate, and skip the copying peer's own [seq, peerId] row (which is managed via PendingRouteState). - Only fall back to readDbisCursorSync(dbisDB, 'copyBound', 0) in mergePersistedCopyBounds when getRange is unavailable or fails. - Re-assert settled-but-unpersisted partialHistory, hole, and floorRevoked marks from partialMarked, holedOrigins, and revokedOrigins when a silent dbisDB.put failure leaves [seq, peerId] without the mark. - Defer revokedSent and lastSentBareFloorCursors updates until after ws.send succeeds, and pass state?.originGeneration when revoking on a wire 0 certificate. Dispatch-Task: pr-maint-2b964fca1b68d933fd0647c5cf664df4 Co-Authored-By: Gemini <noreply@google.com>
…removal, and sort copy threshold checks - Key partialMarked and holedOrigins by origin generation so generation rotations drop stale marks instead of re-asserting them onto a new generation - Preserve downgradedInPass and match undefined bare-floor generations when re-asserting downgrade revocations in sendSubscriptionRequestUpdate - Bump origin-state-epoch in persistMergedCopyBounds finally/catch whenever any [seq, *] certification was cleared before a later put failure - Guard dbisDB.remove([copyCursor, nodeId]) with a 1s unref retry timer in attachCopyCursorRemoval and maybeFinishCopy - Sort otherPeerCertifiedFroms ascending, hoist copyFrameSupportsCheckpoints, and shift crossed thresholds only after persistCopyBounds succeeds - Update DESIGN.md item 32 and SetRouteCoverage JSDoc Dispatch-Task: pr-maint-2b964fca1b68d933fd0647c5cf664df4 Co-Authored-By: Gemini <noreply@google.com>
…date A subscription update cleared the copied-version maxima that settlement merges into the copy-bound row, so a settle waiting on the bound-row lock could retire its reservation with the evidence gone. The maxima are now dropped only once the current pass has settled. Dispatch-Task: hp-1092-repin-main-20261028 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xoBA9NugSVmQfPsr7FeW2
a1c827c to
9f4c3d7
Compare
Local merge, not pushed: core is pinned to harper main 3e8a2fa19 and the branch does not yet work against it (see the dispatch log). - residencyControls.test.mjs runs with `syncWal` shadowed: rocksdb-js 2.13.0, which main now pins, syncs its WAL natively, so those tests would otherwise count the checkpoint route-state barriers beside the sharding traffic they assert on. Dispatch-Task: rebase-merge-kriszyp-harper-pro-1092-9f4c3d7dc179 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Core's durable session now adopts its first checkpoint only at a save where the floors have passed everything the session acknowledged, and a position update after each acknowledgement never qualifies. The failover case therefore lets the floors pass its first delivery and replaces its subscription, a save with no newer acknowledgement, before it holds the transaction. Dispatch-Task: rebase-merge-kriszyp-harper-pro-1092-9f4c3d7dc179 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Core now owns the `[copyBound, peer, ...]` rows through `writeCopyBounds`: a lock on the peer, one staged commit and an epoch bump. The connection reserves a pass into its `reserved` row and settles by raising the settled row while releasing that reservation, both through core. Core merges past an unrelated malformed row and releases a malformed reservation unread, so the connection still reads every bound row first and refuses the write on one, which admission also refuses on. Dispatch-Task: rebase-merge-kriszyp-harper-pro-1092-9f4c3d7dc179 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The failover case retries a replacing SUBSCRIBE until the durable session record carries a checkpoint, then holds the transaction, so the token that B resumes from is the one saved before it. Dispatch-Task: rebase-merge-kriszyp-harper-pro-1092-9f4c3d7dc179 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Takes main's core sync and the record-lock changes; `core` is harper main's tip 2dcd7fb82, which is also main's pin. Dispatch-Task: rebase-merge-kriszyp-harper-pro-1092-9f4c3d7dc179 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A sequence update whose onCommit throws reaches the frame-failure handler with no position. With no earlier attempt recorded, `undefined === undefined` read the missing record's attempt count and threw inside the subscription handler instead of holding the frame. Dispatch-Task: rebase-merge-kriszyp-harper-pro-1092-9f4c3d7dc179 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… copy A level-2 route that stopped mid-copy is marked partial, so its reconnect takes the coverage-widening path, which logs its own line for the same full copy. The case counted only the cursorless path's line. Dispatch-Task: rebase-merge-kriszyp-harper-pro-1092-9f4c3d7dc179 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t's first request; persist projection holes Route state was read, marked and verified on the socket peer's [seq] row, while core writes [seq, remoteNodeIds[0]], the first subscribed node. On a failover subscription the two differ: a generation-scoped COPY_START mark was ignored on the row core wrote, verification of the other row never passed, and the restore/flush/verify cycle held the replication worker at 100%. That worker then answered no sharding bootstrap RPC, so no node froze its origin start once the engine syncs its WAL (level 2). Every route-state reader now uses that row (copy finalization through the pass's captured row), verification checks each batch against its own event's row and reads all rows before consuming any, shared route safety is keyed by the route node, and a failed check retries on the 250 ms timer instead of inline. A same-socket resubscription or include update recorded certifiedFrom before the sender acted on it, while certificates from the old scan could still raise the floor past records it never sent. Only a socket's first request certifies now; later ones keep their floor-only starts. A sharding projection's dropped record was queued as a route hole that its route-less end_txns never persisted. The projection now raises the origin to HOLE_COPY_BOUND in its own settled [copyBound, source] row before the frame continues, so checkpoint admission refuses that origin across a restart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xoBABuL5LPiupQPsrZuft Dispatch-Task: hp-1092-bootstrap-and-majors
…s above the reservation At level 2 a checkpoint-grade route resumes just below its certified floor, which a held reservation keeps below itself, so the sender admits it at once and never logs the closed-prefix wait the test waits for. Whether the route was checkpoint-grade at the restart depended on whether a certificate had arrived, so the case failed intermittently once every runner certifies at level 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xoBABuL5LPiupQPsrZuft Dispatch-Task: hp-1092-bootstrap-and-majors
A failed sequence update names no position, and the clear compared the committed position with undefined, so the entry was never removed: a much later position-less failure counted as a replay of it and was skipped instead of reconnecting. Also drop a DESIGN sentence that said a reservation's over-approximation ends with the copy, which contradicted the kept-reservation rule beside it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xoBABuL5LPiupQPsrZuft Dispatch-Task: hp-1092-bootstrap-and-majors
…peer settles The hole sentinel now has its own row, [copyBound, peer, 'hole'], so it is not permanent: a later pass from that peer that started from the beginning, covered every table and dropped nothing has re-sent what the hole lacked, and releases the row in the same commit that settles its bounds. A pass that drops again, a resumed copy and a partial-coverage copy keep it. The sentinel is written at level 2 only, like every other copy bound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xoBABuL5LPiupQPsrZuft Dispatch-Task: hp-1092-bootstrap-and-majors
…keep holes out of reservations Review round 38 found four ways the hole release could revalidate missing history or strand it: - A complete pass restores the dropped record's state but not its audit entry, so releasing the hole outright let a checkpoint from before it resume past the gap. The release now leaves each origin a bound at the releasing pass's copy start in the settled row: every earlier start stays void, and the route certifies again from after the pass. - Each dropping pass writes its own hole row, and a pass fills only the rows that stood at its COPY_START, so an overlapping pass's newer hole survives. - Settlement captured its pass but read the live pass state after its await; a pass replaced on the socket, or a closed socket, now settles nothing. A dropped record of a replaced pass records no hole. - A connection's bound cache merged hole rows and then reserved them, so an abandoned pass kept a sentinel no release could reach. Hole rows now stay out of that cache; admission and the relay gate still read them from the store. Also: holes are written at any level on RocksDB, so an engine that later syncs its WAL refuses them; a superseded socket no longer takes the route's shared state; a barrier a closed queue refused no longer leaves its latch set; and the position-less failure clear is reverted, since it ran before the sequence write and let a persistent failure replay forever. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xoBABuL5LPiupQPsrZuft Dispatch-Task: hp-1092-bootstrap-and-majors
… to its own key, from any peer Planning recheck on the hole release (codex better-alternative-exists; Gemini sound with blockers): - A captured hole row stayed mutable: a later drop merged into the row a clean pass had captured, and that pass's release erased it. Each drop now writes the next revision of its pass's row, with every origin and the highest loss key so far, and releases the previous revision in the same commit, so a capturing pass never releases a loss recorded after it began. - The copy start is not always above the dropped entry's key (clock skew, or a record deleted before the clean snapshot). A filled hole now leaves each origin a bound at the later of its loss key and the filling pass's copy start. - A replaced pass skipped its hole write although its replacement may resume past the record; it now writes it. - A hole of a peer that left the cluster could never be filled. Any complete clean pass fills every hole row that stood when it began: its own peer's in the settle commit, another peer's right after under that peer's lock. - An undecodable record whose origin the node cannot name now holds every origin it knows. - The bound cache reads its rows in one pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xoBABuL5LPiupQPsrZuft Dispatch-Task: hp-1092-bootstrap-and-majors
…es an event; renumber item 33 Review round 39 (Gemini, Composer): - A closed subscription queue takes an event without calling onCommit or onFailure. The copy-finish event, both cursor-removal barriers and flushRouteState now put the cursor removal back in line (or retry the pending marks) when send() returns false. - A drop on a route whose origin the node cannot name now marks every origin it knows, as the projection path does. - persistCopyHole rejects when there is no store, so "false" from it only ever means unchanged. - The checkpoint item in replication/DESIGN.md becomes item 33: main had already taken 32. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xoBABuL5LPiupQPsrZuft Dispatch-Task: hp-1092-bootstrap-and-majors
…path that re-derives the subscription A closed queue takes an event without calling onCommit or onFailure. Retrying on the same cached subscription could never succeed, since a closed but unretired queue is what subscriptionForConnection keeps returning, and the frame-end drain and route-state sends still dropped a pending cursor removal. Every cursor-removal and route-state send now takes afterRefusedSend: it re-derives the database's subscription, then requeues the removal or retries the pending marks. Also fix the capability table's reference to the renumbered checkpoint item. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xoBABuL5LPiupQPsrZuft Dispatch-Task: hp-1092-bootstrap-and-majors
…ter_status A projection hole voids checkpoint admission for its origins with nothing visible to an operator. Each newly holed origin now logs one warning naming the database, table, reason and origins, and cluster_status lists the standing hole rows under sharding.<database>.checkpointHoles (source peer, origins, lostAt). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xoBABuL5LPiupQPsrZuft Dispatch-Task: hp-1092-bootstrap-and-majors
…in a cluster, and drop narrating comments No operation starts the from-the-beginning pass that fills a copy hole: update_node start_time only raises the start, remove_node keeps the [seq, peer] rows, and FORCE_CLONE reuses the data directory. The warning and DESIGN.md now name the supported fill: drain the node's unreplicated writes, remove_node it, and add_node an empty node under a new name (a sharded database refuses a wiped node's old name as origin-history-lost). They also say that a multi-shard database admits no checkpoint across shards yet, because every restricted route reports routeFullCoverage = false, so the fill restores no admission today. An operation that requests a full copy is harper-pro#1220. checkpointHoleRebuild.test.mjs drops a record in a sharding projection with the decode-failure hook, checks the warning and the cluster_status listing, then replaces the node and checks the new node holds the record and lists no hole. Also: named tuple members for route marks and revocations, and the narrating comments the review grouped are removed or shortened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xoBABuL5LPiupQPsrZuft Dispatch-Task: hp-1092-bootstrap-and-majors
… refused send afterRefusedSend re-derives the database's subscription, which can be a placeholder. Nothing marks a placeholder retired, so the route-state retries kept reading it after it resolved and closed the socket after 60 s, stalling deferred cursor removal and route marks. The placeholder is now adopted on resolution while this connection still owns it. The new unit test refuses the first route-mark event, installs a placeholder that resolves 300 ms later, and checks the marks land (it times out without the fix). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xoBABuL5LPiupQPsrZuft Dispatch-Task: hp-1092-bootstrap-and-majors
⊙ Problem
A subscription checkpoint can certify an origin only if its stored floor means a complete local prefix. Item 2's floors do not (harper-pro#922 items 3–4). Replication resumes at
max(position, below(F)). So a transaction committed below the sender's position during a disconnect is filtered, yet the floor past it stays stored. Nothing revokes a floor when a peer downgrades. A floor names no history identity or table coverage.🤔 Does replication need to change for a subscription feature?
Yes: a token cannot recover entries replication omitted, so completeness must be proven where data flows. A whole prefix requires floor-only resume, generation and coverage on the certificate, and revocation. Level-1 peers are unchanged.
💡 Solution
originFloorslevel 2 carries{ origin: [floor, generation, fullCoverage] | 0 }between two level-2 peers, advertised only by a build whose engine syncs the floor record before advertising it (rocksdb-jssyncWal()). A level-2 route resumes exactly below its stored floor and records where its proven prefix begins. Holes, widened routes and downgrades are marked or revoked. A base copy records what it wrote in rows of its own, and core's checkpoint admission voids any route start those rows cover.🤔 Floor-only resume re-sends up to a certificate interval of a busy origin on reconnect
Resuming at
below(F)rather than the position replays what lies between them, about one 30 s interval of a busy origin. That duplication makes a stored floor a complete prefix; the receiver deduplicates by identity (owner ruling 3a). Advertising level 1 reverses it per route.⚖️ Alternatives
🤔 Who writes a route's
[seq, peer]row?Only core's sequence updater for that route. A copy's effect on other routes lives in its own
[copyBound, peer]rows, written by core'swriteCopyBounds(harper#3685); rewriting other routes' rows raced the updater. Recording it in the copy's own transaction is not crash-safe (#1134).🤔 Rebase onto sharding (#1116)
Sharded routes take no floor-only start and report no full coverage, so a multi-shard database admits no checkpoint yet (deferral below).
🤔 Which row does a route mark?
The row core writes,
[seq, receivingDataFromNodeIds[0]], not the socket's peer on a failover subscription. Marking the peer's row failed every read-back. Inline retries starved sharding RPCs; retries now use the timer.🤔 When may a resume certify?
Only on a socket's first request. A later request on that socket keeps its floor-only start but records no
certifiedFrom: the sender's certificates still come from the old scan.🤔 What keeps a dropped projection record a hole, and what fills it?
An immutable hole row, written before the frame continues. A later clean full pass from any peer fills it. The supported fill is rebuilding or replacing the node (owner decision); it restores no admission until multi-shard databases certify.
🤔 Scope: what this PR fixes and what it defers
Fixed here: one writer per
[seq, P]row, copy bounds in core admission, level-2 certificates and revocation. Findings outside that, or failing closed, are filed below.Review findings by group
acb9246c57b443d9b0cdaff979fe22c4c19400e0810a13e872b84e768b5185ab21ec64ad238bf979cddafce3ec0201ac4dd9d4d19ed06ac76a1b7edconFailure, so settled marks were not restorede908e5160fcc26f54d28972210955718de8c23fb66ca41e68c637608258962fbea7b88ad3e99cf2b9b806eb80ca1b8deda2a355d3a8fa8ec00b5de22c4a4d5b5d46969b54eb3ea40f6d281cc087123198bd0d7e56895a4fa7d826cd60f5390350747669bc2a3c009b177a6c2c3d4ce6370f28a7bd2b89c71601a145a39bfee3112723b06f412b930fc540acdd274113872f7d61a139a849f35bd3b6cc270b88ee34829d8d2da26256549a1e5faaa247b71f29a9b4c6e2fc2412a7cf0df1e945de4f6508a1b2151eeffe90a50758cb83ad1d95f72a66bbbf27fa8347d5e7f0e0bda27443d120974ab1c7e2d1031542b981f4888f5d2c93c04c6e4c0b3(+ core411148f0f6b59dc0b8f1a507b02ad4d37a82)Earlier deferrals: #1134 (bound in the copy's own transaction), #1135, #1136, #1137, harper#3636, harper#3648, harper#3669.
Approaches weighed
update_node start_timeonly raises the start,remove_nodekeeps the[seq, peer]rows,FORCE_CLONEreuses the data directory). The owner chose node rebuild or replacement as the supported fill, a full-copy operation as #1220, and no automatic re-copy: the recurring drop causes would loop full copies.🔧 Changes
replication/protocolCapabilities.ts:ORIGIN_FLOORS_CAPABILITY = 2,peerCertifiesCheckpointFloorsreads the resolved level,buildLocalCapabilitiesadvertises level 2 only when the local floors are durable,resolvePeerCapabilitiestakes the lower of the two sides.replication/copyCursorWatermark.ts:CopyCursorValuepersistswidened,fullCoverage, andmaxCopiedVersionByOriginacross copy resume.replication/replicationConnection.ts:parseOriginCertificatesandencodeOriginCertificatecarry[floor, generation, fullCoverage]or0per level;collectRelayableCertificatesforwards level-2 fields only from a whole, hole-free row whose socket certifies now and whosecertifiedFromis above any[copyBound, originId];PendingRouteStateandSharedRouteSafetyStatekey marks and revocations by generation, settle them on commit, and verify them via a post-seq-writetxnStreambarrier read-back; the floor-only cursor vector starts a checkpoint-grade row below its floor and recordscertifiedFrom; a copy writes only its own bound rows: a reservation committed before the first row, with logarithmic headroom above the copy start, then settled to the exact versions once its rows are durable; a row waits only on bounds that stay committed (readCommittedCopyBounds); bounds are read through core's fail-closed reader; a level-2 row whose socket dropped a level relays nothing until revoked, at send time too; a level-1 floor clear and a coverage restore ride the route's ownend_txn; a failed sequence-row write releases a frame's commit slot once; the downgrade path revokes every level-2 row of a peer whose handshake stopped certifying; the sender tracks certified generations and sends0once per revoked generation;flushRouteStatesends marks right after the request without awaiting the subscription placeholder;CopyPassStatedecides whether a completed copy clearspartialHistoryand restoresfullCoveragebehind thetxnStreambarrier; the shared-status slot holds the level.core: pinned to harper main2dcd7fb82, main's own pin, which contains the merged companion harper#3649; this PR no longer moves it.replication/replicationConnection.ts(this round):routeRowIdaddresses marks, read-backs and holes to the row core writes, andverifyPersistedchecks each batch against its own event's row;bindSharedRouteSafetyis keyed by the subscription; only a socket's first request certifies; a projection's drop writes an immutable hole revision, read atCOPY_STARTbyreadCopyHolesand filled bysettleCopyBounds; one warning per newly holed origin;copyHoleStatusfeedscluster_statussharding.<db>.checkpointHoles; refused sends go throughafterRefusedSend, which adopts a replacement placeholder once it resolves.replication/DESIGN.md: the row-ownership rule; item 33 states the certificate, the floor-only resume and the mark and revocation lifetimes; the route's row and certification needs a fresh socket;DESIGN.md: the index entry.fixture-origin-log:ClosedFloorsexposes the level-2 fields,CheckpointSubscriptionissues and resumes checkpoints,DurableSessionRecordreads and writes the MQTT session row;schema.graphqlexportscheckpoint_origin;config.yamlloads it;standinSyncWal.mjsprovides a preload stand-in forsyncWal()in cluster tests.Product and architecture tour
Product and architecture tour
What a level-2 certificate promises
What does a receiver learn from a floor certificate that item 2's did not carry, and who may send one?
{ origin: floor }from any peer withoriginFloors ≥ 1; a stored floor says nothing about the origin's history identity, the route's table coverage, or whether the peer still certifies.{ origin: [floor, generation, fullCoverage] }between two level-2 peers;0revokes; a level-1 peer still receives{ origin: floor }byte for byte.Floor-only resume and the route's proof
Why does a reconnect on a level-2 route start exactly below the stored floor rather than at the higher applied position?
A reconnect on a level-2 route
below(F), nevermax(position, below(F)). A transaction reserved below the sender's position and committed during the disconnect is re-sent instead of filtered. This lets a stored floor mean a complete local prefix.certifiedFrom, tagged with its origin generation. Core keeps the lowest start of the route that certifies the floor.hole; a route widened to full coverage markspartialHistoryon every origin of that row until a copy begun afterwards completes.0.Example: a peer that comes back without the capability
A restarts as an older binary (no capability bag) while B and C hold level-2 floors for it.
Outcome: B revokes A at the next request and relays
0to C; C issues no checkpoint while A is revoked, and when A returns on level 2 the revocation sticks until A's generation changes.Membership and generations
How can a resume on another node distinguish an origin it never saw from one the issuer certified, without naming healthy origins?
Certificates, token and resume across A, B and C
A certifies to B; B relays to C; C issues a token the client resumes on B.
flowchart LR A[Origin A] -- "[F, gen, full]" --> B[Node B] B -- "relayed [F, gen, full]" --> C[Node C] C -- "token: digest over members" --> Client Client -- "resume with token" --> B B -- "digest matches own rows" --> BThe digest and the resume check live in core (HarperFast/harper#3356): an XOR of
sha256(origin, generation)over the members, and a resume that subtracts at most four late joiners from the node's own candidates before refusing with a 410. This PR supplies what those need from replication: the generation and coverage each certified row carries, and the revocation that removes a row from the candidates. Where two generations of one origin coexist, core's seq-row merge keeps the one whose rows saw the higher floor; this PR tags every mark and revocation with the generation it was derived under so a retained row of a removed peer never speaks for the restored origin.✅ Verification
This round's evidence
a84a36e6, one at a time:shardingBootstrap5/5 ×2,checkpointHoleRebuild2/2 ×3,subscriptionCheckpointResume5/5,removedOriginResumeCursor3/3,declarativeResidency7/7,recordLockRestartAvailability9/9. Replication unit suite: 1702 passing, 14 pending.e0715821, where the void was written back; green now.syncWal:shardingBootstrap.test.mjsfailed 0/5 on 45ff074; it passes 5/5 on this head (runs below).checkpointHoleRebuild.test.mjs: a projection drops a record (decode-failure hook), B warns and lists the hole, then B is replaced and the new node holds the record and lists none. Itsmkdtemproots are removed byteardownHarperfor every node, the retired one included.Sharded bootstrap with
syncWal: cause and fixsystemsubscription through another node. Core writes that route's state to[seq, 2]; this PR marked and read[seq, 1]. COPY_START'spartialHistorymark never landed on the row core reads, the read-back failed forever, and each failure re-queued and re-flushed inline. The http worker sat at 100%, answered no sharding RPC, and every source refused with "Sharding source has no frozen origin start".routeRowId()(receivingDataFromNodeIds[0]) for marks,runPostSeqWriteChecks, received floors and holes;PendingRouteState.verifyPersistedchecks each batch against its own event's row;CopyPassState.routeId;bindSharedRouteSafetykeyed by the subscription; a failed read-back retries on the timer.HARPER_TEST_FORCE_ORIGIN_FLOORS_LEVEL_1.Deferred, ready to file (owner's session files it)
A multi-shard database admits no subscription checkpoint. Every cross-shard route is restricted, so it reports
routeFullCoverage = false(replication/replicationConnection.ts, therestrictedOriginschecks). A cross-shard origin's level-2 certificate is stored withfullCoverage: falseand never qualifies in core's admission (subscriptionCheckpoint.tsqualifies). Strict availability then issues no checkpoint on any node, and admission refuses every token that names a cross-shard origin. Full coverage on such a route has to mean "every record this node should hold from that origin" for sharded tables, plus every record for ordinary tables. Copy-hole rows take effect once this lands. The warning text and DESIGN.md call it "tracked separately". Parent: #922.Executed evidence
A failed sequence update names no position, which threw inside the frame-failure handler;
holdFailedFramenow counts it. The cursorless copy case counts the widening log line as its second full copy, since a level-2 route that stopped mid-copy is marked partial.The MQTT failover case retries a replacing SUBSCRIBE until the session record carries a checkpoint before it holds the transaction: core (harper#3778) adopts a first checkpoint only at a save after the floors pass what the session acknowledged.
The sharding unit tests (
residencyControls.test.mjs) shadowsyncWal: rocksdb-js 2.13.0 syncs natively, so every runner is level 2 and would otherwise count checkpoint barriers beside the sharding traffic.PR QA ("Does a base copy through one route void a disconnected route's checkpoint certification on the same node?")
qa-pr-harper-pro-1092-0c0c53dcovered the pre-rebase heada1c827cd, before the rebase onto main with sharding (Replicate sharded tables through database shard maps #1116) and thecorere-pin to harper main.unitTests/replication/originFloors.test.mjs(new cases: certificate parsing/encoding per level, relay gating by level, floor-only vector, removed-origin start, pending route state, copyBound voiding, partial-coverage CopyPassState, and clearGenlessClosedFloorOnSeqRow) andprotocolCapabilities.test.mjs; fullnpm run test:unit(result below).integrationTests/cluster/subscriptionCheckpointResume.test.mjs, A–B–C with a capability-off E, 5 cases: level-2 certificates carry the generation and coverage on B (direct) and C (one hop) and not on E's link; a restart recordscertifiedFrom; a checkpoint issued on C names no origin and resumes on B, delivering a transaction A held open across it, and a foreign member set is refused with 410; an MQTT durable session fails over from C to B and receives the late lower-key transaction; a route widened from partial coverage to full coverage markspartialHistoryand restores after a full copy while preserving lowest-winscertifiedFromacross a second floor-only reconnect; A restarted on level 1 is revoked on B and on C through B's relay, C issues no checkpoint while it is, the revocation sticks when A comes back on level 2, and C restarted without a capability bag is revoked on B; plusintegrationTests/cluster/removedOriginResumeCursor.test.mjs(3/3 passing).The system database does not replicate in this topology, so the MQTT case copies the session row to B through the fixture where replication would carry it.
On a rocksdb-js without native
syncWal()(before 2.13.0),standinSyncWal.mjsinstalls a stand-insyncWal()onRocksDatabase.prototypevia--importand infixture-origin-log/resources.jsuntil the dependency is bumped past Add syncWal() to sync the write-ahead log without a memtable flush rocksdb-js#949.Full
npm run test:uniton the final tree: 2062 passing.subscriptionCheckpointResume.test.mjs2/2 and 3/3 across the final heads,removedOriginResumeCursor.test.mjs3/3.Checkpoint read cost (harper#3649's bench, bench host, turbo off): 125 µs with no copy bound rows, 202 µs with 16 rows of 16 origins, once per 5 s capture and per resume.
🤖 Generated by Claude Fable 5.1; posted via @kriszyp.
Depends-on: HarperFast/harper#3649, HarperFast/rocksdb-js#949
Release target: main — new replication capability level, dependent on rocksdb-js
syncWal()(HarperFast/rocksdb-js#949)Related PRs: #554 overlaps, #717 overlaps, #815 overlaps, #940 overlaps, #1086 overlaps, #1101 overlaps, #1103 overlaps, #1112 overlaps, #1116 overlaps (merged 2026-10-28; sharded routes take no floor-only start and report no full coverage; all touch
replication/replicationConnection.ts), #997 overlaps (core submodule pointer), #1075 overlaps (core submodule pointer, merged 2026-10-19), #1087 overlaps (residency stubs a copy skips must not clear partial history or a hole), #1065 overlaps (merged 2026-10-20; core f9890778 must descend from its core commit), #1060 overlaps (merged; its concurrent seq-row save test sits beside this PR's single-writer regression), #1140 overlaps (merged; copy-cursor and blob path in the same copy loop), #1133 overlaps (merged core sync; the core pin is now harper main), #1131 overlaps (merged core repin), #1130 overlaps (merged; sharding read path in replicationConnection.ts), #1054 overlaps (open; worker-thread default, touchesreplicationConnection.ts), #1177 overlaps (merged; the full-write-head exports this PR's core pin now includes), #285 overlaps (merged; forced base-copy resync in the same copy loop), #1192 overlaps (merged; non-owner read settlement in replicationConnection.ts), #1202 overlaps (open; replication pool default, same file), #1204 overlaps (merged; core bump), #1210 independent (pins the review-coverage action, which clears this PR's red review-coverage check)Complexity: complicated
Deferred findings, each filed
Deferred findings
a27ac0db862da1d9major data-integrity —core/resources/subscriptionCheckpoint.ts— admission orders database generations by floor timestamp — filed Checkpoint admission orders an origin's database generations by floor timestamp harper#3636ab4feaca0f8afbc5minor reliability —replication/replicationConnection.ts— one malformed [copyBound, *] row fails every persist and wedges checkpoint-capable copies in a 1011 reconnect loop — filed A malformed copy bound row stops checkpoint-capable base copies with no repair path #1137a596c3edd1cfc41cminor data-integrity —replication/replicationConnection.ts— route coverage not persisted for widening after restart — filed Widening after a restart is missed when only a relayed origin recorded partial coverage #1142d34a04883d53415dminor performance —replication/replicationConnection.ts— COPY_START manufactures route marks and barriers on legacy links — filed Checkpoint route marks and barriers run on base copies over level-0/1 links #1143968396cdbac99542minor data-integrity —replication/DESIGN.md— rollback can reopen admission on copy-voided certifiedFrom — filed Enforce the copy-bound ordering contract in core, where admission reads it harper#3647b0968145b2f2ad4bminor correctness —replication/replicationConnection.ts— delayed verification restores partial=true after a later copy clear — filed Scope route-state facts to the generation and connection that produced them #11453d8f16bc1ebf3966minor concurrency —replication/replicationConnection.ts— hole callback unfenced after its await — filed Scope route-state facts to the generation and connection that produced them #11451a1399754d736386minor reliability —replication/replicationConnection.ts— initially unscoped safety facts never bind to a generation; pending revocation blocks the next generation — filed Scope route-state facts to the generation and connection that produced them #114579e52501c546301fminor reliability —replication/replicationConnection.ts— verifyPersisted skips generation-scoped marks; unverified batches strip certifiedFrom/fullCoverage — filed Scope route-state facts to the generation and connection that produced them #11459c091d6823317a7dminor correctness —replication/replicationConnection.ts— partial history clears before copy blob finalization — filed Copy-pass completion: restore whole history only after finalization; unstick cursor removal #1146Late Grok findings
5 surviving (highest minor) — Grok reviewed 220b2fa
replication/replicationConnection.ts:9772— a route widened to full coverage still resumes incrementally, so rows of the newly included tables committed before the cursor are never requested. ThepartialHistorymark is set again on every reconnect (persistedPartial, :9766-9768). Only a fresh copy clears it, becausetryRestoreCopyPassrereplication/replicationConnection.ts:9610— theexclusion-origins-updatedinclude path callscurrentOriginCursorVector()without the floor-only callback, sobuildOriginCursorVectorfalls through toresumeStartWithFloor(:3277-3279) and returnsmax(position, cursorBelowFloor(floor)). Trigger: an origin was excluded while this node rereplication/replicationConnection.ts:5486— disputed: on a level-2 socket, a bare-floor certificate callsnoteGeneration(id, undefined)and erases the cached generation, so a laterrevoke(id)(:5472) andhole()(:3008) carry no generation. Contrary to the lens's reading, a sibling core checkout treats an undefined generation as matchingintegrationTests/cluster/fixture-origin-log/resources.js:122— theSetRouteCoveragefixture cannot work, for four separate reasons:replication/replicationConnection.ts:2951— disputed:checkpointGradeRowdoes not checkhole, so a row with a hole still resumes floor-only and gets acertifiedFrommark. Floor-only resume starts lower, so it re-sends more; it cannot skip records. Core's checkpoint excludes rows with a hole (siblingsubscriptionCheckpoint.ts:138, `:18Not standing: none. Grok ran 17m (17m from launch to report); the fold adjudication (
cursor-grok@220b2fabeb50-1b271d0a, claude) took 3m.Late Grok findings
6 surviving (highest minor) — Grok reviewed 801d9da
replication/replicationConnection.ts:7119— On a sharding-restricted route whose peer is still inexclusionOrigins, the received certificate is stored with index 2 (relayable) =directFullCoverage.includes(name). OnlyrouteFullCoverage(:7043) looks atrestrictedOrigins. That row, which carries a generation and `fullCoverage:falsereplication/replicationConnection.ts:5387— A completed projection settles only the currentcopyPassToken. Earlier projection attempts each wrote a[copyBound, peer, 'reserved', token]row at the anchor for every origin (:8021,:6893-6903).settleCopyBounds(:3943-3946) drops only its own token. Projections have no resume cursor,replication/replicationConnection.ts:5731— A hole recorded duringprojectionCopy(recordReplicationHole:6339-6341) is queued, but every projectionend_txnhasremoteNodeIds: [], so the mark is never attached or settled. The projection then closes the socket (:5397). A restart before another socket on the same worker flushes thereplication/replicationConnection.ts:6688—bindSharedRouteSafetyis keyed only by (database root, peer name) and runs on every socket afterNODE_NAME(:7503). The inbound leg to the same peer therefore takes ownership: it strips unverifiedcertifiedFromfrom the outbound route's live state and replacesonSettled. When that inboundreplication/DESIGN.md:585— The Row-ownership note contradicts itself. "The reservation's over-approximation lasts only while the copy runs" and "only voids starts at or below its own copy start" conflict with "An earlier attempt's reservation stays" in the same paragraph, and withcopyBoundReservationheadroom, which reservreplication/replicationConnection.ts:6975— Comments that narrate the code::6975("re-read, never assumed"), the tuple restatement at:3131-3133,integrationTests/cluster/subscriptionCheckpointResume.test.mjs:61,unitTests/replication/originFloors.test.mjs:801.Not standing: none. Grok ran 16m (16m from launch to report); the fold adjudication (
cursor-grok@801d9da207ce-ec3d1342, claude) took 2m.Late Grok findings
4 surviving (highest major) — Grok reviewed 8e07e80
replication/replicationConnection.ts:5759— A hole recorded during a sharding projection never reaches[seq, peer]before the projection settles its bounds and completes (:5414-5424). Scope: in-scope — Convergence-Class: previously-adjudicatedreplication/replicationConnection.ts:12644— On a same-socket resubscription,certifiedFromis persisted (flushRouteStateat:12680) before the replacementSUBSCRIPTION_REQUESTis sent. Scope: in-scope — Convergence-Class: previously-adjudicatedreplication/replicationConnection.ts:6692— Inbound and retrieval sockets for the same (database, peer) take ownership of the shared route-safety state. Scope: in-scope — Convergence-Class: previously-adjudicatedreplication/DESIGN.md:590— The reservation-lifetime text contradicts the code. Scope: in-scope — Convergence-Class: previously-adjudicatedNot standing: none. Grok ran 9m (10m from launch to report); the fold adjudication (
cursor-grok@8e07e80ac281-a29463ce, claude) took 1m.Late Grok findings
5 surviving (highest major) — Grok reviewed 45ff074
replication/replicationConnection.ts:12644— On a same-socket resubscription,certifiedFromis persisted before the replacement request takes effect. Scope: in-scope — Convergence-Class: previously-adjudicatedreplication/replicationConnection.ts:5759— A hole recorded during a sharding projection never reaches[seq, peer]before the projection settles its bounds and completes (:5414-5424). Scope: in-scope — Convergence-Class: previously-adjudicatedreplication/replicationConnection.ts:568— A failure recorded with no position is never cleared, because the clear at:11075compares a number againstundefined. Scope: in-scope — Convergence-Class: response-codereplication/replicationConnection.ts:6692— Inbound and retrieval sockets for the same (database, peer) take ownership of the shared route-safety state. Scope: in-scope — Convergence-Class: previously-adjudicatedreplication/DESIGN.md:590— The reservation-lifetime text contradicts the code. Scope: in-scope — Convergence-Class: previously-adjudicatedNot standing: none. Grok ran 15m (15m from launch to report); the fold adjudication (
cursor-grok@45ff0749d7d3-b06e416e, claude) took 1m.Late Grok findings
2 surviving (highest minor) — Grok reviewed a84a36e
replication/replicationConnection.ts:5583— when the local queue refuses the copy-finish event, the relayed-origin anchors it carried are lost.drainCopyOriginAnchorsmoves them intopendingOriginCursors, andseqUpdateEndTxnthen takes them onto the event and clears the map (replication/replicationConnection.ts:6823-6827). A refused ereplication/replicationConnection.ts:11375— five long-standing send sites ignore afalsereturn fromsend():replication/replicationConnection.ts:8340,:10674,:11186,:11375and:12236. So when the local queue closes while the socket stays up, the connection keeps the closed queue, and records and sequence updates are dropped wNot standing: none. Grok ran 12m (12m from launch to report); the fold adjudication (
cursor-grok@a84a36e6f8de-e965fb2c, claude) took 1m.Review-Coverage: authored=claude; ran=cursor-composer,gemini,codex,cursor-muse,cursor-grok; late=cursor-grok:adjudicated; adjudicated=domain; blocked=claude(fallback)(out-of-budget),muse(failed); declined=cursor-kimi; rounds=42; full=10 @ a84a36e
Review-Attention: deep ~190m (critical: copyCursorWatermark.ts, protocolCapabilities.ts +2; decisions: do-less-alternative, refused-send-recovery-by-adoption, hole-fill-by-node-replacement, sticky-revocation-within-generation, strict-checkpoint-availability, level2-gated-on-syncwal, route-row-is-remotenodeids0, release-target-unclear) @ a84a36e