diff --git a/docs/plans/2026-07-20-phase2-resume-state.md b/docs/plans/2026-07-20-phase2-resume-state.md new file mode 100644 index 00000000..f7360e46 --- /dev/null +++ b/docs/plans/2026-07-20-phase2-resume-state.md @@ -0,0 +1,106 @@ +# LocalDb Phase 2 — resume state (written 2026-07-20, pre-compaction) + +Scratch handoff for picking the plan back up. Delete once Phase 2 lands. + +**Plan:** [`2026-07-19-localdb-adoption-phase2.md`](2026-07-19-localdb-adoption-phase2.md) +(execute with `superpowers-extended-cc:executing-plans`; task state in +`…-phase2.md.tasks.json`) +**Branch:** `feat/localdb-phase2` (from `feat/localdb-phase1`, which is **not** merged/pushed) +**Tree:** clean @ `8652eab9`. Build 0 warnings. + +--- + +## Where we are + +**Task 1 (the gate) is DONE. Its STOP verdict has been superseded — Phase 2 is unblocked.** + +Task 1 appeared to fail on a Phase 1 blocker (LocalDb throwing `disk I/O error` under load). +That was **root-caused the same day as observer-induced and not a product defect**: host-side +`sqlite3` reads of the live bind-mounted WAL files reset the WAL under the container. The +unmodified library sustains the full soak load indefinitely. Full write-up in +[`docs/known-issues/2026-07-20-localdb-disk-io-error-under-load.md`](../known-issues/2026-07-20-localdb-disk-io-error-under-load.md) +§0 (cause) and §11a (fixes shipped). + +**Tasks 2–21 are untouched. No plan code has been written.** + +### Before starting Task 3 + +1. **Task 2** (trivial, ~3 min) — the decision record. Blocked on Task 1's numbers, which are + partly missing (below). +2. **Re-run the cut-short sampling.** The soak died before producing sustained + `sf_messages` / `native_alarm_state` write rates. Use the **`snap` helper now in the plan** + (Task 1 step 2, ~line 217) — copies the `.db`/`-wal`/`-shm` triplet and queries the copy. + **Never** run host-side `sqlite3` against a live file. Both site-a nodes must have been + restarted since any host-side read (they have been — see rig state). +3. Then Wave 1 (Tasks 3 + 4) can be dispatched in parallel. + +--- + +## Findings that change the plan (carry these forward) + +From [`2026-07-19-localdb-phase2-soak.md`](2026-07-19-localdb-phase2-soak.md): + +- **D6's premise is wrong, its risk is real.** The plan says `config_json` is "documented to + exceed 128 KB per row." The source actually says the *escaped Akka envelope* blew the 128 KB + frame and the raw JSON "only needs to be ~60-70 KB". So the largest known production row is + ~60–70 KB → **no stop condition**. But `MaxBatchSize=500` is unsafe (500 × 70 KB ≈ 35 MB vs a + 4 MB gRPC cap) → **Task 19 must set `LocalDb:Replication:MaxBatchSize = 16`.** This is the one + firmly evidence-backed number. +- **D4 is wrong.** `native_alarm_state` is not "unbounded by design" — `NativeAlarmActor` + coalesces per `SourceReference` on a 100 ms flush (`NativeAlarmActor.cs:473-502`), so + worst case is `distinct_source_refs × 10/sec`. +- **`sf_messages` has a hard 50 rows/sec ceiling** — `SweepBatchLimit` (500) ÷ + `RetryTimerInterval` (10 s). A ceiling, not an estimate. +- **Task 1 step 7's stop condition is much weaker than written.** Exceeding the oplog caps + prunes and flags `needs_snapshot` → graceful snapshot resync (`OplogStore.cs:109-138`, + `MaintenanceBackgroundService.cs:57`). Not data loss, not a wedge. + +The plan's own Task-1 method also needed four corrections (metrics sidecar, safe sampling, +no alarm generator on this rig, template 4 undeployable) — all recorded in the soak doc §1. + +--- + +## Rig operational gotchas (hard-won; not obvious from the repo) + +- **`ExternalSystem.Call` does NOT buffer to store-and-forward — only `ExternalSystem.CachedCall` + does.** Using `Call` gives HTTP traffic and zero `sf_messages` rows. This is the single most + important detail for generating S&F load. +- **Never** query the bind-mounted SQLite files from the macOS host while containers run. Use + the plan's `snap` helper, or `docker run --rm -v