carlos / platform Public

docs: record the currency question and the cutover blocker

Two things the follow-up run established that were not in the design note.

§5b, the second question: the drills only ever asked "does this replica
restore?", and a dead replicator leaves a replica that answers yes forever
while the database moves on without it. The 2026-08-11 flagship incident is the
worked example — caught by a human reading a journal, which is not a control.
Records the change-counter signal and the two one-sided choices that keep it
from crying wolf.

§5c, why the generator has not taken over: the blocker is data, not code. The
kass route names a pre-migration database path that does not exist, so the
generated config covers 2 databases where the hand-written one covers 5 —
demonstrated by a read-only preview on the box, and refused by the shrink guard
rather than discovered afterwards. Also records the conclusion that the
litestream 0.5 upgrade and the takeover want to be ONE cutover, because the
upgrade's payoff only arrives with the config that states the retention.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell f5bf530ed11b3d8002f224902f2de2c8e1fb209c parent dd04065
1 file changed, +77 −1
  • docs/superpowers/specs/2026-08-15-sqlite-corruption-defence-design.md +77 −1
diff --git a/docs/superpowers/specs/2026-08-15-sqlite-corruption-defence-design.md b/docs/superpowers/specs/2026-08-15-sqlite-corruption-defence-design.md
index 420b7844c..ae01d56c0 100644
--- a/docs/superpowers/specs/2026-08-15-sqlite-corruption-defence-design.md
+++ b/docs/superpowers/specs/2026-08-15-sqlite-corruption-defence-design.md
@@ -203,11 +203,87 @@ The general lesson, worth carrying: **files a unit stages are charged to its
cgroup — tmpfs fatally, page cache survivably.** The fix for a drill that OOMs
is to get the bytes onto disk; the fix for one that crawls is a bigger ceiling.
+## 5b. The second question: is the replica CURRENT? (2026-08-16)
+
+Everything above answers "does this replica restore?". Working the follow-ups
+turned up a second question the drills had never asked, with a blind spot
+shaped exactly like the failure it exists to catch.
+
+**A replicator that dies leaves a replica that passes every check.** It is
+valid SQLite, it restores, `integrity_check` says ok — forever, while the
+database it mirrors moves on without it. The drill stays green and the backup
+quietly becomes a snapshot of the day replication stopped.
+
+This is not hypothetical. On 2026-08-11 the flagship's litestream exited with
+`permission denied` writing its shadow WAL into a DynamicUser tenant's state
+directory. A root drop-in fixed it the same minute and today's replicas are
+current (S3 timestamps checked) — but nothing in the platform would have said
+a word if it had not been fixed. A human reading a journal was the detection.
+
+The signal is SQLite's own **file-change counter** (header bytes 24–27), which
+advances on every write transaction reaching the main database file. Compare
+the source's counter with the restored copy's: a replica that stopped tracking
+is a number that stopped moving. No S3 layout knowledge, no litestream version
+dependency, no cross-machine clock — two files, read moments apart, by the
+same process.
+
+Two deliberately **one-sided** choices, because a durability check should
+under-claim rather than cry wolf:
+
+- WAL mode advances the main file's counter only at a checkpoint, so a source
+ with a hot WAL reads *lower* than reality. That can hide a stale replica (a
+ missed alarm); it can never invent one (a false alarm).
+- A source written within `staleGrace` (15m) is never called stale however far
+ behind its replica is. That is ordinary lag, and calling it a fault would put
+ the drill permanently red on the busiest — most valuable — databases on the
+ box, which is how a check stops being read.
+
+Both drills ask it now, and a stale replica FAILS: what comes back is not the
+database. "Source is not on disk" fails too — a config naming a database that
+no longer exists has a replica that can never advance again.
+
+## 5c. Why the generator has not taken over the boxes yet
+
+`carlos ops litestream` now has everything it needs — unit-tenant DB trees, a
+refusal to shrink the replication set, per-path replica keys, and a filter that
+replicates only what a route claims. The remaining blocker is **data, not
+code**, and it is recorded in issue #216.
+
+A read-only preview on the flagship (a build staged in /tmp, generating to a
+temp path, nothing touched) shows the whole problem in one table:
+
+| hand-written config today (5) | what the generator produces (2) |
+| --- | --- |
+| 4 × the kass tree | — |
+| `/data/registry.db` | `/data/registry.db` |
+| — | `/data/console/console.db` ✅ |
+
+The kass route's `db_path` names `/data/home.kass.training.db`, which does not
+exist: it is the pre-migration path from the app-capabilities design §5, and
+the §5 migration was never performed (#58). `stampDBPaths` is deliberately
+conservative about overwriting an operator's `-db`, which is the right rule —
+it simply assumes the pre-migration file exists, and here neither file does.
+
+Two things follow, and both are decisions rather than work:
+
+1. A tenant whose data is a TREE needs more than one `db_path` column. The
+ manifest already carries `data.dbs` as a list; only the stamp is
+ single-valued (`d.DBs[0]`).
+2. **The litestream 0.3 → 0.5 upgrade and the generator takeover want to be
+ ONE cutover.** Each starts a fresh replica lineage on its own, and the
+ upgrade's whole payoff — the 7-day window in §3 — only arrives with the
+ generated config, because that is what states the `snapshot:` stanza.
+ Upgrading the binary alone just swaps litestream's 24h default for its own.
+
+Until then the guard is what keeps the status quo safe: an accidental cutover
+is refused, by name, rather than silently dropping four databases.
+
## 6. Known follow-ups
- **Console surface.** `control/restore-verify.json` is read from the bucket
today, like `control/registry-snapshot.json`. A console/API view of "last
- proven restorable" per instance is the obvious next step.
+ proven restorable" per instance is the obvious next step — and now wants a
+ second column, "last proven CURRENT", since §5b made those different facts.
- **Fleet-box publication.** `hibernate-verify` resolves the deployment store
from `CARLOS_DEPLOYMENT_BUCKET`/`_DIR` only. A fleet box that reaches its
deployment through the console door publishes nothing — and says so loudly