Skip to content

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
zook-bot merged 1 commit into
mainfrom
polecat/tk-xypcy
Jul 22, 2026
Merged

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
zook-bot merged 1 commit into
mainfrom
polecat/tk-xypcy

Conversation

@zook-bot

Copy link
Copy Markdown
Contributor

Summary

Recurring polecat burn. When the operator parks a work bead that a
mol-polecat-work molecule is (or was) working, the park clears the anchor's
gc.routed_to but leaves gc.session_affinity=require on the molecule's
STEP 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.sh cmd_takeaway (line ~376, the
--release path) issues a SINGLE-bead update on the anchor:

--set-metadata "gc.routed_to=" --set-metadata "gc.proactive_reaction=1"

It operates only on $bead (the work/anchor bead). It does not walk the
associated molecule, so the step beads keep gc.session_affinity=require. That
affinity re-pins a pool polecat to the workflow independently of the anchor's
routing.

Recurrence (2nd documented instance)

  • 2026-07-22 ~17:4xZ: work bead gc-z0vi2.1 operator-parked 16:24Z (upstream PR
    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=require pins by hand; lx-aw2ph then reaped
    cleanly. Anchor untouched.
  • Almost certainly the same mechanism behind the earlier "3rd polecat burned"
    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=require on steps) surviving an
operator 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 --release path to also quiesce the associated
molecule's steps: for a work bead that has (or had) a mol-polecat-work
molecule, clear gc.session_affinity (and gc.routed_to) on its step beads in
the same release, so no ready+affine step remains to re-attract the pool.

Scope: gc-helm.sh is gc-toolkit-owned, so the script-side fix can land while
the 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-work molecule has open affine
steps; 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

  • Issue: tk-xypcy (bug, P2)
  • Source branch: polecat/tk-xypcy
  • Target: main
  • Codex signed off pre-open at 1d34a9ac; PR opened codex-green.

…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).
@zook-bot

Copy link
Copy Markdown
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

@zook-bot
zook-bot merged commit 4b09bd5 into main Jul 22, 2026
@zook-bot
zook-bot deleted the polecat/tk-xypcy branch July 22, 2026 20:37
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants