gate: the boot bind gate honours a customer's Stop (R-55, v0.157.0)

shouldRecreateOnBoot keyed on Deployed+drive-present alone. Deployed stays
true across a Stop, so a drive-backed app the customer switched off was
silently restarted on every guest reboot (proven live: immich).

Requires len(Containers)>0 as well - R-52's existing-Exited vs absent
distinction. A UI Stop is compose down and removes the containers; a guest
that went down under a running app leaves them. Container STATE is still
deliberately NOT a filter: that would miss a not-yet-restarted or stuck-Exited
app, which is the bug the boot-id path exists to fix.

Evidence sampled before any recreate - recreate's own StopStack erases it.
Honoured Stops counted and logged separately from no-live-bind skips.
This commit is contained in:
2026-07-21 14:53:11 +02:00
parent 83f20c8293
commit ac3790a11b
4 changed files with 200 additions and 41 deletions
+14
View File
@@ -1113,6 +1113,20 @@ not just those with HDD data. Non-HDD apps can configure destination, method, an
> covered; drives that never go live in the window are left to the drive-absent gate. `processGuestBootChange`
> also runs on every periodic `driveGateLoop` tick now (idempotent, boot-id gated) so a momentarily-unreachable
> agent right after a guest reboot no longer permanently strands recovery.
> **v0.157.0 — R-55, the gate now honours a customer's Stop.** Until this version the recreate keyed on
> `Deployed && HDD_PATH && drive-present` alone, so a drive-backed app the customer had deliberately
> Stopped was silently restarted on every guest reboot (proven live: immich, stopped from the UI seconds
> earlier, came back running). `shouldRecreateOnBoot` now also requires the app to still HAVE containers
> (`len(Stack.Containers) > 0`, from `docker ps -a`, so `Exited` ones count) — R-52's `existing-Exited vs
> absent` distinction (`bootrecon.isBootOrphan`) translated to this gate. A UI Stop is `compose down`,
> which REMOVES the containers; a guest that went down under a running app leaves them present. **State is
> still NOT a filter** — that part of the original design is load-bearing and unchanged; `hasContainers`
> answers a different question ("does docker still have records of it") which, unlike liveness, survives a
> reboot as a statement of intent. The evidence is sampled BEFORE any recreate, because recreate's own
> `StopStack` erases it. Apps stopped by the drive-absent gate are also at zero containers and are likewise
> left alone here — they are restored by `ReconcileDriveGates`' `Return` branch from
> `StoragePath.StoppedStacks`, on the same loop tick. Honoured Stops are logged at INFO (`left stopped …`),
> counted separately from the "no live bind" skips so an intended outcome never fires a WARN.
> **Agent-path prerequisite (also v0.71.0):** the whole drive gate needs `cfg.LocalAPI.Endpoint` (the
> per-guest agent local API). `bootstrap.MaybeIngest` now calls `ensureLocalAPI` on the already-configured
> path — merging `local_api` from `bootstrap.json` into an existing controller.yaml that lacks it (seeded