fix(gc-helm/takeaway): operator-park clears anchor routed_to but not workflow steps' session_affinity → pool re-spawns polecat onto parked husk (tk-xypcy)#211
Merged
Conversation
…lease (tk-xypcy) Recurring polecat burn: `gc-helm takeaway <anchor> "..." --release` parks a work bead (the ANCHOR of a mol-polecat-work molecule) by clearing the anchor's own gc.routed_to, but it operates on that single bead only. The molecule's STEP beads keep their own re-attracting pins — gc.routed_to (the pool-offer lever), an assignee (the assigned-work hand-back lever), and gc.session_affinity=require — so the pool re-spawns a fresh polecat onto the already-parked husk, where it re-derives "nothing to do" and drains: one burned session per scale_check tick. The witness has been clearing these pins by hand (2nd documented instance on this class); this folds that cleanup into the park itself. Fix: after the anchor release, cmd_takeaway now calls a new `quiesce_release_molecule_steps` helper. The anchor carries no pointer to its molecule, so the helper discovers the linkage the way the formula resolves an anchor, walked in reverse: enumerate the live mol-polecat-work STEP beads (bd list), group by gc.root_bead_id, resolve each root's anchor (root -> gc.input_convoy_id -> the convoy's single tracked member), and quiesce ONLY the steps whose root resolves to THIS parked anchor — clearing every present pin (gc.routed_to + assignee + gc.session_affinity) in ONE update per step. This reuses the exact reverse walk proven by the sibling quiesce-completed-workflows.sh (tk-p9ji9), so no core gascity helper is needed and nothing is deferred; the two are siblings with different triggers (witness patrol sweep of terminal workflows vs. this explicit operator park). Guards (mirroring the sibling pass): FAIL CLOSED on an unresolved or mismatched anchor (never drain another, possibly-live, molecule); never close a step or rewrite its status (the DANGER clause — closing load-context walks a polecat onto an already green-gated, PR'd branch and stales check.<gate>); never de-route the workflow-finalize step (its control-dispatcher route is the molecule's only finalize path); all pins cleared atomically to avoid a transient open+unassigned+routed pool-offer window. The helper body is a subshell with `set +e`, so it is best-effort by construction and can never abort the park (cmd_takeaway runs under `set -e`). The root bead is untouched (it carries no gc.step_ref, so it is filtered out), satisfying the acceptance requirement that the anchor + root husk are left alone. Validation: new hermetic gc-helm.test.sh runs the real gc-helm.sh via `sh` (dash, as shipped) with a stubbed `gc` on PATH — 20 assertions covering the release bundle, affine/pool/atomic/idempotent quiescing, the finalize-route and cross-molecule (SCOPE) and unresolved-anchor (FAILCLOSE) exclusions, the non-mol-polecat-work step filter, and both dynamic and static NOCLOSE guards. `sh -n`/`bash -n`/`dash -n` clean; shellcheck clean at warning+ severity (only pre-existing SC2016 info in cmd_board and the house-standard SC2015 info on the test's ok/bad idiom remain).
Contributor
Author
|
Codex signoff (pre-open, comment-only — not an approval): Codex pre-open branch review verdict: COMMENT / Ready to merge. Reviewed origin/polecat/tk-xypcy at 1d34a9a against origin/main. Findings: none. Verification: bash assets/scripts/gc-helm.test.sh passed 20/20; bash assets/scripts/quiesce-completed-workflows.test.sh passed 20/20; sh -n assets/scripts/gc-helm.sh passed; bash -n assets/scripts/gc-helm.test.sh passed. shellcheck not installed in this environment. Artifact: /tmp/compound-engineering/ce-code-review/20260722-182758-2f992462/report.md |
johnzook
approved these changes
Jul 22, 2026
zook-bot
added a commit
that referenced
this pull request
Jul 23, 2026
…ke-reason-aware teardown (tk-z130v.3) (#213) ## Summary # Ground gc-toolkit bead-hosts in core (assigned-work wake reason) + wake-reason-aware teardown Parent decision: tk-z130v (read its notes — the A/B evidence and the accepted trade are there). Repo: `zookanalytics/gc-toolkit` (INTERNAL fork; standard bead→PR→Codex→ refinery flow — open a PR, do NOT self-merge). This IS an implementation bead. ## Goal (one sentence) A gc-toolkit bead-host must survive an INVOLUNTARY config-drift drain by being brought back automatically by the reconciler with its conversation intact — achieved by making the host the **assignee of its hosted bead** (the assigned-work wake reason survives the `drained` state), NOT by any fork exemption. ## Proven facts you are building on (do not re-derive; verify only where noted) - gascity core revives a stopped session that is the assignee of a bead with awake-demand. A/B proven on this city: assignee set ⇒ revived ~20s, no operator action; assignee cleared ⇒ stayed `drained`. (Mechanism: `compute_awake_set.go` assigned-work loop has no `Drained` gate; `workBeadHasAwakeDemand` = in_progress, or open+Ready.) - Assignment MUST use the session **name** (`s-<id>`), not the session id — passing a session id to `gc bd update <tk-bead> --assignee` flips the resolver to the HQ store and fails "no issue found". `s-<id>` is also what the awake-set matches on. - A grounded host does NOT idle-sleep and cannot be stopped by plain `drain-ack` while the assignment + awake-demand hold (revives ~20s). Teardown must clear the wake reason FIRST. ## The change (core of it) The dual-link already written on host bring-up is the natural home. In `tools/gc-bead-host.sh`: 1. **`cmd_link` (bring-up)** — in addition to the existing reverse/forward/lineage writes, set the **work bead's `assignee` to the session name** (`$name`, already resolved in that function). This is the grounding. Keep it idempotent (re-linking the same session must not thrash). Note the existing convention (see `gc-helm.sh` header comment: "a child bead's `assignee` is its OWNING session — the session_name"), so assignee=session_name is already an established meaning here — confirm the host bead is consistent with it. 2. **`cmd_unlink` (teardown / re-bind)** — clear the work bead's `assignee` (alongside the existing metadata clears) so the host can actually be stopped. Without this, a re-bind or cleanup leaves the old session grounded and un-drainable. 3. **`gc-helm.sh takeaway --release`** — ALREADY clears assignee (`--assignee=`) as its closing step (verified). Confirm it still does after your change and that release = ungrounded-then-stoppable. If any other operator-facing "done with this bead" path exists, it must also clear the wake reason before drain. ## The prompt/model change (do not skip — the old guidance is now wrong) `agents/bead-host/prompt.template.md` currently tells hosts to `gc runtime drain-ack` "between visits" for token economy. For a GROUNDED host that is a no-op-with-restart (revives ~20s). Update the model to ONE coherent story and make the prompt match it: - **DEFAULT (recommend): warm-while-live.** A grounded host stays warm as long as its bead has awake-demand; it is put down by ENDING the work — close the bead, or park it (defer/block so it is no longer `ready`), or `takeaway --release`. Replace the "drain-ack between visits" instruction accordingly: intentional teardown clears the wake reason (release / close / park) FIRST, then drains. - **ALTERNATIVE (only if you verify it): hold-to-sleep.** Keep the assignment but use a bounded `gc session suspend`/`held_until` to sleep between visits — the hold beats every wake reason (spike claim, `compute_awake_set.go` "Hold suppression — overrides everything"). BEFORE proposing this, VERIFY EMPIRICALLY that a config-drift occurring DURING the hold still results in eventual revival (not permanent death). This interaction is UNVERIFIED. If you cannot verify it cleanly, do NOT adopt it — ship the warm-while-live model. ## Guardrails to verify (must not regress) - **Assignee=session_name must not make the host bead look like pool/handback work.** Confirm scale_check, keeper prime-sweep (assignee-based), witness reconcile, and any pool router do NOT pick up a host bead just because it now has an assignee. (The child-bead convention suggests safe, but a HOST bead is often a decision/open bead — check it isn't treated as an unassigned handback or a routable root.) - **Migration / live hosts.** After deploy, existing live bead-hosts are still ungrounded. Provide a one-time backfill (set assignee=session_name on every live host bead from its hosts_bead link) OR document that they ground on next `open`. This matters: the exemption drop (tk-z130v.4) is gated on all live hosts being grounded, and THIS conversation's host (tk-z130v) is one of them. - **Idempotency / cross-rig.** `up` runs cross-rig with `BEADS_DIR` pinned; the assignee write must land in the bead's own rig ledger (same pinning as the existing link writes). ## Tests / verification (TDD where practical) - Offline-testable: `link` sets assignee=session_name on the work bead; `unlink` clears it; both idempotent. (gc-bead-host.sh already has offline-testable link/unlink — extend those.) - Live smoke on a THROWAWAY host (mirror tk-z130v's A/B; do not touch real hosts): open → assignee set → drain/idle → auto-revive; release/unlink → assignee cleared → drain sticks. Tear the throwaway down cleanly. - Do NOT drift real city config to test the involuntary path; the assigned-work grounding is the tested invariant and the idle-revive A/B already stands in for it. ## Deliverable A PR on `zookanalytics/gc-toolkit` implementing the above, with the prompt/model updated to the shipped sleep story, guardrails verified, and a backfill (or a documented ground-on-next-open) for live hosts. Report the PR number back on this bead. Do NOT open any upstream (gastownhall) PR. Do NOT touch the gascity fork or gc-8yr6px — that is tk-z130v.4, deliberately separate and blocked on this. ## Implementation notes Implemented (branch polecat/tk-z130v.3 @ ca40c4d, target main). Grounding + warm-while-live teardown. CORE (tools/gc-bead-host.sh): `link` sets work-bead assignee=session_name — the assigned-work wake reason compute_awake_set revives across an INVOLUNTARY drain (no Drained gate); `unlink` clears it (ungrounds → stoppable); new `backfill` grounds already-linked hosts city-wide, rig-pinned per bead. New/resumed hosts ground automatically via up→link. gc-helm `takeaway --release` already clears assignee — verified + noted it ungrounds. MODEL (agents/bead-host/{prompt.template.md,agent.toml}): warm-while-live — a grounded host stays warm while its bead has awake-demand; teardown clears the wake reason (close/park/release) FIRST, then drains. hold-to-sleep NOT adopted (unverified; a polecat must not drift live config). GUARDRAILS (verified, must-not-regress): scale-check/keeper/pool-router/refinery/reconcile never act on a session-name assignee (they key on gc.routed_to/branch/merge_result/own-identity; grounding also fails --unassigned). The witness recover-orphaned-beads scan newly sees grounded host beads, but drained/asleep already classify not-orphaned (revival scenario safe); closed the dead-host edge safe-by-code (cf #206/#210): recover-orphaned-beads drops beads grounded to their own host (assignee==host_session_name). TESTS: bead-host-binding-fixture.sh grounding/unground/idempotent/backfill (31/31 live); host-bead-skip.test.sh (5/5, extracts the witness filter verbatim). bash -n + shellcheck clean. Rebased onto origin/main incl. #205 — quiesce is orthogonal (keys gc.step_ref=mol-polecat-work.*, never host beads). DEFERRED (operator; needs a live LLM session — a polecat must not spawn/reset live sessions): live smoke on a throwaway host (open→assignee set→drain→auto-revive; release/unlink→cleared→drain sticks). Parent tk-z130v already A/B-proved revival; this ships the metadata contract that triggers it. Scope: gc-toolkit only (no upstream/gascity; tk-z130v.4 separate). PR opens via the refinery pre-open codex gate — PR number to be recorded then. Rework tk-5bygy landed on polecat/tk-z130v.3 at 358e0b7; re-review tk-knb1t dispatched (one-anchor-per-PR, tk-ynz4b). Rework tk-v369i landed on polecat/tk-z130v.3 at 9fd8e47; re-review tk-wc3gf dispatched (one-anchor-per-PR, tk-ynz4b). Rework tk-0gnq9 landed on polecat/tk-z130v.3 at cc62091 (rebased onto origin/main 4b09bd5 — resolved the assets/scripts/gc-helm.sh cmd_clear conflict with landed tk-xypcy #211, preserving the codex signoff fixes); re-review tk-q6zwf dispatched (one-anchor-per-PR, tk-ynz4b). Gates at cc62091: bash -n clean; shellcheck clean at warning+ (info-only SC2015 x41 on the A && ok || bad assertion idiom, SC2016 x4). ## Refinery handoff - Issue: `tk-z130v.3` (task, P1) - Source branch: `polecat/tk-z130v.3` - Target: `main` - Codex signed off pre-open at `cc620913`; PR opened codex-green.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Recurring polecat burn. When the operator parks a work bead that a
mol-polecat-workmolecule is (or was) working, the park clears the anchor'sgc.routed_tobut leavesgc.session_affinity=requireon the molecule'sSTEP beads. The pool then re-spawns a polecat, pinned by that affinity, onto
the already-parked husk — where it re-derives "nothing to do" and drains,
burning a session each time.
Verified mechanism (file:line)
rigs/gc-toolkit/assets/scripts/gc-helm.shcmd_takeaway(line ~376, the--releasepath) issues a SINGLE-bead update on the anchor:It operates only on
$bead(the work/anchor bead). It does not walk theassociated molecule, so the step beads keep
gc.session_affinity=require. Thataffinity re-pins a pool polecat to the workflow independently of the anchor's
routing.
Recurrence (2nd documented instance)
fix(context): single-source the model context-window table gastownhall/gascity#4527 shipped/green — nothing stranded). The pool re-spawned
polecat lx-aw2ph (gascity/gc-toolkit.furiosa) onto husk steps gc-mhpc4 +
gc-82afx (root gc-gfb70). The gascity witness cleared the stale
assignee + gc.session_affinity=requirepins by hand; lx-aw2ph then reapedcleanly. Anchor untouched.
churn on this very anchor (lx-wisp-tz59f).
Relationship to tk-p9ji9 (SIBLING, not dup)
tk-p9ji9 is the
gc.routed_to-on-a-never-closed-load-context-step re-offer.THIS is a DIFFERENT pin (
gc.session_affinity=requireon steps) surviving anoperator PARK of the work bead, via a different code path (gc-helm takeaway).
A fix to either does not fix the other; cross-linking so they are triaged as a
family ("parked/completed workflow keeps re-spawning polecats").
Fix direction
Teach the park/takeaway
--releasepath to also quiesce the associatedmolecule's steps: for a work bead that has (or had) a
mol-polecat-workmolecule, clear
gc.session_affinity(andgc.routed_to) on its step beads inthe same release, so no ready+affine step remains to re-attract the pool.
Scope:
gc-helm.shis gc-toolkit-owned, so the script-side fix can land whilethe gascity fork shed is running. IF walking work-bead -> molecule -> steps
needs a core helper (the linkage may not be cheaply discoverable from the work
bead alone), split that piece into a gascity follow-up and defer it until after
the pending rebase — do NOT block the gc-toolkit fix on it.
Acceptance
Operator-park a work bead whose
mol-polecat-workmolecule has open affinesteps; verify the pool does NOT re-spawn a polecat onto the husk on the next
scale_check tick, and that the anchor + root husk are left untouched (parked
anchors are not orphans — reference-takeaway-parked-bead-is-not-an-orphan).
Reported by gascity/gc-toolkit.witness (in-lane containment done; anchor
untouched). Re-verified at file:line by mayor.
Implementation notes
Implemented: takeaway --release now quiesces the parked molecule's step beads (gc-helm.sh: new quiesce_release_molecule_steps + wiring; new hermetic gc-helm.test.sh, 20 assertions). Reverse walk steps->root->convoy->anchor, scoped to the parked anchor; clears gc.routed_to+assignee+gc.session_affinity atomically per step; fail-closed, never closes a step, never de-routes workflow-finalize, leaves anchor+root untouched. bash/sh/dash -n clean; shellcheck clean at warning+. No core helper needed (reuses the sibling tk-p9ji9 walk), nothing deferred. Branch polecat/tk-xypcy @ 8542a03.
Refinery handoff
tk-xypcy(bug, P2)polecat/tk-xypcymain1d34a9ac; PR opened codex-green.