carlos / platform Public

zdd-6 final fix wave 1: W7 roll notes

The unsigned host-retry log is on the owed list as a flag-on blocker (Astra plan 6 R1 #5, plan Q4). Gates F and G repeat Gate E's owed-list guard; the E and F approval lines name the probe's app actions; the deploy-watch bullet states the new fallback rule and tells a refusing box from a declined one; the Q3 note and the probe's control caution follow W6; the claims-check line is impersonal; moved line anchors re-checked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 3625973c4ad511cac51eb7eeda37284314070a7c parent a16d594
1 file changed, +68 −39
  • docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md +68 −39
diff --git a/docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md b/docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md
index 595ccf46b..d2caa4fa3 100644
--- a/docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md
+++ b/docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md
@@ -80,6 +80,16 @@ dormant and `CARLOS_SWAP` must stay unset until everything below is closed.
`bab/adopt-probe` under load with zero failed requests, then the fleet.
(This file.)
+### Added by plan 6's final review (not in #272)
+
+This item comes from Astra's final review of this branch, round 1, not from
+the discussion. The controller parked it as a flag-on blocker, so it belongs
+on #272 too.
+
+- Host retries ride the unsigned restart-scope log; a box that can write the
+ bucket can clear another box's failure hold (Astra plan 6 R1 #5, plan Q4);
+ sign retry entries or accept the risk in writing.
+
### Reply: four claims-on gaps
Plan 5 was merged dormant as `d905a67`, after merging main d6f66ef3 (#268
@@ -98,11 +108,11 @@ claims on, or close all four:**
4. Successful claimed wakes cannot record their good build or serving
version.
-I did not find a claims check in `swapArming`
-(`internal/host/rollout.go:145-158`), which is where the arming decision is
-made. I looked only there, so this is not proof that nothing declines
-elsewhere. Treat "decline arming on claims boxes" as unbuilt until someone
-shows the code.
+No claims check was found in `swapArming`
+(`internal/host/rollout.go:145-156`), which is where the arming decision is
+made. Only that function was read, so this is not proof that nothing
+declines elsewhere. Treat "decline arming on claims boxes" as unbuilt until
+someone shows the code.
### Reply: wiring caution (settings plan 2, merged as `70f8119`)
@@ -142,47 +152,59 @@ review, and only Paul closes the discussion's items.
`internal/console/apisidecars.go:121-130` and `sidecars_cli.go:368-378`.
- **The swap-policy door is person-only.** `PUT
/api/cli/apps/{account}/{app}/swap` is registered with the zero role set
- (`internal/console/authcli.go:858`, reasoning at
+ (`internal/console/authcli.go:863`, reasoning at
`internal/console/apiswap.go:1-8`). A signed-in member can run
`carlos apps set`. A CI token or any credential the app itself holds
cannot. If CI needs to set the policy, that is a one-line role change and
Paul's call.
-- **The first non-default swap policy creates `config.json` and causes one
- config delivery (plan Q3).** Setting the defaults (`overlap`, 1) on an app
- with no `config.json` writes nothing (`internal/appconfig/swap.go:47-56`).
- The first `hold` or concurrency above 1 creates the file, and boxes then
- see a config change once. After that, config delivery restarts an instance
- only when the rendered bytes change. In the probe this means the
- `--swap hold` control may restart `adopt-probe` once, a few seconds after
- the command and before the `hold` load phase has had its 20 s head start.
- Do not read that restart as part of the control's result. Q3 stays open with
- Paul.
+- **The first non-default swap policy creates `config.json` but restarts
+ nothing (plan Q3, Astra plan 6 R1 #1).** Setting the defaults (`overlap`,
+ 1) on an app with no `config.json` writes nothing
+ (`internal/appconfig/swap.go:47-57`). The first `hold` or concurrency above
+ 1 creates the file with no vars in it. Config sync leaves an instance's
+ config file absent when its bundle renders nothing and it has no instance
+ overlay (`internal/appconfig/syncexec.go:1397`), so no instance restarts,
+ no config revision reaches the release scheduler and no config ack is
+ written. That is the state every never-configured app's instances are in.
+ An instance that already has a config file converges on its bytes as
+ before. This answers Q3's restart concern; closing Q3 is Paul's call.
- **A host retry rides the unsigned restart-scope log (plan Q4).**
`carlos instances retry` asks the box to retry through the restart-scope
log. That log is not signed, and the box's `RetryHost`
(`internal/rollout/manager.go:501-515`) trusts any request that names a host
on that box. Only the app's record and the rollout report gate it, at the
- console, not at the box. Q4 stays open with Paul.
+ console, not at the box. It is on the owed list above as a flag-on
+ blocker.
- **`FailureBook` is not rendered anywhere.** The box publishes it
(`internal/status/snapshot.go:268-275`) but no `carlos` surface or console
page shows it. It is parked in #272. What `carlos` shows for a failed or
held build comes from the per-host rollout report instead.
-- **The deploy watch polls the instance list every 5 s.** It does not poll
- every 2 s. It falls back to polling the URL (today's `watchVersion`) only
- after a complete list with no reports for the targeted instances, or after
- 3 failed first lists. A truncated list never triggers the fallback, and a
- box that refuses or declines is named, not URL-polled (declined boxes fall
- back to today's URL watch). The constants are `deployWatchListEvery`,
- `firstListTries` and `deployWatchPollEvery`
- (`deploywatch.go:85-88`). A deploy against a flaky console can therefore
- wait up to about 15 s longer before it falls back.
+- **The deploy watch reads the instance list every 5 s, not every 2 s.** It
+ falls back to polling the URL (today's `watchVersion`) only on evidence
+ that no targeted instance is on a reporting box: the list answers with a
+ 4xx other than 429 (a console with no list for the app), or every targeted
+ instance is on the list, the console read every box's report (the list
+ carries no `rollout_incomplete`), and none of them has a report. A list cut
+ short for the whole app neither causes nor blocks the fallback. A 5xx, a
+ 429 or a network error is retried every 5 s until the deadline, and the
+ timeout then says the instance list could not be read. So a flag-off
+ deploy against a console whose list is failing times out instead of
+ URL-watching. One deadline covers the list reads, the waits between them
+ and the fallback. A box that refuses to arm is named in the timeout, and
+ its instances are never URL-polled. A box that declined overlap runs
+ today's stop-then-start paths, so its instances are URL-watched, as with
+ the flag off. An instance counted as done goes back to pending if its box
+ later reports a different build installed. The constants are
+ `deployWatchListEvery` and `deployWatchPollEvery`
+ (`deploywatch.go:140-142`); the fallback rule is `listAbsent` and
+ `legacyOnly` (`deploywatch.go:107`, `deploywatch.go:413`).
- **A dash under SERVING means the build is not reported, for either of two
reasons.** No rollout report (flag off, or an older console), or a box that
set the flag and declined (`Unavailable`). `servingCell` is at
`instances_cli.go:450-455`. After Gate E, a dev instance still showing a
dash means the box did not arm. Look for the box's reason code and the
agent log line `REFUSING CARLOS_SWAP=overlap`
- (`internal/host/rollout.go:151`).
+ (`internal/host/rollout.go:150`).
- **The migrations rule.** The new "Two builds at once" section of the public
CLI page (`docs/site/cli.md:330`, marked `TODO-COPY zdd.docs.migrations`)
and the release notes carry it: add in one release and remove in a later
@@ -276,9 +298,11 @@ checked here. Both must be live before Gate G.
Needs Paul's approval, naming the boxes: dev box `i-05498eada6f0afd23` (the
dev console box, eu-west-1) and dev box `i-046c7fe43620944bc` (eu-west-1),
-profile `kass`. Both ids come from the brief; only the first appears in
+profile `kass`. Both box ids come from the brief; only the first appears in
another repo document (`2026-10-06-console-release-defences-plan-4-d4.md`),
-and the second is not checked here. **Do not start this gate while the
+and the second is not checked here. The approval also names the probe's app
+actions on dev: creating the `zdd-probe` app, flipping its swap policy to
+`hold` and back, and four deploys of it. **Do not start this gate while the
"Before Gate E: owed" list has an open item that Paul has not waived.**
First the index's D1 guard: measure `systemctl daemon-reload` on a dev box
@@ -332,7 +356,10 @@ dash means the box did not arm (see the dash note above).
## Gate F: the flagship probe box
-Needs Paul's approval, naming the box and its region.
+Needs Paul's approval, naming the box and its region. The approval also
+names the probe's app actions: flipping `bab/adopt-probe`'s swap policy to
+`hold` and back, and four deploys of it. **Do not start this gate while the
+"Before Gate E: owed" list has an open item that Paul has not waived.**
Find the probe's region with:
@@ -362,7 +389,9 @@ APP=adopt-probe ACCOUNT=bab CONSOLE=https://console.carloku.com HOST=probe1.bab.
## Gate G: the rest of the flagship
-Needs Paul's approval, naming each box before it is done.
+Needs Paul's approval, naming each box before it is done. **Do not start
+this gate while the "Before Gate E: owed" list has an open item that Paul
+has not waived.**
One box at a time, with Gate E's flag command. After each box, do one
ordinary deploy of `adopt-probe` and run `carlos instances list --app
@@ -476,15 +505,14 @@ slowest is clearly above the baseline. If it is not, the probe cannot see a
pause and the overlap result means nothing.
The watch prints `swapped https://<host>` (draft word `swapped`,
-`deploywatch.go:459`) and `box <label>: ...` (`deploywatch.go:43`). With the
-flag off it prints `live https://<host>` instead (`deploywatch.go:454`), so a
+`deploywatch.go:569`) and `box <label>: ...` (`deploywatch.go:44`). With the
+flag off it prints `live https://<host>` instead (`deploywatch.go:564`), so a
`live` line during the overlap phase means that box is not armed.
-One caution on the control: the first `--swap hold` on an app creates
-`config.json` and causes one config delivery (the Q3 note above). If the
-hold phase's slowest request is high because of that restart rather than the
-zdd-4 swap, the control is contaminated. If the numbers look odd, rerun the
-hold phase once with the policy already set.
+On the control: the first `--swap hold` on an app creates `config.json`, and
+on boxes running this release that restarts nothing (the Q3 note above), so
+the hold phase's pause is the zdd-4 swap's. If the numbers look odd anyway,
+rerun the hold phase once with the policy already set.
### 4. Clean up
@@ -496,7 +524,8 @@ On dev, run `carlos apps delete zdd-probe` once Paul is done with it.
## Names checked against this branch
-Checked in the code on `zdd-6-surfaces` (HEAD `77d77b3a` plus this file):
+Checked in the code on `zdd-6-surfaces` (HEAD `77d77b3a` plus this file;
+the anchors final fix wave 1 moved were re-checked after it):
the verbs and flags `carlos system update|status`, `carlos apps set|create|delete`,
`carlos instances list|create|enable|retry`, `carlos deploy`, `carlos ship`,
`carlos promote`, `carlos promote --retry`, `carlos rollback`; the flag