carlos / platform Public

activator: a successful seed waits for its cgroup; spec revision 14 records the requested-stop close (plan 3, task 4 fix round 2)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev d278783714ea52196f507975aea30438ef03c0f7 parent 127356e
2 files changed, +27 −2
  • docs/superpowers/specs/2026-09-11-isolated-exec-design.md +17 −2
  • internal/activator/backend_exec.go +10 −0
diff --git a/docs/superpowers/specs/2026-09-11-isolated-exec-design.md b/docs/superpowers/specs/2026-09-11-isolated-exec-design.md
index 33dac7ded..03c033f38 100644
--- a/docs/superpowers/specs/2026-09-11-isolated-exec-design.md
+++ b/docs/superpowers/specs/2026-09-11-isolated-exec-design.md
@@ -1,10 +1,11 @@
# Isolated exec — every exec-backed instance in its own namespace
-**Date:** 2026-09-11 · **Status:** revision 13; codex verdict after
+**Date:** 2026-09-11 · **Status:** revision 14; codex verdict after
round 11 is **accept with corrections**, and the one correction is
applied here (54 findings over 11 rounds, all accepted; §13); a
whole-branch final review (plan 2) found six further items, fixed in
-revision 13 (§13); awaiting Paul's review · **Requested by:** discussion
+revision 13 (§13); plan 3's task 4 review settled the requested-stop
+close, recorded in revision 14 (§6.5, §13 R13); awaiting Paul's review · **Requested by:** discussion
#126 (Tito, with box evidence), approved in direction by Paul on
2026-09-11
@@ -1230,6 +1231,19 @@ agent owns these units and any other invocation is an anomaly to end,
not to adopt; the invocation id decides the event line's wording and
nothing about whether a writer is gone.
+One exception, after the activator itself has asked for the stop
+(`Stop` was called, or a failed health check stopped a fresh unit): a
+unit that then reports an invocation that is not the proc's own, or
+none at all while not terminal, has been replaced. systemd starts a new
+invocation only once the previous stop job completed, which is its own
+termination proof for the old one, so the goroutine emits `instance
+exited (status unknown: replaced after stop)` and closes `done` without
+the cgroup gate (the cgroup now belongs to the new invocation and would
+never read empty). The goroutine does not stop the replacement: a stop
+it did not request is not its anomaly to end, and a discarded proc
+must never be able to stop a later, legitimate wake (plan 3, task 4
+review).
+
**The seed's outcome comes from systemd, always.** The seed unit
carries `RemainAfterExit=yes` (§4.2), so it stays loaded, with its
`Result` and `ExecMainStatus`, until the control service reads them and
@@ -2116,3 +2130,4 @@ rollback runs the old binary).
corrections**). The correction is applied in this revision:
R11-1 the probe fixture comes from tmpfiles at bootstrap, and the control service verifies its ownership and mode before accepting a verdict (§3.2, §8.5).
- R12 (final review, plan 2): cgroup-ro targets the edge's delegated cgroup.procs; DAC precedes the mount flag so a root-owned file answers EACCES.
+- R13 (task 4 review, plan 3): after a stop the activator requested, a unit reporting a foreign or absent invocation proves the old invocation over without the cgroup gate (§6.5); the observer never stops an invocation it did not start once its own stop was requested.
diff --git a/internal/activator/backend_exec.go b/internal/activator/backend_exec.go
index 1287c7576..77b0eca5c 100644
--- a/internal/activator/backend_exec.go
+++ b/internal/activator/backend_exec.go
@@ -616,6 +616,16 @@ func (b *ExecBackend) seedUnit(ctx context.Context, rt registry.Route) error {
if res.Result != "success" {
return b.seedUnitFailed(ctx, rt, dbPath, seedOwnsDB, fmt.Errorf("seed for %s failed: exit %s (%s)", host, res.Status, res.Result))
}
+ // A successful seed is not done until its cgroup is empty (design
+ // §6.2): the control service already waited before answering, but the
+ // activator's own proof is what lets the wake go on to Start with no
+ // seed process still holding the database.
+ wctx, wcancel := context.WithTimeout(context.WithoutCancel(ctx), actStopTimeout)
+ emptyErr := b.waitSeedCgroupEmpty(wctx, rt)
+ wcancel()
+ if emptyErr != nil {
+ return fmt.Errorf("seed for %s: processes remain after a successful seed: %w", host, emptyErr)
+ }
log.Printf("activator: seeded %s", host)
return nil
}