Skip to content

Replication certifies origin floors for subscription checkpoints: level-2 certificates, floor-only resume, revocation on downgrade - #1092

Merged
kriszyp merged 55 commits into
mainfrom
feat/subscription-origin-checkpoints
Nov 2, 2026
Merged

kriszyp merged 55 commits into
mainfrom
feat/subscription-origin-checkpoints

Conversation

@kriszyp

@kriszyp kriszyp commented Oct 20, 2026 •

Copy link
Copy Markdown
Member
Two Buttons meme: a sweating node picks resume at position or resume below the floor

⊙ 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

originFloors level 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-js syncWal()). 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's writeCopyBounds (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
Group Root cause Findings Where
(a) fixed Committed copy-bound cache trusted another pass's droppable reservation acb9246c57b4 43d9b0cdaff9 79fe22c4c194 this PR
(a) fixed Persisting after settlement recreated the reservation 00e0810a13e8 this PR
(a) fixed Settling a resumed pass dropped coverage for rows applied past its cursor 72b84e768b51 85ab21ec64ad 238bf979cdda this PR
(a) fixed Bound rows rescanned on every load fce3ec0201ac 4dd9d4d19ed0 6ac76a1b7edc this PR
(a) fixed A failed seq-row write never reached onFailure, so settled marks were not restored e908e5160fcc 26f54d289722 harper#3649
(a) fixed DESIGN wording, an unused reader kind, a duplicate JSDoc 10955718de8c 23fb66ca41e6 8c6376082589 62fbea7b88ad 3e99cf2b9b80 this PR
(a) accepted No unit test for withholding at send time; the cluster downgrade case covers it 6eb80ca1b8de —
(b) Widening missed after a restart when only a relayed entry was partial da2a355d3a8f a8ec00b5de22 c4a4d5b5d469 69b54eb3ea40 f6d281cc0871 23198bd0d7e5 6895a4fa7d82 #1142
(b) Route marks and barriers on copies over level-0/1 links 6cd60f539035 0747669bc2a3 c009b177a6c2 c3d4ce6370f2 8a7bd2b89c71 601a145a39bf #1143
(b) Whether every row joins the copy-start commit ee3112723b06 f412b930fc54 0acdd2741138 72f7d61a139a 849f35bd3b6c #1144
(b) Route facts not scoped to their generation and connection c270b88ee348 29d8d2da2625 6549a1e5faaa 247b71f29a9b 4c6e2fc2412a 7cf0df1e945d e4f6508a1b21 51eeffe90a50 758cb83ad1d9 #1145
(b) Copy-pass completion: restore before finalization, stuck cursor removal 5f72a66bbbf2 7fa8347d5e7f 0e0bda27443d 120974ab1c7e 2d1031542b98 1f4888f5d2c9 #1146
(b) admission Rollback of the persisted semantics; live boundary below a certified start 3c04c6e4c0b3 (+ core 411148f0f6b5 9dc0b8f1a507 b02ad4d37a82) harper#3647

Earlier deferrals: #1134 (bound in the copy's own transaction), #1135, #1136, #1137, harper#3636, harper#3648, harper#3669.

Approaches weighed
  • Detect a violation on apply instead of binding the origin generation: unsound for a restored origin, whose next certificate is higher and whose filtered keys never reach a detector (owner ruling 6a).
  • Clear a revocation on a later, higher floor: would accept a token whose inclusive seek skips the violating key. Rejected (planning review).
  • Leave tokens out when a known origin is reached only over a partial route: contradicts strict availability; issuance is disabled instead.
  • Framing-Verdict: chosen-approach-sound (ce4683c6692b), for one writer per row with copy bounds applied at admission.
  • Projection holes (this round): one mutable hole row per peer, released by that peer's next clean pass, lost a later loss merged into a captured row, revalidated old checkpoints (a snapshot restores no audit entry), and stranded a departed peer's hole. Rechecks returned better-alternative-exists (42e6e216d43c, then 602b067138d7), adopted: immutable revisions, each origin's own boundary, a fill from any peer. Gemini: chosen-approach-sound (602b067138d7).
  • Hole recovery: no existing operation starts a from-the-beginning pass (update_node start_time only raises the start, remove_node keeps the [seq, peer] rows, FORCE_CLONE reuses 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

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?

Before After
{ origin: floor } from any peer with originFloors ≥ 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; 0 revokes; a level-1 peer still receives { origin: floor } byte for byte.

Level 2 is advertised only by a RocksDB build whose engine syncs the floor record before advertising it (syncWal). The resolved level is the lower of the two sides.
A relay forwards the level-2 fields only from a row whose own socket certifies at level 2 now, never from a row with a hole or partial history, so a downgrade or a gap stops at that hop.

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

  1. Start below the floor — the request resumes at below(F), never max(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.
  2. Record where the proof begins — the start is queued as the route's certifiedFrom, tagged with its origin generation. Core keeps the lowest start of the route that certifies the floor.
  3. Mark what breaks the proof — a dropped record marks the origin hole; a route widened to full coverage marks partialHistory on every origin of that row until a copy begun afterwards completes.
  4. Revoke on downgrade — when a direct peer's handshake no longer certifies at level 2, every origin on its generation-bearing row is revoked. Revocation sticks within the generation; the floor is cleared and relayed as 0.
  5. Carry the marks — marks and revocations ride the next sequence update of any shape and settle only when it commits. A downgraded peer sends none, so a receiver sends its own right after the request: never inside an open frame, never awaiting the subscription placeholder.

A floor-only start is never raised by a recorded transaction time: that would let the sender filter what committed between them, the gap the floor exists to close.

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 0 to 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" --> B
Loading

The 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

  • Unit and cluster suites pass on the final tree (details below).
This round's evidence
Sharded bootstrap with syncWal: cause and fix
  • Cause. In the four-node test, node 1 cannot verify node 2's CA, so failover routes node 2's system subscription through another node. Core writes that route's state to [seq, 2]; this PR marked and read [seq, 1]. COPY_START's partialHistory mark 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".
  • Fix. routeRowId() (receivingDataFromNodeIds[0]) for marks, runPostSeqWriteChecks, received floors and holes; PendingRouteState.verifyPersisted checks each batch against its own event's row; CopyPassState.routeId; bindSharedRouteSafety keyed by the subscription; a failed read-back retries on the timer.
  • Test premise. At level 2 a floor-only resume needs no closed-prefix wait, so the second case restarts node 2 with 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, the restrictedOrigins checks). A cross-shard origin's level-2 certificate is stored with fullCoverage: false and never qualifies in core's admission (subscriptionCheckpoint.ts qualifies). 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

🤖 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, touches replicationConnection.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

Late Grok findings

5 surviving (highest minor) — Grok reviewed 220b2fa
  • minor — 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. The partialHistory mark is set again on every reconnect (persistedPartial, :9766-9768). Only a fresh copy clears it, because tryRestoreCopyPass re
  • minor — replication/replicationConnection.ts:9610 — the exclusion-origins-updated include path calls currentOriginCursorVector() without the floor-only callback, so buildOriginCursorVector falls through to resumeStartWithFloor (:3277-3279) and returns max(position, cursorBelowFloor(floor)). Trigger: an origin was excluded while this node re
  • minor — replication/replicationConnection.ts:5486 — disputed: on a level-2 socket, a bare-floor certificate calls noteGeneration(id, undefined) and erases the cached generation, so a later revoke(id) (:5472) and hole() (:3008) carry no generation. Contrary to the lens's reading, a sibling core checkout treats an undefined generation as matching
  • minor — integrationTests/cluster/fixture-origin-log/resources.js:122 — the SetRouteCoverage fixture cannot work, for four separate reasons:
  • nit — replication/replicationConnection.ts:2951 — disputed: checkpointGradeRow does not check hole, so a row with a hole still resumes floor-only and gets a certifiedFrom mark. Floor-only resume starts lower, so it re-sends more; it cannot skip records. Core's checkpoint excludes rows with a hole (sibling subscriptionCheckpoint.ts:138, `:18

Not 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
  • minor — replication/replicationConnection.ts:7119 — On a sharding-restricted route whose peer is still in exclusionOrigins, the received certificate is stored with index 2 (relayable) = directFullCoverage.includes(name). Only routeFullCoverage (:7043) looks at restrictedOrigins. That row, which carries a generation and `fullCoverage:false
  • minor — replication/replicationConnection.ts:5387 — A completed projection settles only the current copyPassToken. 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,
  • minor — replication/replicationConnection.ts:5731 — A hole recorded during projectionCopy (recordReplicationHole :6339-6341) is queued, but every projection end_txn has remoteNodeIds: [], 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 the
  • minor — replication/replicationConnection.ts:6688 — bindSharedRouteSafety is keyed only by (database root, peer name) and runs on every socket after NODE_NAME (:7503). The inbound leg to the same peer therefore takes ownership: it strips unverified certifiedFrom from the outbound route's live state and replaces onSettled. When that inbound
  • nit — replication/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 with copyBoundReservation headroom, which reserv
  • nit — replication/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
  • major — 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-adjudicated
  • major — replication/replicationConnection.ts:12644 — On a same-socket resubscription, certifiedFrom is persisted (flushRouteState at :12680) before the replacement SUBSCRIPTION_REQUEST is sent. Scope: in-scope — Convergence-Class: previously-adjudicated
  • minor — replication/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-adjudicated
  • nit — replication/DESIGN.md:590 — The reservation-lifetime text contradicts the code. Scope: in-scope — Convergence-Class: previously-adjudicated

Not 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
  • major — replication/replicationConnection.ts:12644 — On a same-socket resubscription, certifiedFrom is persisted before the replacement request takes effect. Scope: in-scope — Convergence-Class: previously-adjudicated
  • major — 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-adjudicated
  • minor — replication/replicationConnection.ts:568 — A failure recorded with no position is never cleared, because the clear at :11075 compares a number against undefined. Scope: in-scope — Convergence-Class: response-code
  • minor — replication/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-adjudicated
  • nit — replication/DESIGN.md:590 — The reservation-lifetime text contradicts the code. Scope: in-scope — Convergence-Class: previously-adjudicated

Not 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
  • minor — replication/replicationConnection.ts:5583 — when the local queue refuses the copy-finish event, the relayed-origin anchors it carried are lost. drainCopyOriginAnchors moves them into pendingOriginCursors, and seqUpdateEndTxn then takes them onto the event and clears the map (replication/replicationConnection.ts:6823-6827). A refused e
  • minor — replication/replicationConnection.ts:11375 — five long-standing send sites ignore a false return from send(): replication/replicationConnection.ts:8340, :10674, :11186, :11375 and :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 w

Not 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

@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 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.

Comment thread replication/replicationConnection.ts Resolved
@kriszyp
kriszyp force-pushed the feat/subscription-origin-checkpoints branch from 09da15d to bfcdfae Compare October 20, 2026 20:36
@kriszyp
kriszyp marked this pull request as ready for review October 20, 2026 22:26
@kriszyp
kriszyp requested a review from dawsontoth October 20, 2026 22:26
@kriszyp

kriszyp commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

Converted to draft so this does not merge before the round-7 pre-push review findings are fixed. The two new majors:

  • A hole can be lost on a crash (replication/replicationConnection.ts:5010 / :8389): the frame's end_txn persists the cursor past a dropped record, but the hole rides a later sequence update.
  • A copy can leave a stale certifiedFrom (:9499): the mark is recorded when the request is sent and survives if the request turns into a copy, so a checkpoint inside the copied span can be admitted.

Two earlier majors are still open as well: :2996 (settle before the sequence row commits) and :4478 (copy callback reads live flags after an await). A dedicated effort: high fix task will take all four, starting with a framing recheck.

— Claude Opus 5.5

@kriszyp
kriszyp force-pushed the feat/subscription-origin-checkpoints branch 2 times, most recently from e763b58 to bbeec7b Compare October 27, 2026 06:17
kriszyp added a commit that referenced this pull request Oct 27, 2026
…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
kriszyp and others added 12 commits October 28, 2026 05:56
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
@kriszyp
kriszyp force-pushed the feat/subscription-origin-checkpoints branch from a1c827c to 9f4c3d7 Compare October 28, 2026 16:59
@kriszyp
kriszyp marked this pull request as ready for review October 30, 2026 21:22
kriszyp and others added 12 commits October 31, 2026 09:24
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
kriszyp and others added 4 commits November 2, 2026 01:42
… 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
kriszyp and others added 2 commits November 2, 2026 05:40
…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
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.

2 participants