spec: record the final review's lifecycle rulings
§7.2 gains the fired-timer consumption rule, the exit-status reading of a hibernating worker's exit, and what "the record exists ⇒ the process runs" costs an always-on one (the sweep's start arm, and max_run spent as a recycle). §7.4 scopes the seed-don't-replay rule to arming and says why a route first seen afterwards is woken. §8 states most-restrictive placement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014WvcQVWggXkE5BAkjRDgWF
1 file changed,
+38
−2
docs/superpowers/specs/2026-08-17-sidecar-members-design.md+38 −2
diff --git a/docs/superpowers/specs/2026-08-17-sidecar-members-design.md b/docs/superpowers/specs/2026-08-17-sidecar-members-design.md| index 0064c4889..75184b49e 100644 |
| --- a/docs/superpowers/specs/2026-08-17-sidecar-members-design.md |
| +++ b/docs/superpowers/specs/2026-08-17-sidecar-members-design.md |
| @@ -361,6 +361,21 @@ as it refuses `StartSidecar` today. |
| backoff (2s → 1m, reset after a healthy minute) — the constants the |
| co-spawn path already uses, now applied to a standalone member. |
| +A hibernating worker's exit is read by **exit status**, the only thing that |
| +separates the two meanings it can carry (final review, 2026-08-17): a clean |
| +exit is "my work is done" and parks; a non-zero exit is a crash and restarts |
| +with that same backoff, exactly as an always-on worker's does. Without the |
| +distinction, a bot that fell over before setting a next-due slept forever — |
| +nothing can dial one. |
| + |
| +A **fired** due time is consumed by the wake it causes, never by the sleep |
| +that follows (final review, 2026-08-17): a worker's stop writes no |
| +`next_due` at all, so otherwise the passed value is re-served on every sweep |
| +and the sidecar wakes, idles, parks and re-wakes forever. The consumption is |
| +conditional on the row still holding the value that fired, so a next-due the |
| +child posted during its own wake survives — and a worker that sleeps BEFORE |
| +its timer fires keeps its schedule untouched. |
| + |
| **Sleep**, triggered by `/v1/idle`, by `max_run`, or by the idle timer |
| (no `/v1/heartbeat` and no agent-socket call within `idle`): |
| SIGTERM (5s grace, then SIGKILL) → record `next_due` from whatever the |
| @@ -370,7 +385,14 @@ sidecar's next-due arrived over the socket while it was alive, which is |
| strictly better than asking a process we are about to kill. |
| **Always-on** is `hibernate: false`: the record exists ⇒ the process runs, |
| -same supervision, no idle clock, no lease churn. |
| +same supervision, no idle clock, no lease churn. Two consequences of that |
| +rule, both from the final review (2026-08-17): the sweep starts an always-on |
| +worker that has no live entry — otherwise one created while the agent was |
| +running never ran at all, since the boot pass runs once, at arming — and |
| +`max_run` applies to it too (it is the wedged-reconnect-loop guard, and a |
| +long-lived worker is exactly that shape), but is spent there as a RECYCLE of |
| +its child on the same entry, keeping the lease, because stopping an |
| +always-on worker leaves nothing to start it again. |
| ### 7.3 Where this lives: inside the activator, off its request path |
| @@ -414,6 +436,16 @@ on someone else's. |
| serving that app notices within seconds (the `carlos restart` pattern |
| verbatim) and wakes the named sidecar. Nothing is pushed at a box — |
| which is what lets the box live in a private subnet. |
| + |
| + The box's etag memory is seeded once, at arming, and that seeding is |
| + what keeps an agent restart from replaying every wake in history. It is |
| + scoped to arming precisely (final review, 2026-08-17): a sidecar route |
| + seen for the first time AFTER that baseline has just been minted, and a |
| + wake object already standing at it is a wake somebody asked for and the |
| + CLI already reported as accepted — the create-then-wake flow, which is |
| + the feature's primary one, since an operator wake is a hibernating |
| + sidecar's only first-run mechanism. A spurious extra wake is idempotent |
| + and cheap; a lost one is a bot that never runs. |
| 3. **Boot** — a non-hibernating sidecar starts with the agent. |
| ### 7.5 Delivery wake — designed, not built |
| @@ -447,7 +479,11 @@ apps-domain-style gate this repo already uses. On a `none` box: |
| **The record asks.** `placement: "no-inbound"` on the manifest/record makes |
| the sidecar reconciler mint **only** on boxes declaring `CARLOS_INBOUND=none`. |
| -`placement: "any"` (default) places by pool/region alone. |
| +`placement: "any"` (default) places by pool/region alone. Where the two |
| +disagree, the **most restrictive wins** (final review, 2026-08-17): a record |
| +may narrow an app that asks for nothing, but a record stating `"any"` under a |
| +`no-inbound` manifest does not widen it — that is the direction the console |
| +door already refuses, and a direct bucket write must not reach around it. |
| The two are independent on purpose: `none` is a property of the box's |
| network, `no-inbound` is a requirement of the workload. A member who wants |