carlos / platform Public

spec: zero-downtime deploys rev 4 — answers Astra round 3 (7 new, 3 partial); restores the feature rollout section

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 81a232c32c7f60649d44553500dc41d6163c7d4d parent 29bd6c5
2 files changed, +268 −56
  • docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md +196 −56
  • docs/superpowers/specs/CODEX-FINDINGS-ZERO-DOWNTIME-R3.md +72 −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 a5410e08e..8606a4ce4 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,10 +1,10 @@
# Zero-downtime deploys: overlap swaps, update settings, paced rollout
-Status: draft rev 3, 2026-10-07. Discussions #231 (deploy pickup) and #230
+Status: draft rev 4, 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 (`CODEX-FINDINGS-ZERO-DOWNTIME.md`) and
-rev 2 with 12 new ones (`CODEX-FINDINGS-ZERO-DOWNTIME-R2.md`), leaving two
-round-1 answers partial. §15 maps every finding to its answer.
+rev 2 with 12 new ones (`CODEX-FINDINGS-ZERO-DOWNTIME-R2.md`), and
+rev 3 with 7 new ones and 3 partials (`CODEX-FINDINGS-ZERO-DOWNTIME-R3.md`). §16 maps every finding to its answer.
## 1. Why
@@ -143,11 +143,12 @@ runs in `overlap`.
## 5. Target resolution
There is **one resolver**, `release.Desired(record, policy, heads,
-failures, good)`. Only the agent runs it. Root no longer decides anything.
-It only carries out the agent's exact per-host requests, and checks each
-one against an authorization it can verify itself (§7.7). No other path
-reads a channel pointer to make its own choice: not the root stage pass,
-not the hourly adopt loop, and not first-time materialization.
+failures, good)`. Only the agent runs it. For exec routes on a box where
+the protocol is active (§7.11), root no longer decides anything. It only
+carries out the agent's exact per-host requests, and checks each one
+against an authorization it can verify itself (§7.7). No other path reads
+a channel pointer to choose a build for those routes: not the root stage
+pass, not the hourly adopt loop, and not first-time materialization.
The resolver works in two steps.
@@ -168,10 +169,15 @@ installed build:
held until the pin is set again or the build is retried.
3. `major` is set and the candidate is not pinned, and the head's tag is
absent or has another major. The instance is *held at major N*.
-4. **The instance has no good build yet but does have an installed build**
- (§8.5). It stays where it is until a generation has served there.
- Instances that predate this feature therefore establish a baseline
- before any cold move.
+4. **Bootstrap.** The instance has no good build, its installed build is
+ *untested*, meaning no failure record matches it (§8.5), and the
+ candidate didn't come from a pin. The instance stays where it is until
+ a generation has served there. This way an instance that predates this
+ feature establishes a baseline before any cold move.
+
+ Hold 4 doesn't apply when the installed build has a failure record. An
+ instance whose only build never started moves to any other authorized
+ candidate, and that build's first wake is its canary.
An instance with neither an installed nor a good build is new. It resolves
to the candidate, ignoring `manual`.
@@ -180,7 +186,14 @@ If no hold applies, the desired build is the candidate.
**Installing by hand** is refused on a pinned instance, naming the pin.
Otherwise it moves the instance to its channel head, ignoring `manual`,
-`major` and bake holds, and clears the instance's failure hold (§8.4).
+`major`, bake holds and hold 4, and clears the instance's failure hold
+(§8.4).
+
+**Which routes this covers.** Settings, the resolver and the scheduler
+apply to exec routes only. Named-unit routes, the console's own route
+included, keep root's existing channel-driven adoption and its privileged
+restart, unchanged (§13). The console refuses update settings on a
+named-unit route, saying they aren't supported there yet.
## 6. Rollout and pacing
@@ -245,6 +258,15 @@ covers a deferred restart or config attempt, a hibernate, a move or park,
lease loss and `Shutdown`. The same rule applies to the canary settle in
§6.2.
+**Swap mode is carried by every attempt**, including first wakes and
+socketless replacements. It is `hold` if either the app policy or the
+manifest of the attempt's build says so (§4.3). **Once a `hold` build has
+executed against an instance's database, nothing falls back automatically
+on that instance.** That covers a failed health check, an exit during the
+settle, and a failed worker replacement. The build may have migrated past
+what the good build can read. The instance gets the §8.3 treatment
+instead: it stays down, the cohort halts, and recovery is explicit.
+
### 6.5 Processes without a socket
Workers and native sidecar members have no HTTP health check. For them:
@@ -256,9 +278,9 @@ Workers and native sidecar members have no HTTP health check. For them:
- **The good build** for such a host is set once it is ready.
- **No incumbent survives a stop-first replacement.** So a failed
`release`, `install` or `retry` attempt is treated as a failed first
- wake: the host's good build is spawned in its place, and the cohort
- fails (§8.1). A failed `restart` or `config` attempt respawns the build
- it replaced.
+ wake. In `overlap` mode, the host's good build is spawned in its place and
+ the cohort fails (§8.1). In `hold` mode, there is no fallback (§6.4). A
+ failed `restart` or `config` attempt respawns the build it replaced.
### 6.6 Across boxes
@@ -270,8 +292,13 @@ out of scope (§14).
### 7.1 Generations and app-socket consumers
-The activator entry gains a generation counter. Each spawn takes the next
-generation `n`:
+The activator keeps **one generation counter for the whole process** and
+never resets it. Each spawn of any instance takes the next value `n`. An
+entry is deleted when its instance stops, and a later wake creates a new
+one. Because the counter isn't per entry, that new entry can't reuse a
+number an earlier entry had. The edge cache lives in the same process, so
+a cached `(route, n)` pair can never match a later generation. Generation
+numbers are used as:
| Canonical (registry) | Generation path |
|----------------------|-----------------|
@@ -432,9 +459,12 @@ override `manual`, a pin or a failure hold.
The agent now writes **one request file per host** under its data dir:
`stage-requests/<host>.json`. The file holds the destination build, the
installed build it expects to replace, the host's incarnation, the attempt
-identity and a sequence number. It is rewritten in place, so a newer
-request supersedes an older one. Root becomes an executor and decides
-nothing (§5).
+identity and a sequence number. **The agent writes or replaces a request
+only while holding the host's guard**, using a temp file and a rename, so
+a newer request supersedes an older one. Root does its re-check and commit
+under the same guard (below). So a request can't be replaced between
+root's sequence check and its repoint. Root becomes an executor and
+decides nothing (§5).
**Authorization root checks for itself.** A request is only as trustworthy
as the sandboxed agent that wrote it, so root accepts destination B for
@@ -447,6 +477,15 @@ host H only when it can verify one of these:
- A **console-signed pin authorization** binds account, app, H, B and the
pin revision. The console signs it with the key that signs channel
pointers when a pin is set. It travels on the provision record.
+- B is in **root's own install ledger** for H. Whenever root installs a
+ build after verifying one of the two authorizations above, it keeps that
+ signed evidence in a root-owned directory, `/opt/carlos/auth/<host>/`,
+ which the agent can't write. Fallback to a good build is then accepted
+ even after the channel has moved more than ten releases on (history is
+ capped at ten, `internal/release/release.go`), after a channel change,
+ or after the pin that authorized it has been replaced. Ledger entries
+ are pruned along with the version sweep: an entry goes when its build
+ is no longer pinned for that host (§8.5).
A request that fails this check is refused, and the refusal is written as
its outcome. Being a shipped build of the right app is not enough.
@@ -476,8 +515,8 @@ sequence, the agent touches the marker again every `stageRequestEvery`
(15 s), through the existing throttle. It also rescans the directory when
it starts, so a restart doesn't lose pending requests.
-A box whose root binary predates this feature ignores the directory and
-behaves as it does today.
+Whether root acts on the request files at all is decided by the protocol
+handshake (§7.11).
### 7.8 A `hold` swap
@@ -496,18 +535,18 @@ in §6.5.
### 7.10 Scheduled work during an overlap
-Timers are written by host and name (`internal/schedule/control.go`), so
-two generations could overwrite each other. The rule is that only the
-current generation's schedule writes take effect:
-
-- **A successor's** `PUT` and `DELETE` on its control socket are recorded
- against its generation, and nothing is applied before the switch. At the
- switch they are applied in order, under the entry lock. A successor that
- fails has its record discarded, so a failed candidate never changes the
- live schedule.
-- **A successor's reads** (`GET /schedule`) see the live schedule with its
- own pending writes on top, so a boot-time "assert my timers" call can
- work out what it needs to change.
+Timers are written by host and name (`internal/schedule/control.go`). They
+are **shared state of the instance, like its database**, and the overlap
+contract (§2 decision 1) covers them the same way:
+
+- **A successor's** `PUT` and `DELETE` apply immediately, with today's
+ registration guarantee (`registration_fence`) unchanged. Deferring them
+ wouldn't help. The successor's database writes aren't deferred either,
+ so a deferred timer could be lost while the work it schedules was
+ committed. A successor that fails leaves its acknowledged registrations
+ in place, as its database writes stay. Builds that run side by side must
+ agree on timer names, just as they agree on schema. The migrations rule
+ in §9.5 says so.
- **A retiring generation's writes** are refused with 409 once the switch
has happened. Its reassert-at-shutdown behaviour
(`backend_exec.go`'s stop path) exists for hibernation, where the stopping
@@ -516,6 +555,40 @@ current generation's schedule writes take effect:
so the stopping generation is the current one, and its writes apply as
today.
+### 7.11 The agent–root protocol handshake
+
+The agent and root run the same binary, but at different times. Root runs
+`/opt/carlos/carlos` fresh on every stage pass. The agent keeps running
+whatever version it started with until it restarts. During a roll, or
+after a rollback, the two can disagree. The new behaviour turns on only
+when both sides agree.
+
+**The stamps.**
+- The agent writes `stage-protocol.agent` in its data dir, naming protocol
+ 2, with the time.
+- Root writes `stage-protocol.root` in a root-owned place the agent can
+ read, naming protocol 2, the agent stamp it saw, and the time.
+
+**What each side does.**
+- **Root** runs as an executor (§7.7) for exec routes only while the agent
+ stamp says 2 and is fresher than `CARLOS_HIBERNATE_IDLE`. Otherwise it
+ runs today's channel-driven adoption. Named-unit routes always get
+ today's adoption (§5).
+- **The agent** enforces update settings and requests exact repoints only
+ once the root stamp says 2 *and* names the agent stamp it is answering.
+ Only a root pass that started after the agent declared itself can have
+ written that, so no legacy pass that began earlier is still running.
+ carlos-stage.service runs one pass at a time.
+- **Until both sides agree**, the agent keeps today's behaviour: it
+ converges onto whatever root installs, with stop-then-wake. The console
+ shows the box as *settings not yet enforced*.
+
+**Rollback.** If the binary is rolled back below protocol 2, both sides go
+back to today's behaviour. Root adopts from the channel again, so `manual`
+and pins stop being enforced on that box. The console shows that state for
+each affected instance, and the roll docs name it. The overlap swap itself
+is also gated by the box flag (§15), independently.
+
## 8. Failure and rollback
### 8.1 A failed overlap swap
@@ -548,14 +621,26 @@ the cause:
### 8.2 A failed first wake (`overlap` mode)
-A first wake onto build B runs inside a **first-wake budget** of 55 s:
-- up to 30 s for B to become healthy;
-- up to 2 s to kill it;
-- up to 23 s to boot the good build.
+A first wake onto build B gets a **first-wake budget**. Requests waiting
+on it have a deadline of arrival + 55 s, instead of `wakeWaitBound`'s 30 s,
+but only for first wakes onto a new build.
-Requests waiting on that wake are held for up to 55 s instead of
-`wakeWaitBound`'s 30 s, but only for first wakes onto a new build. If B
-fails:
+**The budget, in order:**
+1. **The work before exec** runs once and is shared by any fallback:
+ taking the lease, restoring or seeding, and the guard checks. None of it
+ is repeated.
+2. **B's health check** runs until the earlier of exec + 30 s
+ (`spawnHealthTimeout`) and deadline − 25 s.
+3. **Killing B** takes up to 2 s.
+4. **The good build** has whatever remains.
+5. **Replication** (`lease.Replicate`) starts once the generation that
+ will serve is healthy, as it starts after spawn today.
+
+If the work before exec was slow and the fallback misses the deadline, the
+waiting requests get today's 503 with Retry-After. The fallback carries on
+booting and serves the next request.
+
+If B fails:
1. B's generation is killed outright with `cgroup.kill` on its child and
then waited on until empty. There's no SIGTERM drain, because it never
@@ -573,8 +658,10 @@ repoint, instead of booting the failed build again. That is a second line
of defence for a sleeper whose repoint hasn't landed yet.
An instance with no good build has nothing to fall back to and fails as a
-wake fails today. Rule 4 of §5 means that only happens on an instance's
-very first build. The settle rule for first wakes is in §6.4.
+wake fails today. §5's hold 4 means that only happens on an instance's
+very first build, and the next authorized build is free to replace it. In
+`hold` mode none of this falls back (§6.4). The settle rule for first wakes
+is in §6.4.
### 8.3 A failed `hold` swap
@@ -685,9 +772,11 @@ cause. It names the attempt's cause instead.
### 9.5 Docs
-The deploy docs gain the migrations rule for `overlap`: add in one release
-and remove in a later one, or deploy with `--swap hold`, or set the app to
-`hold`.
+The deploy docs gain the migrations rule for `overlap`. Two builds run
+side by side for a few seconds and share the instance's database and its
+scheduled timers. So add in one release and remove in a later one, and
+keep timer names stable across the two builds. Otherwise deploy with
+`--swap hold`, or set the app to `hold`.
## 10. Interactions
@@ -750,14 +839,29 @@ and remove in a later one, or deploy with `--swap hold`, or set the app to
other, and parent stop evidence. Include a memory-hog successor that
overruns its child limit and is OOM-killed while the incumbent keeps
serving.
-- **Root authorization and execution:** refusing an unauthorized shipped
- build, accepting a pin authorization and a history entry, superseding a
- request mid-download, recovering when root dies between the repoint and
- the registry write, and resending the marker while a request has no
- outcome, including across an agent restart.
-- **Scheduled work:** successor writes are deferred and then applied at the
- switch, discarded on failure, and visible to the successor's own reads.
- Retiring writes are refused after the switch.
+- **Root authorization and execution:**
+ - refuses an unauthorized shipped build;
+ - accepts a pin authorization, a history entry, and a ledger entry for a
+ good build that has left the channel's ten-entry history;
+ - handles a request replaced mid-download, with the agent's write and
+ root's commit both under the guard;
+ - recovers when root dies between the repoint and the registry write;
+ - resends the marker while a request has no outcome, including across an
+ agent restart.
+- **Handshake:** every pairing of new and old agent with new and old root,
+ including a legacy pass already running when the agent declares itself
+ and a rollback below protocol 2, leaves exactly one decider per exec
+ route.
+- **Scheduled work:** a successor's writes apply immediately with the
+ fence. A failed successor's acknowledged registrations survive. A
+ retiring generation's writes are refused after the switch.
+- **`hold` carried everywhere:** a `hold` build that fails after exec, on a
+ first wake, a settle or a worker replacement, never falls back.
+- **Bootstrap recovery:** an instance whose only build never started moves
+ to the next authorized build.
+- **Generation identity:** a stop, then a crash, then a wake never reuses a
+ generation number, and an aliased cache entry from the earlier entry
+ misses.
- **Lifecycle versus settle:** an intentional stop during the settle records
no failure.
- **Cache aliases:** a custom domain, a wildcard and a path-routed request
@@ -790,6 +894,11 @@ while address-backed routes have no socket. The scheduler (§6) is built to
take a third kind of attempt for them, and #230 stays open for that spec.
#230 item 3 (`-unit` at provisioning) is separate again.
+Until that follow-up, named-unit routes, the console's own route included,
+keep **today's adoption unchanged**. Root reads their channel, stages,
+repoints, restarts the unit with its rollback-on-failed-restart, and
+stamps the result. Nothing in this spec touches that path (§5, §7.11).
+
## 14. Out of scope
- A fleet-wide canary (one box first).
@@ -800,7 +909,24 @@ take a third kind of attempt for them, and #230 stays open for that spec.
marker while any request has no outcome, which closes the gap without
changing the unit.
-## 15. Findings and their answers
+## 15. Rollout of this feature
+
+The box-side swap lands **dormant** behind a box flag,
+`CARLOS_SWAP=overlap`. While the flag is off, the default, every attempt
+uses today's stop-then-wake. It is proven on dev and then on
+`bab/adopt-probe`.
+
+The resolver, the per-host requests and the handshake are not behind the
+flag. With every setting at its default, they produce today's result.
+
+The console's settings and policy can ship first. Until a box has
+completed the handshake, it shows *not yet enforced*.
+
+The changelog and the deploy docs tell implementers about the migrations
+rule (§9.5) before the flag goes on across the fleet. From then on, every
+exec app swaps in `overlap` unless it has chosen `hold`.
+
+## 16. Findings and their answers
### Round 1
@@ -842,3 +968,17 @@ take a third kind of attempt for them, and #230 stays open for that spec.
| 10 | Intentional stops during the settle count as failures | §6.4: lifecycle stops cancel the observation |
| 11 | The cache needs the serving-route identity | §7.2: entries tagged with the serving route and its generation, checked on insert and lookup |
| 12 | Request files still depend on a lost trigger | §7.7: the marker is resent while a request has no outcome; §14 |
+
+### Round 3
+
+| # | Finding | Answer |
+|---|---------|--------|
+| R2-4, R1-4 (partial) | Replacing a request doesn't take the lock | §7.7: the agent writes requests only under the host's guard, with temp file and rename |
+| R1-11 (partial) | The budget ignores lease, restore and replication | §8.2: work before exec is done once and shared; deadline arithmetic; replication starts after the serving generation is healthy |
+| 1 | Recovery can roll back a `hold` migration | §6.4: swap mode carried on every attempt, no automatic fallback once a `hold` build has executed; §6.5 |
+| 2 | The bootstrap hold traps a broken first build | §5 hold 4 applies only to an untested build; install by hand and pins waive it |
+| 3 | Mixed agent and root versions | §7.11: two-sided protocol stamps, the legacy-pass drain, rollback behaviour |
+| 4 | A good build loses its authorization | §7.7: root's own install ledger, pruned with the version sweep |
+| 5 | Generation numbers repeat across entries | §7.1: one counter for the whole process |
+| 6 | Discarded candidate timers strand work | §7.10: successor writes apply immediately; timers are shared state under the overlap contract |
+| 7 | Named-unit adoption has no path | §5, §13: named-unit routes keep today's root adoption unchanged |
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-ZERO-DOWNTIME-R3.md b/docs/superpowers/specs/CODEX-FINDINGS-ZERO-DOWNTIME-R3.md
new file mode 100644
index 000000000..9898ef34c
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-FINDINGS-ZERO-DOWNTIME-R3.md
@@ -0,0 +1,72 @@
+REJECT
+
+R2-1: CLOSED — Root must verify release or pin authorization.
+R2-2: CLOSED — Per-generation limits and OOM boundaries isolate the candidate.
+R2-3: CLOSED — Generation handles identify candidate and retiring sockets.
+R2-4: PARTIAL — Root locks its commit, but request replacement is not required to take that lock; supersession can still land after the sequence check.
+R2-5: CLOSED — Failure matching precedes pinned installation and includes the pin revision.
+R2-6: CLOSED — Cohort restoration and wake-time failure checks cover sibling sleepers.
+R2-7: CLOSED — Bootstrap is specified; its new recovery problem is below.
+R2-8: CLOSED — Candidate and retiring schedule mutations have explicit ownership rules.
+R2-9: CLOSED — Worker readiness, clean completion and replacement failure are defined.
+R2-10: CLOSED — Intentional lifecycle stops cancel settle observations.
+R2-11: CLOSED — Cache validation includes the serving route.
+R2-12: CLOSED — Pending requests cause marker retransmission across agent restarts.
+R1-4: PARTIAL — The remaining request-writer locking gap is the same as R2-4.
+R1-11: PARTIAL — The allocation includes termination, but still consumes all 55 seconds without accounting for lease acquisition, restore/materialization and replication startup.
+
+1. **HIGH — §§6.4, 6.5, 8.1–8.3: First-wake and worker recovery can roll back a `hold` migration.**
+
+ **Code:** [internal/activator/backend_exec.go:942](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/activator/backend_exec.go:942) starts the application against the instance database before [the health check at :989](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/activator/backend_exec.go:989). Worker execution likewise precedes readiness at [internal/activator/activator.go:1280](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/activator/activator.go:1280).
+
+ **Failure:** Sleeping H boots B, whose manifest demands `hold`. B migrates the database, becomes healthy, then crashes during its first-wake settle. §6.4 unconditionally says the next wake uses the good build A, although A may no longer read that database. §6.5 independently mandates fallback for failed worker replacements without checking `hold`. §8.3 protects only the described hold-swap path.
+
+ **Smallest fix:** Carry the effective swap mode through every first-wake, settle and worker failure transition. Once a `hold` candidate has executed, prohibit automatic fallback on that instance and retain the explicit recovery requirement.
+
+2. **HIGH — §§5, 8.5: The bootstrap hold prevents a broken initial build from receiving its replacement.**
+
+ **Code:** Cold adoption explicitly installs without health checking at [internal/release/adopt.go:883](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/adopt.go:883); failed startup returns without a successful process at [internal/activator/backend_exec.go:989](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/activator/backend_exec.go:989).
+
+ **Failure:** A new instance installs B, but B never becomes healthy. `GoodVersion` remains empty. The owner promotes fixed build C—or sets `pin=C`—and hold 4 still resolves to installed B. B must serve before the instance can install the build that would make it serve. The manual-install paragraph does not explicitly waive hold 4 either.
+
+ **Smallest fix:** Distinguish an untested bootstrap baseline from a baseline already proven unable to start. Define a recovery transition that permits a different authorized build, including an explicit pin/install, without falsely recording B as good.
+
+3. **HIGH — §§5, 7.7, 8.6: Mixed agent/root versions have no safe activation gate.**
+
+ **Code:** Root adoption independently chooses the channel head at [internal/release/adopt.go:942](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/adopt.go:942). The stage service starts its own binary invocation at [internal/host/units/carlos-stage.service:31](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/host/units/carlos-stage.service:31).
+
+ **Failure:** §7.7 explicitly allows an old root binary to ignore the request directory and continue its existing behavior. With a new agent enforcing `manual` at A, an old root pass installs channel head B anyway. After hibernation or agent shutdown, the new agent boots installed B; manual resolution holds that installation rather than restoring A. In the opposite pairing, the new root executor receives no exact requests from an old agent.
+
+ **Smallest fix:** Require a versioned agent/root protocol handshake before enabling these settings and swaps. Define upgrade and rollback ordering, including draining already-running legacy adoption passes, so independent channel adoption cannot coexist with the new policy.
+
+4. **MEDIUM — §§7.7, 8.5, 11: A good build can lose its root-verifiable fallback authorization.**
+
+ **Code:** Channel history is capped at ten builds at [internal/release/release.go:169](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/release.go:169), and promotions discard older entries at [internal/release/release.go:767](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/release.go:767).
+
+ **Failure:** H keeps good build A while its channel advances more than ten times. H eventually attempts B and rejects it. Root refuses the required repoint to A because A is absent from the current signed pointer. Changing channels can cause the same refusal immediately. A previous pin-only good build also lacks authorization once its signed pin is replaced, unless that authorization is retained. Direct staged execution can serve A, but the specified installation recovery cannot complete.
+
+ **Smallest fix:** Retain authenticated installation evidence alongside `GoodVersion`, and explicitly allow root to verify that evidence for fallback independently of current channel history or the current pin.
+
+5. **MEDIUM — §§7.1–7.2: Entry-local generation numbers can validate an earlier incarnation’s cached responses.**
+
+ **Code:** Stopping deletes the activator entry at [internal/activator/activator.go:2373](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/activator/activator.go:2373); the next wake constructs a fresh entry at [:748](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/activator/activator.go:748). Existing purging removes only the requested hostname’s entries at [internal/edge/cache.go:464](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/edge/cache.go:464).
+
+ **Failure:** A response under a custom domain is cached with `(instance H, generation 1)` while A serves. A crashes; its entry is removed. An uncached request wakes installed B through a new entry, again generation 1. The custom-domain entry survives the canonical-host purge and now passes the proposed generation comparison, serving A’s content and version header after B starts.
+
+ **Smallest fix:** Include an entry incarnation in the generation identity, or allocate generations monotonically outside the entry for the lifetime of the edge cache. Use that identity consistently for cache validation and transport ownership.
+
+6. **MEDIUM — §7.10: Discarding acknowledged candidate timer writes can strand committed work.**
+
+ **Code:** The control API advertises `registration_fence=true` at [internal/schedule/control.go:59](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/schedule/control.go:59) and currently acknowledges registration only after writing the timer at [:127](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/schedule/control.go:127).
+
+ **Failure:** Candidate B registers a wake timer, receives the ordinary successful response, and commits corresponding work to the shared SQLite database. B then fails health checking. §7.10 discards its pending registration, but its database transaction remains. A continues serving and can later hibernate without the timer needed to resume that work. Deferring platform writes does not defer the candidate’s database writes.
+
+ **Smallest fix:** Define candidate registration acknowledgement explicitly. Do not advertise or return the existing registration guarantee for disposable writes; either preserve acknowledged registrations durably with reconciliation on candidate failure, or introduce a provisional contract that prevents callers from treating staging as completed registration.
+
+7. **MEDIUM — §§5, 7.7, 13: Removing root resolution leaves named-unit adoption without a complete replacement path.**
+
+ **Code:** Root invokes the shared adoption pass at [internal/host/agent.go:3820](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/host/agent.go:3820), and adoption invokes the privileged restart at [internal/release/adopt.go:1142](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/adopt.go:1142).
+
+ **Failure:** §5 removes channel selection from every root adoption path. §7.7’s replacement performs staging, repointing and registry stamping, but no named-unit restart. §13 defers the scheduler attempt that would own that restart. Following these instructions leaves an existing console or other named-unit route either receiving no adoption request or having its installation stamped while its old process continues indefinitely.
+
+ **Smallest fix:** Explicitly preserve the existing named-unit adoption path outside this feature’s scope, or specify agent-issued exact requests that retain root’s existing restart and restart-failure recovery behavior without adding the deferred pacing feature.
\ No newline at end of file