spec: zero-downtime deploys rev 9 — answers Astra round 8 (sidecar adoption leg, watch completion bound to the build)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+80
−21
docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md+61 −21docs/superpowers/specs/CODEX-FINDINGS-ZERO-DOWNTIME-R8.md+19 −0
CI passed — 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 8746767e7..77b9f9058 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 8, 2026-10-07. Discussions #231 (deploy pickup) and #230 |
| +Status: draft rev 9, 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 |
| @@ -422,20 +422,34 @@ requests, and two copies could do the same work twice. They keep |
| stop-then-start, on the respawn-on-same-entry path |
| (`respawnWorkerLocked`), with the readiness and failure rules in §6.5. |
| -**Native sidecars keep their own install path.** `Adoptable` admits only |
| -`Kind=instance`, so root doesn't adopt them. `ExecBackend.StartWorker` |
| -fetches their build from the signed channel itself, and the sweep's |
| -converge decision (`activator.go`) notices drift. That stays as it is. The |
| -only change is that the converge decision is **queued as a `release` |
| -attempt** with the scheduler instead of respawning at once. The pacing, |
| -the canary and the failure records then apply to sidecars like any other |
| -host. |
| - |
| -For a sidecar, the **desired build** is what `StartWorker` resolves from |
| -the channel, and the **serving build** is the version the running worker |
| -reports. These are kept apart: the worker-reported version no longer |
| -overwrites registry `Version` as the installed build, and is recorded as |
| -serving. |
| +**Native sidecars gain an adoption leg.** Today nothing moves a sidecar's |
| +build forward: |
| +- `Adoptable` admits only `Kind=instance` (`internal/release/adopt.go`); |
| +- `StartWorker`'s `ensureLiveLink` returns as soon as a live link |
| + resolves; |
| +- the sweep compares the live link with `spawnVersion`, never with the |
| + channel. |
| + |
| +So root's adoption pass gains a second admission predicate, |
| +`SidecarAdoptable`: `Kind=sidecar`, a known app, not a platform route, and |
| +ready. `Adoptable` itself is not widened, so its other callers are |
| +untouched. |
| + |
| +Sidecars go through the same signed-pointer selection, bake holds, |
| +staging, repoint and stamp as instances. They follow the channel on their |
| +own host row, as `sidecar_reconcile.go` provisions it. Cold |
| +materialization of a new sidecar uses the same leg. |
| + |
| +Once root has repointed a sidecar, the sweep's existing drift check (live |
| +link versus `spawnVersion`) sees the change. That check is now **queued as |
| +a `release` attempt** with the scheduler, instead of respawning straight |
| +away, so pacing, the canary and failure records apply to sidecars as to |
| +any host. |
| + |
| +For a sidecar, the **installed build** is what root stamped. The |
| +**serving build** is the version the running worker reports. These are |
| +kept apart: the worker-reported version is recorded as serving and no |
| +longer overwrites registry `Version`. |
| ### 7.10 Scheduled work during an overlap |
| @@ -628,8 +642,23 @@ strings here describe meaning, not proposed copy. |
| their good build. It ends when every box is done or halted, or at its |
| existing deadline. |
| - **A box is done when its scheduler has no queued or running attempt and |
| - no open canary or settle observation for the app.** Each sleeping |
| + **Completion is tied to the build being watched.** The watch knows the |
| + build B it promoted and the instances that follow that channel. Each one |
| + reaches a final state: |
| + - **swapped**: it serves B, its swap passed the health check, and, if it |
| + was the canary, the settle; |
| + - **installed, awaiting first wake**: root's stamp of B has been |
| + observed for a sleeping instance; |
| + - **failed**: a failure record for B, or a `hold` failure; |
| + - **held**: a bake hold, with the time it lifts, or another instance's |
| + failure record on that box. |
| + |
| + An instance whose box hasn't yet observed B installed is **pending**, |
| + however idle its scheduler is. |
| + |
| + **A box is done when every instance it hosts has reached a final state |
| + and the scheduler has no queued or running attempt, and no open canary |
| + or settle observation, for the app.** Each sleeping |
| instance moved cold is counted as **installed, awaiting first wake**. It |
| doesn't hold the watch open, because no traffic may reach it for hours, |
| and the watch reports it as such. If that first wake later fails, it |
| @@ -702,12 +731,16 @@ keep timer names stable across the two builds. Otherwise deploy with |
| - **Swap mode source:** a staged copy without `swap`, from an older root, |
| can't produce an overlap for a `hold` build. An unreadable canonical |
| manifest runs in `hold`. |
| -- **Sidecars:** a channel advance queues a paced `release` attempt. The |
| - worker-reported version is recorded as serving, not installed. |
| +- **Sidecars:** `SidecarAdoptable` admits sidecars, and `Adoptable`'s |
| + other callers are unchanged. A channel advance is staged, repointed and |
| + then queued as a paced `release` attempt. A new sidecar materializes. |
| + The worker-reported version is recorded as serving, not installed. |
| - **Failure-hold scope:** after a cohort failure, an instance already |
| serving the build still completes `config` and `restart` attempts. |
| -- **Deploy-watch completion:** sleeping instances are reported as awaiting |
| - first wake and don't hold the watch open. An open canary settle does. |
| +- **Deploy-watch completion:** an idle scheduler on a box that hasn't yet |
| + observed B installed is pending, not done. Sleeping instances are |
| + reported as awaiting first wake only after B's stamp is observed. An |
| + open canary settle holds the watch open. |
| - **Scheduler:** per-instance desired builds across cohorts, canary gating, |
| `swap_concurrency`, halting on the first failure, repoint-back to the |
| good build, supersession and re-checks, attempt-identity dedupe, the |
| @@ -889,3 +922,10 @@ answer for each. |
| | 3 | A cohort failure blocks config on hosts already serving the build | §8.4: the hold applies only to hosts that haven't served the build | |
| | 4 | Generation paths exceed the socket length limit | §7.1: bounded flat path, at most 60 bytes | |
| | 5 | Deploy-watch completion | §9.1: done comes from scheduler state; cold installs are reported as awaiting first wake | |
| + |
| +### Round 8 |
| + |
| +| # | Finding | Answer | |
| +|---|---------|--------| |
| +| R7-2 (open) | Sidecars have no channel resolution | §7.9: a `SidecarAdoptable` leg in root's pass, with the same signed selection, bake, staging and materialization; the drift check then queues a paced attempt | |
| +| R7-5 (partial), 1 | An idle scheduler reports done before B is observed | §9.1: completion per targeted instance, tied to the watched build: pending until B's install is observed, then a final state | |
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-ZERO-DOWNTIME-R8.md b/docs/superpowers/specs/CODEX-FINDINGS-ZERO-DOWNTIME-R8.md| new file mode 100644 |
| index 000000000..2f02be5bf |
| --- /dev/null |
| +++ b/docs/superpowers/specs/CODEX-FINDINGS-ZERO-DOWNTIME-R8.md |
| @@ -0,0 +1,19 @@ |
| +REJECT |
| + |
| +R7-1: **CLOSED** — §4.2 requires the canonical manifest and defaults unreadable policy to `hold`. |
| +R7-2: **OPEN** — §7.9 relies on an installation path that does not exist; details below. |
| +R7-3: **CLOSED** — §8.4 permits config and restart attempts on hosts already serving the build. |
| +R7-4: **CLOSED** — §7.1 bounds generation and control socket paths. |
| +R7-5: **PARTIAL** — Sleeping instances and outstanding settles are addressed, but the replacement completion rule permits success before adoption starts; new finding 1 below. |
| + |
| +**R7-2 — HIGH, §7.9:** [`StartWorker` calls `ensureLiveLink`]( /home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/activator/backend_exec.go:1358), which [returns immediately when the existing link resolves](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/activator/backend_exec.go:794). It does not fetch the channel head. The [sweep compares the live link against `spawnVersion`](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/activator/activator.go:2592), not against the channel. A sidecar running A therefore sees no drift when its channel advances to B, and never queues the promised release attempt. With no existing link, materialization still reaches the pass whose [admission predicate excludes sidecars](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/adopt.go:830). **Smallest fix:** Specify an actual sidecar channel-resolution and staging leg, using the existing signature and bake rules, that supplies the scheduler’s target independently of live-link drift. |
| + |
| +Earlier in-scope closures remain closed: §§8.1–8.2 explicitly select staged fallback without repointing and cover sleepers moved cold by root; §8.5 separates and pins `GoodVersion`; §§7.7 and 14 retain stage-marker retransmission. Removing the agent→root request machinery does not reopen those failures. |
| + |
| +1. **MEDIUM — §§6.1, 9.1: An idle scheduler can report a deployment complete before it observes that deployment.** |
| + |
| + **Code:** [`deploy.go:419`](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/deploy.go:419) currently watches the specific requested version. The convergence leg [waits for root’s installation stamp to match the live link](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/host/agent.go:1816). |
| + |
| + **Failure:** The CLI promotes B while every instance remains installed and serving A. Before root stages and stamps B, §6.1 supplies no new scheduler attempts. Every scheduler has no queued work, running work, or open observation—precisely §9.1’s definition of “done.” The watch can therefore report completion without any instance having installed B. |
| + |
| + **Smallest fix:** Bind completion to the watched build/promotion and targeted instances. Require each target to have observed that installation or reported a terminal failure before applying the scheduler-idle test. Count a sleeper as “installed, awaiting first wake” only after its watched installation is observed. |
| \ No newline at end of file |