carlos / platform Public

Merge pull request #217 from carlosframework/corruption-doc-update

docs: record the currency question and the cutover blocker
Paul Campbell f5c5d07a9160b5050b8cf675b7486e9c5ba68364 parent d63a566parent f5bf530
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