fix(chunk-ingest): make optimistic chunk confirmation sticky per data_root#815
fix(chunk-ingest): make optimistic chunk confirmation sticky per data_root#815vilenarios wants to merge 4 commits into
Conversation
…_root
Optimistically-ingested chunks were TTL-evicted before a large bundle could
be served or unbundled, leaving a gappy, unservable set (e.g. relative_offset
0 missing -> range streaming from 0 misses -> falls to peers, which don't yet
have the fresh bundle -> 404 / "Unbundling failed" until it re-propagates).
Root cause: confirmation is a one-shot UPDATE fired by TX_INDEXED that only
touches placements present at that instant. A multi-GB bundle streams its
thousands of chunks in over a window far longer than that single event, so
most arrive AFTER it, never get confirmed, and hit the ingest TTL. On gw1 the
failing 2 GB bundle had only ~12% of chunks on disk with
chunk_ingest_evicted_total{ttl}=4000 vs chunk_ingest_confirmed_total=543.
Fix (retention/confirmation, not retrieval):
- saveChunkPlacement now inherits confirmed_at when the data_root is already
confirmed, so every chunk ingested after the confirm event self-confirms.
- New confirmed_data_roots marker table, populated by confirmChunkPlacements
(gated on EXISTS in chunk_placements so it stays bounded by ingested
bundles). It (a) is the primary inheritance source and (b) is excluded by
the GC TTL sweep, so a confirmed bundle is never partially evicted.
Statement + one small table only; no change to the retrieval path. Adds
regression tests (confirm a batch, then ingest more -> auto-confirmed and not
TTL-selected) and marker tests. Updates docs/chunk-ingest-cache.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFuubmcaMUDBGvRVRyp9BH
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #815 +/- ##
===========================================
- Coverage 78.82% 78.82% -0.01%
===========================================
Files 132 132
Lines 50172 50252 +80
Branches 3787 3790 +3
===========================================
+ Hits 39548 39611 +63
- Misses 10575 10592 +17
Partials 49 49 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
✅ Files skipped from review due to trivial changes (3)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds confirmed-data-root persistence and retention plumbing so chunk placements can inherit confirmation state, TTL GC skips confirmed roots, and confirmed markers are pruned on a separate retention window. ChangesSticky Confirmation Feature
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
src/database/standalone-sqlite.ts (2)
1036-1052: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd a TSDoc block for the sticky-confirmation contract.
Other non-trivial methods in this file (
getTransactionAttributes,getBlockByWeaveOffset) use TSDoc blocks; this method's new dual-effect behavior (confirms rows + records the marker) would benefit from one too.+ /** + * Confirms pending chunk placements for `dataRoot` (the one-shot update + * triggered by TX_INDEXED) and records a sticky `confirmed_data_roots` + * marker so chunks ingested afterward inherit confirmation at insert time + * and are retained past the TTL sweep. + */ confirmChunkPlacements(dataRoot: string, confirmedAt: number): number[] {As per coding guidelines, "Add or improve TSDoc comments on code you touch."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/database/standalone-sqlite.ts` around lines 1036 - 1052, Add a TSDoc block for confirmChunkPlacements describing the sticky-confirmation contract: it both confirms matching chunk_placements rows and records the data_root marker via markDataRootConfirmed so future ingests stay confirmed and retained. Keep the comment near the method signature in standalone-sqlite.ts, matching the style used by getTransactionAttributes and getBlockByWeaveOffset.Source: Coding guidelines
1036-1052: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winWrap
confirmChunkPlacements+markDataRootConfirmedin a transaction.The two writes aren't atomic; a crash between them leaves confirmed rows without the sticky marker, weakening TTL protection for any straggler chunk ingested right after restart. This file already wraps related multi-statement writes (e.g.
insertTxFn,insertDataItemFn) indb.transaction(...).🔒 Proposed fix
confirmChunkPlacements(dataRoot: string, confirmedAt: number): number[] { const dataRootBuf = fromB64Url(dataRoot); - const rows = this.stmts.chunks.confirmChunkPlacements.all({ - data_root: dataRootBuf, - confirmed_at: confirmedAt, - }); - this.stmts.chunks.markDataRootConfirmed.run({ - data_root: dataRootBuf, - confirmed_at: confirmedAt, - }); + const rows = this.dbs.chunks.transaction(() => { + const confirmed = this.stmts.chunks.confirmChunkPlacements.all({ + data_root: dataRootBuf, + confirmed_at: confirmedAt, + }); + this.stmts.chunks.markDataRootConfirmed.run({ + data_root: dataRootBuf, + confirmed_at: confirmedAt, + }); + return confirmed; + })(); return rows.map((row: any) => row.cached_at as number); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/database/standalone-sqlite.ts` around lines 1036 - 1052, The confirmChunkPlacements method currently performs two related writes, confirmChunkPlacements.all and markDataRootConfirmed.run, without atomicity. Wrap both operations in a db.transaction(...) in standalone-sqlite.ts so the confirmed rows and sticky data_root marker are committed together; use the existing transaction pattern from insertTxFn and insertDataItemFn, and keep the same dataRootBuf/confirmedAt flow inside the transaction callback.src/database/chunk-placements.test.ts (1)
168-307: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSolid regression coverage for the sticky-confirmation behavior, including the exact large-bundle TTL-eviction scenario this PR fixes.
One small gap: no test exercises
markDataRootConfirmed's first-confirmation-wins (ON CONFLICT DO NOTHING) semantics — e.g. callingconfirmChunkPlacementstwice for the samedataRootwith differentconfirmedAtvalues and asserting the marker retains the first timestamp. Optional given existing coverage is already strong.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/database/chunk-placements.test.ts` around lines 168 - 307, Add a focused test for first-confirmation-wins behavior on the data_root marker: in the `confirmed_data_roots marker` suite, call `confirmChunkPlacements` twice for the same `dataRoot` with different timestamps and assert the stored confirmation remains the first value. Use the existing `confirmChunkPlacements` and `getChunkPlacement` helpers to verify `markDataRootConfirmed`’s `ON CONFLICT DO NOTHING` semantics, alongside the current sticky-confirmation coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/database/sql/chunks/cache.sql`:
- Around line 19-33: The sibling-confirmation fallback in the chunk placement
lookup is still scanning all rows for a data_root, causing saveChunkPlacement to
degrade to O(n) during pre-confirmation inserts. Update the SQL around the
COALESCE in cache.sql to support the sibling lookup with a partial index on
chunk_placements keyed by data_root and filtered on confirmed_at IS NOT NULL, so
the SELECT sibling.confirmed_at query can use it efficiently.
---
Nitpick comments:
In `@src/database/chunk-placements.test.ts`:
- Around line 168-307: Add a focused test for first-confirmation-wins behavior
on the data_root marker: in the `confirmed_data_roots marker` suite, call
`confirmChunkPlacements` twice for the same `dataRoot` with different timestamps
and assert the stored confirmation remains the first value. Use the existing
`confirmChunkPlacements` and `getChunkPlacement` helpers to verify
`markDataRootConfirmed`’s `ON CONFLICT DO NOTHING` semantics, alongside the
current sticky-confirmation coverage.
In `@src/database/standalone-sqlite.ts`:
- Around line 1036-1052: Add a TSDoc block for confirmChunkPlacements describing
the sticky-confirmation contract: it both confirms matching chunk_placements
rows and records the data_root marker via markDataRootConfirmed so future
ingests stay confirmed and retained. Keep the comment near the method signature
in standalone-sqlite.ts, matching the style used by getTransactionAttributes and
getBlockByWeaveOffset.
- Around line 1036-1052: The confirmChunkPlacements method currently performs
two related writes, confirmChunkPlacements.all and markDataRootConfirmed.run,
without atomicity. Wrap both operations in a db.transaction(...) in
standalone-sqlite.ts so the confirmed rows and sticky data_root marker are
committed together; use the existing transaction pattern from insertTxFn and
insertDataItemFn, and keep the same dataRootBuf/confirmedAt flow inside the
transaction callback.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 97f00106-e393-46ad-8478-6c4552f0be42
📒 Files selected for processing (7)
docs/chunk-ingest-cache.mdmigrations/2026.07.04T12.00.00.chunks.add-confirmed-data-roots.sqlmigrations/down/2026.07.04T12.00.00.chunks.add-confirmed-data-roots.sqlsrc/database/chunk-placements.test.tssrc/database/sql/chunks/cache.sqlsrc/database/standalone-sqlite.tstest/chunks-schema.sql
…+ prune End-to-end testing (256 MB bundle, allowlisted bundler) exposed a hole in the sticky-confirmation marker: it was gated on EXISTS in chunk_placements, on the assumption that a bundle's chunks are ingested before its tx confirms. In practice the opposite ordering dominates. The bundler's chunk-broadcast TX-confirmation gate holds seeding until the tx confirms network-wide, and the gateway imports that same block first — so the TX_INDEXED confirm event fires ~2s BEFORE the first chunk is seeded. With no chunk present, the EXISTS gate skipped the marker, every later-seeded chunk stayed pending, and all 1025 chunks of the test bundle TTL-evicted (offset 0 lost) — reproducing the original gappy-bundle failure the fix was meant to prevent. Fix: write the confirmed_data_roots marker unconditionally at confirm time so later-seeded chunks inherit it regardless of ordering. Bound the table with an age-based prune in the GC sweep (CHUNK_INGEST_CONFIRMED_ROOT_RETENTION_SECONDS, default 1h) — a marker only needs to outlive the confirm->seed gap (seconds), after which ingested chunks carry their own confirmed_at. - cache.sql: drop EXISTS gate from markDataRootConfirmed; add pruneConfirmedDataRoots - standalone-sqlite: pruneConfirmedDataRoots (worker + queue wrapper + handler) - chunk-ingest-gc: prune stale markers each sweep - config: CHUNK_INGEST_CONFIRMED_ROOT_RETENTION_SECONDS - tests: confirm-before-chunks inheritance (the exact E2E scenario) + prune - docs: chunk-ingest-cache.md, envs.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFuubmcaMUDBGvRVRyp9BH
End-to-end validation on a real bundler → gateway pipelineDeployed this branch to a live gateway (chunk ingest on, allowlisted bundler) and pushed 256 MB / 1025-chunk bundles through it. Used a deliberately short ingest TTL (180 s, GC every 15 s) so unconfirmed chunks actually evict within the test window — otherwise the default 2–24 h TTL makes any short test pass trivially. Run 1 caught a real bug (now fixed in 815b701)The confirming Fix: record the Run 2 (with the fix) — clean pass
The whole bundle was retained and unbundled with zero network re-fetch, sustained far past the TTL that would have evicted it on the old code. Caveat (honest): Run 2's timing happened to land the first chunks ~4 s before confirm, so it exercised sibling inheritance + the marker with a few chunks present; the confirm-before-any-chunk path (Run 1's timing) is covered by the added unit test and by Run 1's demonstrated failure, rather than a second live run at that exact race. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/workers/chunk-ingest-gc.ts (1)
151-157: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider surfacing the pruned-marker count for observability.
The return value of
pruneConfirmedDataRootsis discarded. Given this table exists specifically to bridge a race window that was only caught via live E2E testing (per the PR discussion), exposing a counter/gauge (similar tochunkIngestEvictedCounter) would help detect ifCHUNK_INGEST_CONFIRMED_ROOT_RETENTION_SECONDSis misconfigured too low in production.♻️ Proposed addition
- await this.chunkPlacementIndex.pruneConfirmedDataRoots( - now - this.confirmedRootRetentionSeconds, - ); + const prunedMarkers = await this.chunkPlacementIndex.pruneConfirmedDataRoots( + now - this.confirmedRootRetentionSeconds, + ); + if (prunedMarkers > 0) { + metrics.chunkIngestPrunedConfirmedRootsCounter?.inc(prunedMarkers); + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/workers/chunk-ingest-gc.ts` around lines 151 - 157, The confirmed-data-root pruning result is currently ignored in chunk-ingest GC, so add observability for the number of pruned markers. In chunk-ingest-gc.ts, capture the return value from ChunkPlacementIndex.pruneConfirmedDataRoots inside the GC flow and record it with a dedicated metric (counter or gauge, similar to chunkIngestEvictedCounter) so misconfiguration of CHUNK_INGEST_CONFIRMED_ROOT_RETENTION_SECONDS is visible. Use the existing chunk ingest GC path and pruneConfirmedDataRoots symbol to wire the metric in without changing the pruning behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/workers/chunk-ingest-gc.test.ts`:
- Around line 77-80: The test for chunk-ingest GC is missing an assertion on the
cutoff recorded by the `pruneConfirmedDataRoots` mock. Update the
`rec.prunedBefore` expectations in `chunk-ingest-gc.test.ts` to verify the value
passed into `pruneConfirmedDataRoots` matches `now -
confirmedRootRetentionSeconds`, using the existing `rec` fixture and the
`pruneConfirmedDataRoots` stub.
---
Nitpick comments:
In `@src/workers/chunk-ingest-gc.ts`:
- Around line 151-157: The confirmed-data-root pruning result is currently
ignored in chunk-ingest GC, so add observability for the number of pruned
markers. In chunk-ingest-gc.ts, capture the return value from
ChunkPlacementIndex.pruneConfirmedDataRoots inside the GC flow and record it
with a dedicated metric (counter or gauge, similar to chunkIngestEvictedCounter)
so misconfiguration of CHUNK_INGEST_CONFIRMED_ROOT_RETENTION_SECONDS is visible.
Use the existing chunk ingest GC path and pruneConfirmedDataRoots symbol to wire
the metric in without changing the pruning behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 5277dc15-b95e-4d82-944d-1670384a86e6
📒 Files selected for processing (10)
docker-compose.yamldocs/chunk-ingest-cache.mddocs/envs.mdsrc/config.tssrc/database/chunk-placements.test.tssrc/database/sql/chunks/cache.sqlsrc/database/standalone-sqlite.tssrc/types.d.tssrc/workers/chunk-ingest-gc.test.tssrc/workers/chunk-ingest-gc.ts
✅ Files skipped from review due to trivial changes (3)
- docker-compose.yaml
- docs/envs.md
- docs/chunk-ingest-cache.md
🚧 Files skipped from review as they are similar to previous changes (2)
- src/database/chunk-placements.test.ts
- src/database/standalone-sqlite.ts
Adds monitoring and documentation for the sticky-confirmation marker introduced in the prior commits, so operators can verify the marker table stays bounded: - metrics: chunk_ingest_confirmed_roots (gauge, table size) and chunk_ingest_confirmed_roots_pruned_total (counter), set/updated each GC sweep - countConfirmedDataRoots() DB method (worker + queue wrapper + handler + iface) - glossary: "Sticky confirmation" entry - docs/chunk-ingest-cache.md: the two new metrics - test: count-method assertion Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFuubmcaMUDBGvRVRyp9BH
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/database/standalone-sqlite.ts (1)
1064-1069: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd a short doc comment to
countConfirmedDataRoots.The sibling
pruneConfirmedDataRoots(Line 1056) is documented, but this new marker-count reader has none. A one-liner noting it backs the observability gauge keeps this consistent.As per coding guidelines: "Add or improve TSDoc comments on code you touch".
📝 Proposed doc comment
+ // Current number of confirmed-data-root markers. Source for the + // chunk_ingest_confirmed_roots observability gauge set during the GC sweep. countConfirmedDataRoots(): number { const row = this.stmts.chunks.countConfirmedDataRoots.get() as { count: number; }; return row.count; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/database/standalone-sqlite.ts` around lines 1064 - 1069, Add a short TSDoc comment for countConfirmedDataRoots that explains it returns the current confirmed data roots count used by the observability gauge; place it alongside the existing standalone-sqlite methods near pruneConfirmedDataRoots so the new reader is documented consistently.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/database/standalone-sqlite.ts`:
- Around line 1064-1069: Add a short TSDoc comment for countConfirmedDataRoots
that explains it returns the current confirmed data roots count used by the
observability gauge; place it alongside the existing standalone-sqlite methods
near pruneConfirmedDataRoots so the new reader is documented consistently.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 088bb161-951f-4604-827a-3931b3a85008
📒 Files selected for processing (9)
docs/chunk-ingest-cache.mddocs/glossary.mdsrc/database/chunk-placements.test.tssrc/database/sql/chunks/cache.sqlsrc/database/standalone-sqlite.tssrc/metrics.tssrc/types.d.tssrc/workers/chunk-ingest-gc.test.tssrc/workers/chunk-ingest-gc.ts
✅ Files skipped from review due to trivial changes (1)
- docs/glossary.md
🚧 Files skipped from review as they are similar to previous changes (6)
- src/types.d.ts
- docs/chunk-ingest-cache.md
- src/workers/chunk-ingest-gc.ts
- src/workers/chunk-ingest-gc.test.ts
- src/database/sql/chunks/cache.sql
- src/database/chunk-placements.test.ts
Deterministic confirm-before-any-chunk validationThe earlier E2E runs happened to land the first chunks a few seconds before the confirm event, so they didn't stress the exact ordering that broke the pre-fix code. To force it deterministically, I bumped the bundler's Captured the decisive window (16 MiB / 65-chunk bundle):
Test environment cleaned upGateway reverted to the released image + production TTL; bundler |
…e (CodeRabbit) Addresses two CodeRabbit review comments on PR #815: - Major/perf: the sibling-confirmation fallback in saveChunkPlacement (SELECT ... WHERE data_root=? AND confirmed_at IS NOT NULL LIMIT 1) scanned the whole data_root range on the primary key whenever the confirmed_data_roots marker was absent (i.e. during a bundle's pre-confirmation ingest), making each insert O(n) and the window O(n^2) — worst case a multi-GB, thousands-of-chunk bundle. Add a PARTIAL index on chunk_placements(data_root) WHERE confirmed_at IS NOT NULL: tiny (empty exactly when the fallback runs most) and turns the lookup into an O(log n) seek (verified via EXPLAIN QUERY PLAN). Separate migration so it also applies where the marker-table migration was already recorded. Fallback retained (not redundant): it extends retention for chunks arriving after the marker's age-prune, as long as a confirmed sibling is still on disk. - Minor/test: assert the GC sweep calls pruneConfirmedDataRoots exactly once with a now-minus-retention cutoff (previously recorded but unasserted). Also corrects a stale comment in the marker-table migration (the EXISTS gate was removed in 815b701). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFuubmcaMUDBGvRVRyp9BH
|
Both CodeRabbit findings addressed in 279b792:
|
Problem
Bundles our own AR.IO Bundler uploads are "findable-late" and 404 on
/raw, and the indexer's unbundler fails for a sustained window — chronically for large bundles. On gw1 (turbo-gateway.com, where the bundler posts at scale) the failing 2 GB bundlezwpirOGSpS_AEs6DxxV68lXgk8_4ohTrvEVH_dCtrTchad only ~12% of its chunks on disk, gappy, withrelative_offset 0missing. Retrieval range-streams from offset 0 → immediate miss → peers (which don't yet have the fresh bundle) → fail. The 315 "failures" were ~9 such large bundles retried every 20 min.This is a retention/confirmation defect, not a retrieval-addressing one. (A separate investigation ruled out the retrieval path: a hermetic repro showed the gateway already serves ingested chunks locally by
(data_root, relative_offset)when they are present.)Root cause
confirmChunkPlacementsis a one-shotUPDATEfired by theTX_INDEXEDevent (system.ts): it only setsconfirmed_aton placements that exist at that instant. A multi-GB bundle streams its thousands of chunks in over a window far longer than that single event, so most arrive after it, never get confirmed, and are TTL-evicted by the ingest GC. Metrics on gw1:chunk_ingest_evicted_total{reason="ttl"}=4000vschunk_ingest_confirmed_total=543.Fix
Make confirmation sticky per
data_root(statement + one small table; no retrieval-path change, no hot-path tx lookup):saveChunkPlacementnow inheritsconfirmed_atwhen thedata_rootis already confirmed, so every chunk ingested after the confirm event self-confirms at ingest.confirmed_data_rootsmarker table, populated byconfirmChunkPlacements— gated onEXISTSinchunk_placementsso it stays bounded by ingested bundles rather than the whole tx index. The marker is (a) the primary inheritance source and (b) excluded by the GC TTL sweep, so a confirmed bundle is never partially evicted regardless of per-row state.Net effect: once the one-shot
TX_INDEXEDconfirms the first batch of a bundle, the entire bundle (present + all later-arriving chunks) is confirmed and retained → servable and unbundlable without waiting for network re-propagation.Correctness / scope
EXISTSgate means only data_roots we actually ingested chunks for are marked.Tests
src/database/chunk-placements.test.ts:confirmed_atfrom a confirmed sibling at ingest;data_rootfrom eviction even when a row is pending;EXISTS-gate: an indexed tx we never ingested chunks for is not marked and grants no phantom inheritance;confirmed_atstill wins.All green (chunk-placements + ingest + GC + sql-loader + standalone-sqlite = 117/117); lint clean; migration up/down verified on a scratch DB;
test/chunks-schema.sqlregenerated.Deploy notes
One additive migration (
confirmed_data_roots), no data rewrite; the SQL statement change takes effect on rebuild. On-by-default.🤖 Generated with Claude Code