carlos / platform Public

spec: instance update settings rev 7 — S5 fix applied (an unresolved request never blocks recovery); accepted by Astra

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev b48c037dc7fab474a3fc0f60d75aeaece03c2d54 parent 1cb6dbd
2 files changed, +39 −5
  • docs/superpowers/specs/2026-10-07-instance-update-settings-design.md +22 −5
  • docs/superpowers/specs/CODEX-FINDINGS-SETTINGS-S5.md +17 −0

CI passed — run details

diff --git a/docs/superpowers/specs/2026-10-07-instance-update-settings-design.md b/docs/superpowers/specs/2026-10-07-instance-update-settings-design.md
index 8c457c3df..39aa0dcbe 100644
--- a/docs/superpowers/specs/2026-10-07-instance-update-settings-design.md
+++ b/docs/superpowers/specs/2026-10-07-instance-update-settings-design.md
@@ -1,6 +1,6 @@
# Instance update settings: auto-update, channel, major hold, pin
-Status: draft rev 6, 2026-10-07. Split out of the zero-downtime deploys
+Status: rev 7, 2026-10-07. **Accepted by Astra** (S5: accept with fixes, the one fix applied in rev 7). Awaiting Paul's review. Split out of the zero-downtime deploys
spec (`2026-10-07-zero-downtime-deploys-design.md`) at its rev 6. Rev 1 was
a design with console-signed per-instance directives. Astra's round 6
(`CODEX-FINDINGS-ZERO-DOWNTIME-R6.md`) rejected it with 6 findings.
@@ -18,7 +18,9 @@ review of rev 3 (`CODEX-FINDINGS-SETTINGS-S2.md`) closed two of those five
and left three partial. Rev 4 answered those three and one new finding
(§10). S3 (`CODEX-FINDINGS-SETTINGS-S3.md`) closed one and left three
partial. Rev 5 answered them (§11). S4 (`CODEX-FINDINGS-SETTINGS-S4.md`)
-closed two and left two partial. Rev 6 answers them (§12).
+closed two and left two partial. Rev 6 answered them (§12). S5
+(`CODEX-FINDINGS-SETTINGS-S5.md`) closed every earlier finding and accepted
+with one fix, applied in rev 7 (§13).
## 1. Why
@@ -215,7 +217,7 @@ build its box holds as failed unless it has already served that build.
1. **`pin` set:** install the pinned build, if §4.1 authorizes it. Bake
doesn't apply, because a person chose it.
-2. **An `install_request` R above the completed watermark.** The
+2. **A resolved `install_request` R above the completed watermark.** The
watermark is the **highest completed id known from either source**: the
box's own record (`/opt/carlos/consumed/<host>`) or the build state
object. It is never one source in place of the other, so an older local
@@ -225,7 +227,8 @@ build its box holds as failed unless it has already served that build.
- **If R has already been resolved to a build T** (locally or in the
build state), install T. That covers a pass that was interrupted, and
a request picked up on another box.
- - **Otherwise**, resolve R now: take the channel head that this box's
+ - **Resolution** happens at the start of every pass, before the rules,
+ for any pending R that has no T yet: take the channel head that this box's
own bake check says is eligible, or the current head if the request
asked for `FastTrack`. Write {R, T} to the root-owned file
`/opt/carlos/consumed/<host>` before staging. The agent mirrors it
@@ -240,7 +243,11 @@ build its box holds as failed unless it has already served that build.
new request.
- `manual` and `major` are ignored for this one move.
- **If the head is still baking**, R stays unresolved and the reason is
- reported.
+ reported. **An unresolved R doesn't block anything else**: rule 2
+ applies only once R has a T, so until then the next rule decides. A
+ held instance on a fresh box therefore still recovers onto
+ `last_executed` (rule 3), and R is resolved later, when the head is
+ eligible on this box.
3. **`manual`, or `major = N` with a head whose semver tag is absent or has
another major:** the instance is **held**, and its target is:
- the build state's **`last_executed`**, if there is one. If the build
@@ -386,6 +393,9 @@ build whatever these settings say.
- **Completion:** a request completes at the pre-exec write, not at
install. A held sleeper with a pending request wakes onto T, not
`last_executed`. A failed T completes the request as failed.
+- **Unresolved request during recovery:** a held instance with an
+ unresolved request, whose head is still baking, moves to a fresh box. It
+ recovers onto `last_executed`, and the request resolves after the bake.
- **Watermark merge:** a box the instance returns to, with an older local
completion, doesn't replay a request the build state shows as
completed.
@@ -436,3 +446,10 @@ build whatever these settings say.
| S3-1 (partial), 1 | Backfill signs whatever is in the bucket now | §4.1: no automatic backfill. Older builds are recovered from a staged copy, refused elsewhere, or authorized explicitly by an app member (`carlos releases authorize`) |
| S2-4 (partial), 2 | An older local marker overrides a newer durable completion | §4.3 rule 2: the watermark is the highest id from either source; completed requests never re-enter |
| 3 | Completing at install loses the update for held instances | §4.2: completion is the pre-exec write; §4.3: a pending T outranks the held target in root and at wake; a failed T completes as failed |
+
+## 13. Astra S5 (rev 6): accept with fixes
+
+Every earlier finding is closed. The one fix: an **unresolved** request
+blocked recovery while the head was baking. Rev 7 makes rule 2 apply only
+to a **resolved** request, so recovery proceeds and the request resolves
+later (§4.3 rule 2).
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-SETTINGS-S5.md b/docs/superpowers/specs/CODEX-FINDINGS-SETTINGS-S5.md
new file mode 100644
index 000000000..ccff9897e
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-FINDINGS-SETTINGS-S5.md
@@ -0,0 +1,17 @@
+ACCEPT WITH FIXES
+
+S4-1: **CLOSED** — Automatic backfill is removed; legacy authorization requires explicit publisher confirmation.
+S4-2: **CLOSED** — Completion uses the maximum local and durable watermark.
+S4-3: **CLOSED** — Requests remain pending until pre-exec, and their resolved target outranks held recovery.
+Carried S3-1 / S2-1: **CLOSED** — Added installs require digest-bound authorization, with the explicit legacy exceptions.
+Carried S2-4: **CLOSED** — Lost, unexecuted resolutions may be reconsidered; completed requests cannot replay through an older local watermark.
+
+1. **HIGH — §4.3 rules 2–3: An unresolved update request blocks recovery of an otherwise recoverable instance.**
+
+ **Code:** [internal/provision/reconcile.go:394](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/provision/reconcile.go:394) creates destination routes without an installed build. [internal/release/bake.go:364](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/bake.go:364) calculates eligibility from the destination box’s observation time. [internal/release/materialize.go:47](/home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/internal/release/materialize.go:47) routes cold materialization through adoption and fails when no executable is installed.
+
+ **Failure:** Manual instance H has durable `last_executed=A`, with A available and its promotion record valid. The owner requests an update while head B is still baking, leaving R unresolved. H then moves to a fresh box or loses its existing box.
+
+ Under the specified “first rule that applies decides” ordering, rule 2 matches R and waits for B’s bake. Rule 3 never selects A. The wake check has no resolved T, so its recovery target is A; however, A is not staged locally, and every relayed root pass again stops at rule 2. H remains unavailable throughout B’s bake window despite having a valid recovery build. Repeated head changes can prolong the outage.
+
+ **Smallest fix:** While R remains unresolved, retain it as pending but allow rule 3 to recover and run `last_executed`. Only a **resolved** T should override that recovery target. Keep B’s bake check unchanged, and add a move/recovery test with an unresolved request.
\ No newline at end of file