Relationships
#3258 Datastore rework Phase 3: history compaction so superseded chunks can be reclaimed, dormant (no behaviour change)
Opened by stack72 · 10/11/2026· Shipped 10/12/2026
Background (read this first)
swamp is reworking its datastore layer (tracking: swamp-club#2865). In the target design, v3, every change is a commit in a single-writer SQLite commits.db, replicated by Litestream, and content over 8 KiB is stored as content-addressed chunks through storage/1. Phase 3 builds this dormant: nothing reaches it until a user opts a new, empty datastore into v3.
What's on main (897b64a0), documented in design/enablers/datastores.md:
- The commit store (swamp-club#3236):
commits.dbkeeps every commit of every partition, plus partition snapshots every 1,000 commits (Partition.snapshot()/restore(), aggregate state only: no content), and a store-wide change cursor. - The read model (swamp-club#3239):
- each checkout catches up from its own cursor and applies each commit in turn, reading every put's content as it goes;
- a fresh checkout replays the whole history from cursor 0;
- it keeps a live-record index of present records and their
BlobRefs (liveRecords,chunksInUse). That index keeps puts skipped for a path conflict, so their chunks count as live.
- Chunk GC (swamp-club#3249). It deletes only chunks that no stored commit names, through the
chunk_refsindex written with each append, and keeps everything else, because a fresh catch-up needs every chunk any commit ever named. That includes content later overwritten, and versions removed bydelete,data gcorprune. So storage grows with history: superseded content is never reclaimed.- Its race is closed by the sweep's re-check after it deletes, plus the writer's post-commit check.
- One residual crash case is left to the Phase 4 outbox.
Goal
Let superseded content be reclaimed safely, by making catch-up stop depending on the full history. It stays dormant.
Design questions to settle first, recorded in datastores.md
- How a fresh or far-behind checkout catches up without old puts. For example:
- a compaction point per partition (or store-wide): a materialisable state at seq N, meaning the present records with their
BlobRefs or inline content, plus catalog state; - a checkout whose cursor is below the point loads that state, reconciling its files and
_catalog_v3.dbwith it (removing what's no longer present), then replays only the commits after N; - a checkout already past N is unaffected.
- a compaction point per partition (or store-wide): a materialisable state at seq N, meaning the present records with their
- What happens to commits at or before a compaction point. Delete them from
commits.db, or keep them for audit with their content no longer guaranteed? Either way:chunk_refsmust stop counting their superseded puts;- the partition's
txnIdduplicate rule must keep working for retries of old commits. See how #3236's snapshotpriorTxnlookup handles txnIds before a snapshot; - Litestream replicates whatever you delete, so measure replica growth or shrinkage.
- The reference rule for GC after compaction: chunks named by compaction-point state, or by any commit after a point. Keep #3249's guarantees: the sweep's re-check and restore, and the writer's post-commit check. Adjust the referenced-chunk index so it tracks exactly this.
- Ordering and races: a checkout catching up while a compaction runs, and a GC sweep running around a compaction. No checkout may ever need a chunk that GC can delete.
- When compaction runs. Is it triggered by size or by commit count, and who runs it: the single writer (solo or the serve leader)? Wiring it into a command is the opt-in's job. Here it's a callable service.
- Run-record and audit needs. A run record naming
name@versionwhose content was compacted away (#3250 already allows version reuse) has to resolve to "content no longer available", never to the wrong content. Say how.
Tests
- Property test: for random histories, a checkout that catches up through compaction plus later commits ends with files, catalog and live index identical to one that replayed the full history before compaction.
- A checkout behind a compaction point: it reconciles, removing records that are no longer present.
- GC after compaction: superseded chunks are reclaimed, and present content is never deleted. #3249's race tests still pass unedited.
- Retries: of commits at or before a compaction point, they still land once.
- Crash safety: a crash mid-compaction leaves either the old state or the new one, never a mix.
- Dormancy: extend
integration/datastore_v3_rules_test.ts.
Out of scope
- Wiring a command (the opt-in).
- The Phase 4 outbox.
- Litestream itself.
Done when
- The decisions are recorded.
- Compaction, compaction-aware catch-up and the adjusted GC reference rule are on main, dormant, with these tests.
- All existing suites pass unchanged.
Blocked by: nothing (#3249 has merged). It can run in parallel with the two repository staging issues. Expect small merge conflicts in datastores.md and the rules test.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.