Repository navigation
docs(design): the as-of scoped share as the replica bootstrap - #799
Conversation
Design only. It specifies the bootstrap half of local-sync-mode section 5. - A cut taken like an acquisition (one read transaction, one pinned tx) that emits change-feed records with valid time and lossless bytes, so a follower needs one conversion for bootstrap and tail. - A manifest with source identity and log epoch, watermark, scope and policy digests, shapes hash and per-graph completeness. - The feed's chunk frames past the frame budget. - Frozen graphs read from their mounted packs, so a rebuilt follower equals its source. - An epoch handoff that refuses to splice. Until per-graph read authorization has its own ruling, the route is write-class and fails closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF
A whole-board cut is one long read transaction, which pins the WAL. The read now spools to disk and closes its transaction before anything is sent, so a slow follower never holds the source's snapshot. The doc also states the targets (wall time and RSS at production size, measured on a scratch copy in the source PR) and the time and WAL-growth ceilings that abort a cut, which is refused and never sent partially. Only one read phase runs per source at a time. The route checklist (endpoint lists, share completeness, REST reference) is now named too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF
Pre-commit failed on 13 MD032 blanks-around-lists findings. Whitespace only: one blank line before each list; no wording changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF
|
CHANGES at
Five independent private contract-model controls pass, including the collision, short-cut checkpoint gap and quota example, with a clean HOME guard. These are design counterexamples, not a claim about an implemented endpoint or production behavior. No runtime operations performed. Native prepared content |
grant's three findings (CHANGES at eb11635): 1. scope_digest is SHA-256 over tagged compact JSON {"context_graphs","scope"}. Moving an IRI between the lists changes it. 2. The ceilings also run inside SQLite every 1,000 VM steps, so one slow statement is interrupted. A final check after the spool is flushed and before any manifest exists covers short and empty cuts. 3. The disk allowance is concrete: min(quota - bytes already under the spool dir, free disk - 1 GiB) It is fixed before the transaction opens. Every part and sorted run is charged before it is written, and ENOSPC aborts as disk_full, with cleanup. Also aligned with the frame contract wu pinned on the item: - the single quipu.asof frame and the quipu.asof.begin/part/commit names; - records carry tx, timestamp, actor and source; - "ROOT" can be cut when named; - per-graph digest = the applier's share_graph_digest, computed by external sort. Serving a re-request from its spool is marked as a later optimization. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF
|
Repair for CHANGES at
Also changed: the text now matches the frame contract pinned on the item:
Serving a re-request from its spool is now marked as a later optimization: the source deletes the spool after sending. Fresh CI is running on |
|
Budget measured: the read-phase target is MISSED. This is a design finding for §Budget, posted here because it affects the targets this PR states. Setup:
Cause (inferred from 23% CPU while cold): random I/O. The scan walks Proposed fix, to be measured before it goes into this design:
If that still misses, the alternative is to re-rule the targets, for example ≤ 120 s warm with a read ceiling above the measured cold time. |
|
CHANGES — Quipu #799 SAME repair review, exact 17a2ab7, aegis-hf1. Grant remains the sole independent nonauthor reviewer; Malcolm author. Rechecked all 21 hosted checks COMPLETED/SUCCESS. Current native classification d4b4e53aacd1d21aff27b263ccaa45d0228def21c262358ac35258cbfcf24073 and PREPARED 7eeaea94995d8ecf83c33fdd1c7bae2a26b091191452319e190b1a4458ace1d1 accepted:false preceded repair-source inspection. Own isolated clean detached checkout. Reviewed only the one-file repair against the retained full review, not a second broad audit. Resolved R1: the scope digest is now a compact canonical object with separately tagged context_graphs and scope arrays, each sorted. Moving an IRI across roles changes the encoded input; reordering within a role does not. Resolved R3: pre-transaction allowance counts all existing spool-directory bytes, including active sends, partial spools and digest runs, respects the free-space floor, and precharges each part/run; spool_quota and disk_full abort and clean up. Cached re-request service is explicitly deferred. R2 remains P1, narrowed to non-SQLite work while the transaction is open: docs/design/as-of-share.md lines 92-97 introduce bounded-size external sort runs and a merge into the hash; lines 129-131 close the read transaction only after the digests are final. Lines 163-173 check SQLite VM steps, each 10000 records, and after the final flush. SQLite progress callbacks do not execute during an external sort/merge/digest operation. Therefore a cut can finish its last SQLite read at t=1, spend another 301 seconds in non-SQLite digest work while retaining the read transaction, and only then reach the final check. That check correctly prevents publication but cannot undo the already exceeded WAL-pinning window. This is a static contract counterexample, not a native endpoint reproduction or measured claim that sorting normally takes 301 seconds. Require an explicit bound/cancellation/check schedule for non-SQLite work while the read transaction remains open, including sorting/merging/finalization, or close the snapshot transaction after raw snapshot rows have been safely spooled and do ordering/digests afterward. Add a controlled late non-SQLite-finalization case: no frames, scratch cleanup and a bounded read-transaction lifetime. Do not describe a SQLite progress handler as interrupting an arbitrary stalled I/O operation. No demand for impossible hard cancellation of an uninterruptible kernel operation; state that limitation honestly. Seven deterministic private contract controls PASS, covering role separation/order invariance, quota precharge, SQLite-check and empty-cut positives, the non-SQLite lifetime counterexample, and an off-transaction-digest alternative. These models test the design argument, not production code. Command exit0; HOME guard final CLEAN for 157 watched paths and 5 UID resources. No proposal edits, build, live export, runtime change or approval. Author consultation delivered live as aegis-wcil2k3wb. Retain original full review and repair history. Native record remains accepted:false; signed CHANGES names the new prepared content and full head. Resume only on Malcolm same-lineage repair plus complete-green CI and current native preparation, then Grant reviews that delta. Tim retains normal landing/watch; fzv/profile/runtime holds remain unchanged. |
…w R2) grant's R2 on 17a2ab7: merging digest runs ran with the read transaction still open, so slow non-SQLite work could pin the WAL past the ceiling. Now the transaction closes as soon as the scan ends and passes its final ceiling check. Merging the runs and writing the last part hold no snapshot. Inside the read, a spill is bounded to one run (32 MiB, shared by all graphs), and the ceilings are checked after every spill. The verification plan adds the checkpoint test (completes after the scan, blocked during it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF
|
R2 repair: head
|
…erging One instrumented cold release run on the production-sized scratch copy (23.8 us/record): frame 38.4%, sql_step 20.5%, serialize 18.2%, terms 11.4%. perf is unavailable (perf_event_paranoid 4), so the cut times its own phases. Fixing the top two: - frame: every record was serialized three times (digest line, size check, part text). PartBuilder now holds serialized lines and writes the part text directly, byte-identical to the serde_json form. A test keeps the old serde_json construction as the oracle. - serialize: the record is written from an AssertRecord struct whose fields are in alphabetical order (the canonical line, with no map per record), and the one line feeds both the digest and the part. Also, from grant's #799 point R2: the read transaction now closes right after the scan and its final ceiling check. Merging digest runs and writing the last part happen afterwards and never pin the WAL. Ceilings are checked after every spill. A test proves a TRUNCATE checkpoint from another connection completes at that point, and is blocked while reading. Sabotages (a part key renamed, record fields reordered, no early close) each turn a test red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF
…ance From grant's review of the design (#799): - scope_digest is SHA-256 over compact JSON {"context_graphs","scope"}, each list sorted. Moving an IRI between the lists changes the digest; reordering a list does not. - The final time/WAL check runs after the spool is flushed and before any manifest exists, so a cut of any size, including an empty one, ends inside its ceilings or is aborted with no spool left. - The disk allowance is fixed before the transaction opens: min(quota - everything already under the spool directory, free disk - disk_floor_bytes (1 GiB)) Nested run directories are counted. No allowance refuses the cut without reading. Parts and sorted runs are charged before they are written. A write that hits ENOSPC aborts as disk_full, with cleanup. Sabotages of each (a concatenated digest, no final check, a flat usage count, no disk floor) turn a test red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF
…erging One instrumented cold release run on the production-sized scratch copy (23.8 us/record): frame 38.4%, sql_step 20.5%, serialize 18.2%, terms 11.4%. perf is unavailable (perf_event_paranoid 4), so the cut times its own phases. Fixing the top two: - frame: every record was serialized three times (digest line, size check, part text). PartBuilder now holds serialized lines and writes the part text directly, byte-identical to the serde_json form. A test keeps the old serde_json construction as the oracle. - serialize: the record is written from an AssertRecord struct whose fields are in alphabetical order (the canonical line, with no map per record), and the one line feeds both the digest and the part. Also, from grant's #799 point R2: the read transaction now closes right after the scan and its final ceiling check. Merging digest runs and writing the last part happen afterwards and never pin the WAL. Ceilings are checked after every spill. A test proves a TRUNCATE checkpoint from another connection completes at that point, and is blocked while reading. Sabotages (a part key renamed, record fields reordered, no early close) each turn a test red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF
…ance From grant's review of the design (#799): - scope_digest is SHA-256 over compact JSON {"context_graphs","scope"}, each list sorted. Moving an IRI between the lists changes the digest; reordering a list does not. - The final time/WAL check runs after the spool is flushed and before any manifest exists, so a cut of any size, including an empty one, ends inside its ceilings or is aborted with no spool left. - The disk allowance is fixed before the transaction opens: min(quota - everything already under the spool directory, free disk - disk_floor_bytes (1 GiB)) Nested run directories are counted. No allowance refuses the cut without reading. Parts and sorted runs are charged before they are written. A write that hits ENOSPC aborts as disk_full, with cleanup. Sabotages of each (a concatenated digest, no final check, a flat usage count, no disk floor) turn a test red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF
…erging One instrumented cold release run on the production-sized scratch copy (23.8 us/record): frame 38.4%, sql_step 20.5%, serialize 18.2%, terms 11.4%. perf is unavailable (perf_event_paranoid 4), so the cut times its own phases. Fixing the top two: - frame: every record was serialized three times (digest line, size check, part text). PartBuilder now holds serialized lines and writes the part text directly, byte-identical to the serde_json form. A test keeps the old serde_json construction as the oracle. - serialize: the record is written from an AssertRecord struct whose fields are in alphabetical order (the canonical line, with no map per record), and the one line feeds both the digest and the part. Also, from grant's #799 point R2: the read transaction now closes right after the scan and its final ceiling check. Merging digest runs and writing the last part happen afterwards and never pin the WAL. Ceilings are checked after every spill. A test proves a TRUNCATE checkpoint from another connection completes at that point, and is blocked while reading. Sabotages (a part key renamed, record fields reordered, no early close) each turn a test red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF
* fix(freeze): mint a new log epoch when freeze or thaw changes served history Deep freeze relocates a graph's rows into a pack. The graph reads the same, so a following replica needs no record, but the change feed no longer serves those rows: a replay from an older cursor silently returns fewer records (local-sync-mode.md F2). Thaw re-imports the rows behind every cursor. Both now mint a new log epoch inside their savepoint, so a follower sees the epoch change and re-bootstraps instead of trusting its cursor. Freeze emits no tombstone records: that would make a replica drop facts the source still serves. The test checks that the window graph's feed records go from present to absent and that the epoch changes on freeze and again on thaw. Removing either mint turns it red. Refs aegis-xdvg.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0185RxTpLsPCQFB9ni72Nzac * fix(freeze): keep the log epoch when a freeze or thaw moves no rows A freeze that removed no feed rows, or a thaw that restored none, changed no served history. Minting a new epoch then would make every follower re-bootstrap for nothing. A refused freeze already rolled back before the mint, and a test now pins that too. The freeze test also checks that the next change page names the new epoch, which is how a follower learns its cursor belongs to the old history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * refactor(stream): build oversized-transaction parts one record at a time Move the F6 part builder into the library as feed_parts::PartBuilder. It hands out each part as it closes, so a caller can write parts to disk as they are made, and memory holds one part rather than the whole transaction. chunk_transaction now drives it. Part layout, sizes and the digest chain are byte-identical; the stream tests pass unchanged. This lets the as-of share send a cut with the same frames and digests instead of a second chunking scheme. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * feat(asof-share): cut a replica bootstrap at one transaction asof_share::cut takes the live facts of 1..=64 committed graphs at transaction T, inside one read transaction, and writes them as /changes assert records into an on-disk spool of feed parts. The manifest binds source store and log epoch, watermark_tx, scope, policy and shapes digests, and per-graph record counts and digests. The read transaction is open only while spooling. Wall time and WAL growth are checked every 10,000 records, and either ceiling aborts the cut and deletes the spool. A full spool directory refuses the cut before the transaction opens. The scan rides idx_geav, so it needs no sort. Graphs served from an attached or frozen pack are refused until the frozen-pack read lands (s24.1.1). Conformance tests bootstrap a replica from the cut, tail /changes from T, and compare live-fact digests with the source. They cover: a retract after T, a write committed during the cut, a pinned T with a later assert, splitting, the ceilings and the refusals. Four sabotages (no retract bound, no tx bound, raw valid_to, no WAL check) each turn the suite red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * feat(asof-share): POST /share/asof streams a cut from its spool The handler cuts under read admission, then streams the spool as quipu.asof.begin (manifest, totals, digest, source), numbered quipu.asof.part frames and a quipu.asof.commit barrier. Only the barrier carries an SSE id. Every refusal and abort is answered before the stream starts. A cut that cannot be finished never sends a barrier. Admission: - one read phase per source; a second request gets 503 cut_in_progress with Retry-After and is not queued, so cuts never extend the pinned window back to back; - a send slot from the change stream's pool is taken first; - the spool is deleted when the stream ends, and spools older than an hour are swept. The route is write-class: it writes nothing, but this keeps it fail-closed until per-graph read authorization has its own ruling. docs: REST reference entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * feat(asof-share): meet the pinned frame contract and bound every stall Frame contract (wu, aegis-s24.1; consumer quipu #803): - a cut that fits one frame is a single quipu.asof {manifest, records}; - larger cuts keep F6's begin/part/commit chain; - records carry their transaction's timestamp, actor and source, as /changes records do; - "ROOT" can be cut when named; - the per-graph digest is the applier's share_graph_digest: SHA-256 over the canonical record lines, sorted, joined by "\n". The producer gets there by sorting runs of at most 32 MiB, spilling them beside the spool and merging, so memory stays bounded. A test checks it against the applier's function, verbatim, at three run sizes. Ceilings (grant's review of the design): - The time and WAL ceilings also run as SQLite's progress handler during the read phase, so one slow step is interrupted mid-statement instead of waiting for the next 10,000-record check. - Spilled digest runs count against the spool quota as they are written. Sabotages: a handler that never stops, unsorted spilled runs and an unsorted in-memory sort each turn a test red. examples/asof_budget measures a cut on a scratch copy against the design budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * fix(asof-share): tagged scope digest, final ceiling check, disk allowance From grant's review of the design (#799): - scope_digest is SHA-256 over compact JSON {"context_graphs","scope"}, each list sorted. Moving an IRI between the lists changes the digest; reordering a list does not. - The final time/WAL check runs after the spool is flushed and before any manifest exists, so a cut of any size, including an empty one, ends inside its ceilings or is aborted with no spool left. - The disk allowance is fixed before the transaction opens: min(quota - everything already under the spool directory, free disk - disk_floor_bytes (1 GiB)) Nested run directories are counted. No allowance refuses the cut without reading. Parts and sorted runs are charged before they are written. A write that hits ENOSPC aborts as disk_full, with cleanup. Sabotages of each (a concatenated digest, no final check, a flat usage count, no disk floor) turn a test red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * docs(asof-share): REST reference for the pinned frames; refresh witnesses The REST entry now covers: - the single quipu.asof frame; - "ROOT" when named; - record transaction fields; - the per-graph and scope digest definitions; - disk_full; - the disk allowance. rest-api.md and src/lib.rs changed, so their recorded source_sha256 witnesses are refreshed. Their summaries still hold. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * fix(asof-share): clippy then_some; budget harness cuts ROOT too Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * perf(asof-share): one sequential rowid-order pass instead of index-order lookups The full-board budget missed its read target (611.6 s partly cold, 163.5 s warm, against 120 s). The cause is inferred from 23% CPU while cold: random I/O. The scan walked idx_geav (g, e, a, v) with one table lookup per row, plus one transactions lookup per record. The cut is now one pass over facts in rowid order (NOT INDEXED) for every selected graph at once: - the read is sequential; - consecutive rows share a transaction, so its metadata is read once per transaction; - the per-graph digest sorts its lines, so emission order does not reach it; - every graph's digest run shares one memory budget, and the largest run spills first. Tests: the plan is exactly "SCAN facts". Each record carries its own transaction's time. Sabotages (a stale transaction cache, unsorted spilled runs) each turn a test red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * fix(asof-share): name the transaction metadata tuple for clippy Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * perf(asof-share): serialize each record once; close the read before merging One instrumented cold release run on the production-sized scratch copy (23.8 us/record): frame 38.4%, sql_step 20.5%, serialize 18.2%, terms 11.4%. perf is unavailable (perf_event_paranoid 4), so the cut times its own phases. Fixing the top two: - frame: every record was serialized three times (digest line, size check, part text). PartBuilder now holds serialized lines and writes the part text directly, byte-identical to the serde_json form. A test keeps the old serde_json construction as the oracle. - serialize: the record is written from an AssertRecord struct whose fields are in alphabetical order (the canonical line, with no map per record), and the one line feeds both the digest and the part. Also, from grant's #799 point R2: the read transaction now closes right after the scan and its final ceiling check. Merging digest runs and writing the last part happen afterwards and never pin the WAL. Ceilings are checked after every spill. A test proves a TRUNCATE checkpoint from another connection completes at that point, and is blocked while reading. Sabotages (a part key renamed, record fields reordered, no early close) each turn a test red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * docs(asof-share): describe the graph digest as it is computed GraphCompleteness.digest is the applier's share_graph_digest (sorted lines joined by a newline), not lines in send order; and digest runs share one memory budget across graphs. The budget harness also reports read plus digest merge, the baselines' boundary. (wu, s24.2 seam check.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * fix(asof-share): refuse a frozen graph instead of cutting it empty The refusal checked only attached_graphs, which lists graphs CONTRIBUTED by an attachment (registry source = alias). A frozen graph stays a local graph whose rows moved into its own pack, so it was not on that list. The cut read main for it and emitted it with zero records and no error: the silent-empty-graph failure the design forbids. This was found by writing the frozen-graph test first (s24.1.1): the cut returned Ok. is_frozen checks the frozen-pack registry (mounted, not thawed). The test freezes a graph and requires a refusal, both in-session and after reopening. Sabotage (drop the check) turns it red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * refactor(asof-share): split under the file-size ratchet CI's file-size ratchet failed on #805: src/asof_share.rs had 706 code lines and src/server.rs 503, against a limit of 500. - asof_share/manifest.rs: the manifest types, re-exported unchanged. - asof_share/support.rs: the record form, the read-phase guard and ceilings, disk accounting, the graph digest and the shapes hash. - The asof handler module and its route move into publication.rs, beside /acquire, so server.rs is back at its 500-line baseline. The handler uses crate:: paths, and server/asof.rs leaves the route-free list. Code moves only; no behaviour change. asof_share.rs is 450 code lines, server.rs 500. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * fix(asof-share): give each cut its own spool Review on 5c86758: two cuts of the same source at the same T share a manifest, and both were renamed to <manifest>.parts. Dropping the first response deleted that path, so the second, already-complete cut emitted quipu.error instead of its commit barrier. The spool is now <manifest>-<pid>-<n>.parts, owned by one response for its whole life. The manifest digest is unchanged. identical_cuts_own_their_spools abandons, then completes, the first of two identical cuts while the second still has to open and read its spool, with a control that both share the manifest. Restoring the shared name turns it red at the commit-barrier assertion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
What
A design document, with no code:
docs/design/as-of-share.md. It covers the bootstrap half oflocal-sync-mode.md§5. A follower starts from a cut of its source at transactionT, then tails/changesfromT.The design in brief
/acquire: one read transaction, one pinnedT, complete or an error. But it emits change-feed records (typed values, lossless bytes,valid_from/valid_to) instead of N-Triples. N-Triples cannot carry valid time, so a replica bootstrapped from them could never equal a source the feed describes. With records, a follower uses one record-to-datum conversion for both bootstrap and tail.source {store_id, log_epoch},watermark_tx,scope_digest,policy_digest,shapes_hash, and per-graph completeness.begin/part/commitframes, not a second chunking scheme.Tfrom their mounted packs. Freezing remaps term ids but not transaction ids, so a rebuilt follower equals its source. An unmounted or corrupt pack refuses the cut.log_epoch, the follower stops, refuses to splice, and re-bootstraps.Review asks
T, relying on transaction ids being preserved. The implementation will test this explicitly.Rollback
Docs only: revert the file.
🤖 Generated with Claude Code
https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF