zdd-readiness: plan adds the dev-box flag blockers (cgroup.kill respawn kill, Task 19; legacy-parent guard evidence, Task 14)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+28
−2
docs/superpowers/plans/2026-10-11-zero-downtime-flag-readiness.md+28 −2
diff --git a/docs/superpowers/plans/2026-10-11-zero-downtime-flag-readiness.md b/docs/superpowers/plans/2026-10-11-zero-downtime-flag-readiness.md| index 23a993036..409195599 100644 |
| --- a/docs/superpowers/plans/2026-10-11-zero-downtime-flag-readiness.md |
| +++ b/docs/superpowers/plans/2026-10-11-zero-downtime-flag-readiness.md |
| @@ -12,7 +12,7 @@ |
| ## Global Constraints |
| -- **Flag off is unchanged.** With `CARLOS_SWAP` unset, behaviour stays as on origin/main, with exactly three authorized exceptions: Task 13's bucket retention also pins `GoodVersion` (it can only keep more, never delete more); Task 15's F6 change (flag-off by nature); Task 15's `/run` sweep (flag-independent cleanup). Every new hook defaults to nil and is set only on an armed or declined box. A changed shared helper (for example `configAckFunc`) keeps its flag-off behaviour; new behaviour goes on the armed path only. |
| +- **Flag off is unchanged.** With `CARLOS_SWAP` unset, behaviour stays as on origin/main, with exactly four authorized exceptions: Task 13's bucket retention also pins `GoodVersion` (it can only keep more, never delete more); Task 15's F6 change (flag-off by nature); Task 15's `/run` sweep (flag-independent cleanup); Task 19's recreation of a cgroup that took `cgroup.kill` (dev-box kernel defect). Every new hook defaults to nil and is set only on an armed or declined box. A changed shared helper (for example `configAckFunc`) keeps its flag-off behaviour; new behaviour goes on the armed path only. |
| - **Wire shapes are additive** (AGENTS.md). Restart-scope entries gain an optional `signed` field. The status rollout report gains per-host `held_epoch` (Task 8) and `transient` (Task 17), both `omitempty`; the console's instance-list rollout view passes them through. |
| - **The failure book file** (`<dataDir>/swap-failures.json`) is written as `"schema": 2` from Task 5 on. Readers accept 1 and 2. A schema-1 file must read unchanged. |
| - **Lock order** (`internal/rollout/manager.go:98`): goodMu, then activator.Service.mu, then Manager.mu, then scheduler.mu, then FailureBook.mu. No disk write under activator.Service.mu or scheduler.mu. |
| @@ -963,6 +963,8 @@ Rules (Astra R1 T12, R3 §5 item 9): |
| ### Task 14: The stage relay gives up, and a populated legacy parent is refused |
| +**Flag blocker confirmed on the dev box (#272, 2026-10-11, arm64, kernel 6.12.105, systemd 257, real-unit check (d)):** a generation started under a legacy `carlos-exec@<host>` parent that still holds a process starts silently with NO memory limit, while the strict path refuses. The guard must fail closed and be reported: the generation start is refused while the host's parent holds processes directly, with an agent log line naming the host and the pid count. Unit-test it with a fake cgroupfs, and add a kernel-gated test (skipped unless the env the existing kernel tests use, e.g. `CARLOS_CLAIM_SPAWN_KERNEL=1` with `cgroup.Setup`, is present) that puts a process directly in a delegated parent and asserts the generation start is refused and creates no `g*` child. |
| + |
| **Corrections (plan review round 1; these override the text below):** |
| - **Refuse direct parent members whatever children exist** (plan review #12). `ensureParent` (`internal/cgroup/gen.go:127`) returns an error now (its signature changes; update every caller, including `StartChildGated` at `:168`). It reads the parent's own `cgroup.procs` (direct members only, not `cgroup.events`' descendant population) and returns `ErrLegacyParentPopulated` whenever that file lists a process, with or without `g*` children. Replace the "one with a `g1` child still passes" test with `TestAPopulatedParentWithAStaleEmptyGenerationIsRefused`; keep a test that an empty parent with live `g*` children passes. |
| @@ -1052,4 +1054,28 @@ Rules (Astra R3 §5 item 10; plan review #8): spec §9.1 counts a **sleeping** i |
| - [ ] **Step 1:** `git grep -n 'TODO-COPY'` and build `copy-review/strings.json` in the copy-review skill's format (`id`, `section`, `label`, `text`, `context`, `notes`), one row per marker, the `id` matching the marker. If there are none, write none and say so. |
| - [ ] **Step 2:** Update the roll notes: which #272 items this branch closes (task commit refs filled in by the controller), the claims-on limitation and its reason code, the retry-signing roll order (boxes before consoles; unsigned retries are ignored), the schema-2 downgrade note, and the real-box checks unchanged (D1, the kernel suite, real-unit checks, the flagship probe). |
| -- [ ] **Step 3: Commit** `zdd-readiness: roll notes and copy index` |
| +- [ ] **Step 3:** Roll note from #272: `platform-infrastructure/scripts/update-platform-binary.sh` does not restart `carlos-instance-control`; use `carlos system update` (main's system apply restarts it, 0bb57a31), or restart it by hand after the old script. |
| +- [ ] **Step 4: Commit** `zdd-readiness: roll notes and copy index` |
| + |
| +--- |
| + |
| +### Task 19: A killed cgroup is recreated before anything respawns into it |
| + |
| +(Added 2026-10-11 from the dev-box run on #272. Execute it before Task 15, which uses `KillHost`.) |
| + |
| +**Flag blocker confirmed on the dev box** (arm64, kernel 6.12.105, systemd 257): once `cgroup.kill` has been written to a cgroup, every later `clone3(CLONE_INTO_CGROUP)` into that same cgroup is SIGKILLed at once. Recreating the cgroup fixes it. `TestClaimContainmentCleanupRespawn` (`internal/activator/claimspawn_kernel_linux_test.go:23`) reproduces it 3 of 3 on the box. Exposed: `ExecBackend.KillSidecar` (`internal/activator/genbackend.go:340`, the role-sidecar child) and `ExecBackend.KillHost` (`:431`, `carlos-exec@<host>`), which kill a cgroup and keep it. Generation kills remove their cgroup and are safe. |
| + |
| +**Files:** |
| +- Modify: `internal/cgroup/cleanup_linux.go:~282` (`Cleanup.KillAndEmpty`) and/or the cgroup `Manager`'s start path (`internal/cgroup/cgroup.go:~295-370`), `internal/cgroup/cleanup_other.go` if a signature changes |
| +- Modify: `internal/activator/genbackend.go:340-370, 431-449` if the fix lives there |
| +- Test: `internal/cgroup/*_test.go` (fake cgroupfs), the kernel-gated `internal/activator/claimspawn_kernel_linux_test.go` |
| + |
| +Rules: |
| +1. After a `cgroup.kill` on a cgroup we keep (the host cgroup, a role child), nothing is ever cloned into that same directory again. Before the next spawn into it, the emptied cgroup (and any empty child directories left under it) is removed and created fresh. Choose the place, removal right after the proven-empty kill or recreation at the next start, and say why. Either way, the recreated cgroup gets the same limits and controller settings it had (the ones the start path applies when it creates the cgroup), and it stays under the same parent, so the parent's accounting is unchanged. |
| +2. Removal must not race a concurrent start into the same cgroup. Use whatever lock the start path already holds for that host, or add one, and document it. |
| +3. A removal that fails (busy, EBUSY on a cgroup that is somehow repopulated) is an error: the kill is not proven, and the next start refuses rather than clone into a killed cgroup. |
| +4. Flag off: `KillHost` is reached with the flag off only through Task 15's F6 path. The new behaviour applies there too; it is an authorized exception (it only replaces a cgroup that was just proven empty). |
| + |
| +- [ ] **Step 1: Write the failing tests**: a fake-cgroupfs test that a kill of a kept cgroup followed by a start creates a fresh directory (new inode, or a recorded remove-then-create) with the limit files written again; a test that a failed removal makes the next start refuse; and make `TestClaimContainmentCleanupRespawn`'s kernel-gated path (unchanged gate, `CARLOS_CLAIM_SPAWN_KERNEL=1`) exercise the real `KillHost`/`KillSidecar` then respawn sequence if it does not already. The kernel test cannot run on this build box; say so, and leave it for the dev-box rerun. |
| +- [ ] **Step 2–4:** run to fail, implement, run cgroup and activator `-run 'Kill|Respawn|Cleanup|Sidecar'` (unsandboxed); paste. |
| +- [ ] **Step 5: Commit** `zdd-readiness: a cgroup that took cgroup.kill is recreated before anything respawns into it` |