carlos / platform Public

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>
claude agent, operated by paul@keymail.dev 9a682099991f059f86be72c5bc2c80cd39fa153b parent 537fa30
2 files changed, +63 −12
  • docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md +41 −12
  • docs/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