# REPORT — F-CRIT-1 + F-A1 fixed (controller v0.179.0, 2026-07-28) Docs here. Implementation, all six red-proofs and the full live replay live in `felhom-controller/REPORT.md`. The campaign that found both: `documentation/audits/CAMPAIGN-8-backup-restore-2026-07-27.md`. ## What changed **F-CRIT-1** — an app that failed to restart after a quiesce never alarmed, for two independent reasons, either of which alone kept it dead: `restartAll` returned nothing (the failure was logged and dropped), and `classifyRunStates` whitelisted `StateStopped` on invariant I1 ("the user stopped it") — which the quiesce loop had made false by stopping stacks the same `docker compose down` way. A failed restart and a user stop are the *same* Docker state; the only difference is that the loop tried and could not, now surfaced by `Loop.FailedRestarts()`. **F-A1** — HTTP 409 is the agent's single-flight gate refusing while a restore-test holds it, not a failure. It is now contention: the tier stays DUE, is dropped before anything stops, and unending contention raises its own **BLOCKED** signal rather than going silent. ## Bounds, justified against measured reality - `contentionRetryAfter` **15m** — longest restore-test observed on the fleet is 12m01s; the agent's local restore-test wait is 10m. Caps app-stop churn at 4/hour instead of 12/hour. - `contentionAlarmAfter` **3h** — the agent's own PBS restore-test task is capped at 120 minutes, so contention outliving that is a *stuck* gate, not a busy one. 3h adds margin and is 15× the longest contention actually observed. ## Verified live, with the hub DB as arbiter — not from logs Same box, same day, same event type; the only difference is 409 versus a genuine error: | injection | operator emails (demo-hp) | |---|---| | **409 contention** | 8 → **8** (none) | | **real transport failure** | 8 → **9** | And for F-CRIT-1: the failed restart alarmed **9 seconds** after grace expiry with the dashboard banner naming the `(stopped)` state, while a **deliberate** user stop on the same box stayed silent through **9** dead-app scans (the positive observable that the silence is suppression, not a dead detector). ## The rule this arc earned Added to **both** copies of `CLAUDE.md` (live + `documentation/runbooks/workspace-CLAUDE.md`): **a comment asserting an invariant needs a test pinning it, or it is a wish.** Six instances in this project have shipped guarantees the code did not provide — `EffectiveProtected`, `newestArchiveOn`, the R-97a operator-only claim, `classifyRunStates`' I1, `inflight.go`'s defer claim, and `quiesce.go`'s spurious-failure claim. Two were found only on live hardware, and one of those had a green, red-proofed test suite over a production path broken two independent ways. Corollary recorded with it: prefer a test that asserts the **consequence** (does the alarm fire?) over one that asserts the **mechanism** (does suppression expire?). R-97b's Scenario F proved the mechanism; the consequence was still broken. ## Docs touched - `documentation/backlog/OPEN-ITEMS.md` — F-CRIT-1 and F-A1 → SHIPPED + PROVEN-LIVE. - `documentation/audits/CAMPAIGN-8-backup-restore-2026-07-27.md` — both rows struck through, closing section added. **All three of the campaign's alarm findings are now closed** (F-CRIT-1, F-CRIT-2, F-A1). - `documentation/runbooks/workspace-CLAUDE.md` — the invariant rule. ## Still open, highest first **Fault 4** (restic transport interruption) — four injection approaches were defeated by guest-bridged networking, and it is now the most valuable follow-up: F-CRIT-2 answered the phantom question for PBS and left the identical question open for restic. Then **R-99** (prune never removes phantoms) and **F-LEAK** (a failed restore-test cannot destroy its own scratch guest — observed again during this work).