spec: zero-downtime deploys rev 13 — answers Astra round 12 (recovery honours current app hold; claims-off admission path)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+63
−12
docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md+41 −12docs/superpowers/specs/CODEX-FINDINGS-ZERO-DOWNTIME-R12.md+22 −0
CI cancelled — superseded by a newer push — run details
diff --git a/docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md b/docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md| index dfc14e673..8f1cff8b3 100644 |
| --- a/docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md |
| +++ b/docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md |
| @@ -1,6 +1,6 @@ |
| # Zero-downtime deploys: overlap swaps and paced rollout |
| -Status: draft rev 12, 2026-10-07. Discussions #231 (deploy pickup) and #230 |
| +Status: draft rev 13, 2026-10-07. Discussions #231 (deploy pickup) and #230 |
| (per-app restart pace, first-route health step) feed into this. Astra |
| rejected rev 1 with 17 findings, rev 2 with 12 new ones, rev 3 with 7, rev 4 |
| with 3 and rev 5 with 4. The findings files are |
| @@ -149,9 +149,12 @@ fallback to the good build, worker recycles, and `restart` or `config` |
| attempts onto the serving build. All of these run an artifact this box |
| has already verified and run. The good-build record (§8.5) and each |
| generation's captured identity keep three things: the staged path, the |
| -sha256, and the swap mode that applied when it passed. Recovery checks the |
| -staged file against that **retained digest** and uses that **retained |
| -mode**. A later re-ship of the same name, with a different canonical |
| +sha256, and the **release's own demand**. That demand is the manifest's |
| +`swap` field as verified when the build passed, not the effective mode of |
| +that attempt. Recovery checks the staged file against that **retained |
| +digest**. It runs in `hold` if either the retained release demand **or the |
| +app's current policy** says `hold`, so an app switched to `hold` later |
| +still gets `hold` on its restarts and config changes. A later re-ship of the same name, with a different canonical |
| manifest, never revokes local recovery. If the retained digest doesn't |
| match the file, the staged copy has been damaged. The recovery is refused |
| and reported, which is what any failed wake does today. |
| @@ -383,13 +386,25 @@ retiring generations. Each is tracked until its termination is proven. |
| (`ErrStillRunning`), ownership and the lease are kept, as today. |
| - `Shutdown` covers entries that are waking or swapping. Today it skips |
| waking entries. |
| -- Before a spawn and before a switch, the attempt re-checks, under the |
| - host's guard (`hostguard.With`), that the entry is still awake, that the |
| - lease is still held, that its attempt is still the newest (§6.3), and |
| - that no claim operation holds the guard. If any check fails, the |
| - successor is abandoned. |
| -- A swap never starts while the host's guard is held for an operation that |
| - needs stop evidence. |
| +- Before a spawn and before a switch, the attempt re-checks four things: |
| + the entry is still awake, the lease is still held, its attempt is still |
| + the newest (§6.3), and no stop-evidence operation is in progress. If any |
| + check fails, the successor is abandoned. |
| + |
| + **How it checks depends on whether host claims are on.** It goes through |
| + the existing lifecycle-aware abstraction (`internal/hostwrite`), and |
| + never calls `hostguard.With` directly: |
| + - **Claims off** (`CARLOS_HOST_CLAIM` unset, which is today's |
| + production): `hostwrite`'s legacy work. The checks run under the |
| + entry lock, against the entry's barrier, the lease, and the local |
| + fences that maintenance and moves already set (`FenceAndStop`, the |
| + move pass). That is how those operations exclude wakes today. |
| + - **Claims on:** `hostwrite` selects the claim-backed work, which also |
| + validates the claim and the installation stamp (`hostguard`). |
| + - **A failed claims-on check abandons the successor.** It is never |
| + treated as permission to take the claims-off path. |
| +- A swap never starts while a stop-evidence operation holds the host, |
| + whichever of the two mechanisms that operation uses. |
| ### 7.5 An overlap swap, step by step |
| @@ -674,7 +689,8 @@ Clearing: |
| ### 8.5 The good build |
| `GoodVersion` is a new registry column, with two companions: the good |
| -artifact's sha256 and its swap mode at the time it passed (§4.2). It is set when a generation on |
| +artifact's sha256 and the release's own `swap` demand as verified when it |
| +passed (§4.2). It is set when a generation on |
| build B becomes current after passing its health check. For a canary, that |
| means after the settle; for a host with no socket, it means when it is |
| ready (§6.5). It is written with an expected-current condition. Fallback |
| @@ -813,6 +829,12 @@ keep timer names stable across the two builds. Otherwise deploy with |
| - **Restage refusal:** a reused version name with different bytes is not |
| restaged over a copy that a good build or a live generation refers to. |
| It is refused and reported. An unreferenced stale copy is restaged. |
| +- **Recovery mode:** an artifact that passed under `overlap`, on an app |
| + since switched to `hold`, gets `hold` on its `restart` and `config` |
| + attempts. |
| +- **Claims off and on:** swaps work on a box with `CARLOS_HOST_CLAIM` |
| + unset, with maintenance and move fences still excluding them. With |
| + claims on, a failed claim or stamp check abandons the successor. |
| - **Recovery identity:** a good build whose version name was re-shipped |
| with different bytes still falls back, checked against its retained |
| digest. A damaged staged copy refuses recovery and reports it. |
| @@ -1046,3 +1068,10 @@ answer for each. |
| |---|---------|--------| |
| | 1 | The canonical digest check forbids the retained good build | §4.2: the canonical check applies to adoption only; recovery checks the retained digest and mode (§8.5) | |
| | 2 | Legacy co-spawned sidecars run the failed installed build beside the fallback | §7.5: every legacy sidecar spawn execs the current generation's captured path, inside the operation set | |
| + |
| +### Round 12 |
| + |
| +| # | Finding | Answer | |
| +|---|---------|--------| |
| +| 1 | Retained mode overrides a later app-level `hold` | §4.2, §8.5: the release's own demand is retained, not the effective mode; recovery is `hold` if either that or the current app policy says so | |
| +| 2 | Mandatory `hostguard.With` breaks claims-off boxes | §7.4: checks go through `hostwrite`, with explicit claims-off and claims-on paths; a failed claims-on check never falls through to the claims-off path | |
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-ZERO-DOWNTIME-R12.md b/docs/superpowers/specs/CODEX-FINDINGS-ZERO-DOWNTIME-R12.md| new file mode 100644 |
| index 000000000..ff8915c47 |
| --- /dev/null |
| +++ b/docs/superpowers/specs/CODEX-FINDINGS-ZERO-DOWNTIME-R12.md |
| @@ -0,0 +1,22 @@ |
| +REJECT |
| + |
| +R11-1: **CLOSED** — §4.2 validates recovery against the retained artifact identity instead of the replacement canonical manifest. |
| +R11-2: **CLOSED** — §7.5 binds all four legacy sidecar spawn paths to the serving generation. |
| + |
| +The split/removal has not reopened fallback without repointing, channel-installed failure holds, cold-sleeper recovery, `GoodVersion` versus `PrevVersion`, or stage-trigger retransmission. |
| + |
| +1. **HIGH — §§4.1–4.2, 6.4, 8.5: Retaining the effective swap mode can override a later app-level `hold`.** |
| + |
| + **Code:** [internal/appconfig/syncexec.go:1302](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/appconfig/syncexec.go:1302) replaces configuration before requesting a bounce. [internal/activator/backend_exec.go:849](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/activator/backend_exec.go:849) reads configuration afresh for the replacement; [backend_exec.go:942](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/activator/backend_exec.go:942) executes it against the existing database. |
| + |
| + **Failure:** A passed under `overlap`. The owner subsequently selects app policy `hold`, then changes startup configuration or requests a restart. Revision 12 classifies these same-build attempts as recovery and requires their **retained mode**, still `overlap`. The replacement therefore runs beside the incumbent despite the current `hold` policy. Configuration can change startup migrations or exclusive-access requirements without changing executable bytes. This contradicts §6.4’s requirement that every attempt honor the app’s policy. |
| + |
| + **Smallest fix:** Retain the artifact’s release-level demand separately from the effective attempt mode. Recovery must use `hold` when either the retained release demand **or the current app policy** requires it. Canonical-manifest replacement must remain irrelevant to retained-artifact recovery. |
| + |
| +2. **HIGH — §§7.4, 15: The mandatory `hostguard.With` checks prevent swaps on ordinary, claim-disabled boxes.** |
| + |
| + **Code:** [internal/hostguard/guard.go:88](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/hostguard/guard.go:88) refuses a missing claim reader; [guard.go:101](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/hostguard/guard.go:101) refuses an absent claim. `With` additionally requires a matching active installation stamp. Existing callers preserve claim-disabled operation through [internal/hostwrite/write.go:33](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/hostwrite/write.go:33). Moreover, the production claim-provider factory currently returns unavailable at [internal/host/agent.go:209](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/host/agent.go:209). |
| + |
| + **Failure:** Enable `CARLOS_SWAP=overlap` on an otherwise ordinary box with `CARLOS_HOST_CLAIM` unset. Root installs B while A serves. Section 7.4 requires `hostguard.With` before spawning and switching, but this box has neither the required claim infrastructure nor installation records. Every queued swap is refused; enabling claims instead prevents the current production agent from starting. |
| + |
| + **Smallest fix:** Define both admission paths explicitly: retain the entry barrier, local fences and lease checks in claim-disabled mode; add claim/stamp validation through the existing lifecycle-aware abstraction when claims are enabled. Never treat a failed enabled-mode claim check as permission to use the legacy path. |
| \ No newline at end of file |