The 4th hard-reset failure had a different signature, verified not assumed: all of rallly healthy, papra missing entirely with state=stopped deployed=True containers=0. Zero containers is exactly what bootrecon deliberately never touches, because the UI's Stop is compose down which removes containers - but a hard reset landing during a compose operation produces the identical state. The signature the safety rule depends on cannot distinguish the two. Worse: in that state the deadapp check reported 0 currently down while a deployed app was not running. No app_start_failed, no banner. That is the workspace's own false-invariant #4 (F-CRIT-1, StateStopped assumed deliberate) recurring through a hard reset rather than quiesce. NOT filed as new - CLAUDE.md already records it - but confirmed live on 0.188.0 via a new path. papra returned after ~15 min, later than the harness's 10-min window, so this instance was slow rather than permanent and the doc says so. What restarted it is not established. Mechanism A (Exited, missed by the unsettled snapshot) alarms; mechanism B (zero containers) is invisible on every channel. A settle-condition fix closes A only.
9.0 KiB
R-157 — bootrecon's start-ONCE sweep acts on an unsettled snapshot, so the boot orphan it exists to recover survives
INTERMITTENT — 3 failures in 6 hard resets (~50%), across two independent runs. Controller 0.188.0, agent 0.119.0.
Correction to this document's first version. It said the failure reproduced "at the same cycle in both runs — deterministic, not a coincidence". That was wrong, and the correction matters because it changes what kind of bug this is. The cycle numbers matched only because the runner's RNG is seeded, so both runs drew the same permutation; the failure is a coin flip. Run 2b's four hard resets went PASS (c2) → FAIL (c10) → PASS (c18) → FAIL (c26), and run 2a went PASS (c2) → FAIL (c10). Intermittency is what a race predicts — a wrong predicate would fail every time — so this strengthens the root cause below rather than weakening it.
R-52 built internal/bootrecon for exactly this failure (audit F5: immich-server and calibre-web
left Exited after an interrupted boot, ten siblings back, those two still down ~18 h later —
"the controller REPORTED them (the 30 s deadapp-check) but never started them"). This is that
failure recurring through a timing hole in the fix.
What happened
Cycle 10's permutation ran hard_reset_mid_write — a qm reset of the VM during a backup.
Everything came back except the app half of the DB-backed stack:
rallly |
Exited (255), oom=false, no error string, restarts=0 |
rallly-postgres |
Up (healthy) |
| every other app | back and healthy, both drives bound |
| rallly's own last log line | ▲ Next.js 16.2.6 … ✓ Ready in 0ms — it died healthy |
The controller start-up sequence, verbatim:
20:28:13 [stacks] ScanStacks complete: 55 stacks found (5 deployed, 50 available)
20:28:13 [stacks] Status refresh: 8 containers across 55 stacks
20:28:18 [bootrecon] Boot reconciliation: no boot-orphaned apps (nothing to start)
20:28:24 [stacks] Status refresh: 8 containers across 55 stacks
20:28:25 [stacks] Status refresh: 7 containers across 55 stacks <-- still churning
20:28:25 [stacks] Status refresh: 8 containers across 55 stacks
20:29:44 Event pushed: app_start_failed (warn) — Telepített alkalmazás nem fut: Rallly
20:39:14 [deadapp] check alive: 20 scans since boot, 5 deployed app(s) evaluated, 1 currently down
docker ps -a reports 9 containers; every Status refresh reports 8. The controller's boot-time
snapshot is one short, and the missing one is the exited rallly.
Why this is a defect and not the documented safety boundary
bootrecon deliberately never touches a stack the customer stopped, and the predicate for that is
sound (bootrecon.go:100):
len(s.Containers) > 0 && stacks.IsDownState(s.State)
Both terms hold for this stack once the system settles — the controller's own API now reports:
rallly state=degraded deployed=true containers=2
rallly exited Exited (255)
rallly-postgres running Up (healthy)
IsDownState includes StateDegraded (manager.go:55), and len(Containers) is 2. So the predicate
was never the problem. The snapshot was. bootrecon fires as go runBootReconcile(...) at
cmd/controller/main.go:236, ~5 s after start, while docker is still bringing containers back — and
it is start-once by design, so it never re-evaluates.
Two mechanisms are consistent with the evidence and this report does not claim to distinguish them:
- the exited container was not yet enumerated, so the stack looked
running(1/1) rather thandegraded(1/2) — supported by the 8-of-9 count; or - the stack's state had not yet aggregated to
degradedat that instant.
Either way the root is the same: a single observation taken before the system settles, with no re-check.
Consequence
The app stays down indefinitely. Detection works perfectly and recovery never happens — the
deadapp check correctly counted 1 currently down across 20 scans / 10 minutes, and
app_start_failed reached the customer. In run 2a the app only returned because a later campaign
atom happened to redeploy the stack; nothing in the product recovered it.
This is the R-52 failure shape exactly: an alarm with no recovery.
Why it is not a harness artifact
The preceding two violations in this campaign were harness defects and are documented as such
(run2a-violations-were-harness.md). This one is not:
- both drives were bound (
adatok: True,mentes: True), so no drive gate stopped anything; - every other app returned, including
calibre-web, whose data is on an enrolled drive; - only the app half of a two-container stack is missing, while its database is healthy;
- the fixed harness reported it through the check written for precisely this ("apps did not return within 10 min — that IS the failure"), not through a seed or canary path;
- it recurred 3 times out of 6 hard resets with an identical signature every time:
ralllyExited (255)withrallly-postgreshealthy,bootreconreporting "no boot-orphaned apps" ~5 s after start, and the container count still churning after the sweep. Third occurrence (2026-08-02 01:05):Status refresh8 → bootrecon at 01:05:13 → refresh 8 → 7 → 8 at 01:05:19-20.
Shape of a fix — not applied (the fences forbid it, and a fix mid-run proves a version that did not exist)
The bounded start-once property is the safety argument and should be kept. What is missing is a
settle condition before the one observation: wait for the container set to stop changing (or for
docker to report ready) before the sweep, or re-evaluate once at the end of deadAppBootGrace and
recover then, still bounded to the same total attempts. A test would pin the consequence — an app
left Exited by an interrupted boot is running again before the grace expires — rather than the
mechanism, per the workspace rule about invariants needing a test that fails when the dependency moves.
Register
R-52 is the feature; grep found no existing item covering its timing. R-156 is this campaign's
papra finding. R-157 is free and is claimed here.
Second mechanism, observed 2026-08-02 ~05:25 (run 2b cycle 34) — the zero-container case
The 4th hard-reset failure had a different signature, and it matters because it is not a race at all. Verified rather than assumed (the two preceding non-R-157 violations in this campaign turned out to be harness artifacts, so each occurrence is now checked):
all of rallly healthy this time; papra MISSING ENTIRELY
papra: state=stopped deployed=True containers=0
bootrecon 03:14:34: "no boot-orphaned apps" (refresh 7 → 6 → 7 after it, same churn)
containers=0 is precisely the state bootrecon deliberately never touches — its documented safety
rule is "a stack with ZERO containers is deliberately never touched", because the UI's Stop is
docker compose down, which removes containers. But a hard reset landing during a compose
operation produces the identical zero-container state. The signature the safety rule relies on
cannot distinguish "the customer stopped this" from "an interruption caught it between containers".
And in this state the app is SILENT, not merely unrecovered
[deadapp] check alive: 20 scans since boot, 5 deployed app(s) evaluated, 0 currently down
papra was deployed=True and not running, and the deadapp check counted 0 down. No
app_start_failed, no banner. This is the workspace's own false-invariant #4 recurring —
"StateStopped means deliberately stopped by the user" (F-CRIT-1), reached here through a hard
reset rather than through quiesce. Not filed as new: CLAUDE.md already records it. Confirmed
live, on controller 0.188.0, via a new path.
That makes the zero-container case worse than the Exited case this document opens with: the Exited
one at least alarmed (app_start_failed, 1 currently down); this one is invisible on every channel.
Recovery in this instance
papra did come back roughly 15 minutes after the reset — later than the harness's 10-minute window,
which is why the violation fired. So this instance was slow, not permanent, and the document does
not claim otherwise. What is not established is what restarted it: the campaign's own later atoms
are a candidate, and nothing in the logs attributes the recovery to a product mechanism.
Summary — two mechanisms, one root
| signature | bootrecon's view | alarmed? | observed | |
|---|---|---|---|---|
| A | container Exited (255), state=degraded |
missed — snapshot taken before settle | yes (app_start_failed, 1 down) |
3× |
| B | zero containers, state=stopped |
deliberately skipped — matches a user Stop | no — 0 currently down |
1× |
Both leave a deployed: true app not running after an interrupted boot, which is the exact condition
R-52 exists to eliminate. A fix that only adds a settle condition closes A and leaves B open.