Skip to content

Pool re-offers a completed mol-polecat-work's never-closed load-context step FOREVER — permanent per-molecule polecat burn, does NOT stop when the anchor lands (tk-p9ji9)#205

Merged
zook-bot merged 1 commit into
mainfrom
polecat/tk-p9ji9
Jul 22, 2026
Merged

Pool re-offers a completed mol-polecat-work's never-closed load-context step FOREVER — permanent per-molecule polecat burn, does NOT stop when the anchor lands (tk-p9ji9)#205
zook-bot merged 1 commit into
mainfrom
polecat/tk-p9ji9

Conversation

@zook-bot

Copy link
Copy Markdown
Contributor

Summary

OBSERVED (gc-toolkit, 2026-07-17T09:21Z-09:32Z): four polecat sessions
(lx-xz3a6, lx-94hvo, lx-8tznd, lx-r2nhg) were spawned ~5min apart and each
claimed the SAME step bead tk-cnt3i (mol-polecat-work.load-context) for work
that was already complete, shipped, and parked in the merge gate. Each session
burned a wisp to re-derive "nothing to do" and drain. See tk-3b5hv notes for the
per-incarnation record.

MECHANISM:

  • mol-polecat-work (graph.v2) materializes 7 step beads but the polecat executes
    them INLINE in one session; no step contains bd close. Steps are meant to be
    finalized by machinery.
  • The step graph is chained: load-context blocks workspace-setup blocks ... So
    while load-context stays open it is the ONLY ready step.
  • load-context carries gc.routed_to=gc-toolkit/gc-toolkit.polecat. Open + ready +
    pool-routed = the pool re-offers it to every idle polecat, forever.
  • The workflow root (tk-6j9ue) appears to wait on its input convoy (tk-we737) to
    complete. The convoy's single member is the work bead tk-3b5hv, which stays OPEN
    by design under close-on-land until the refinery lands the PR. So the workflow
    cannot finalize while the PR sits in the gate, and the loop runs for the whole
    duration of the HUMAN approval wait (potentially hours/overnight).

SCOPE: likely affects EVERY mr-mode PR awaiting operator approval, not just this
bead. Corroborating: six "Finalize workflow" step beads are currently open in this
rig (tk-nzuv8, tk-smqq0, tk-sbhp9, tk-f372s, tk-tdey1, tk-x82ck).

DANGER — DO NOT "FIX" BY CLOSING THE STEP BEAD:
Closing load-context to stop the churn would unblock workspace-setup, and the pool
would walk the next polecat forward into implement/self-review on a branch that is
ALREADY green-gated and PR'd. Any push there moves the head, which stales the
anchor's check.codex=green@<oid> marker and BLOCKS the open PR from merging. The
"polecat never closes mol-polecat-work step beads" rule exists for this reason.
The correct fix is at the machinery level, not by hand on live workflow state.

CANDIDATE FIXES (for triage, not prescriptive):

  1. Stop pool-offering steps of a workflow whose root is already parked/waiting —
    e.g. clear/suppress gc.routed_to on step beads once submit-and-exit has run
    (the anchor carries merge_result=pre_open_gate|pull_request, a reliable signal
    that the workflow's inline execution is DONE).
  2. Have the hook skip a ready step whose workflow root's convoy member already
    carries a terminal merge_result.
  3. Finalize the step graph at submit-and-exit time rather than on convoy
    completion, so close-on-land's intentionally-open anchor stops holding the
    whole step graph open.

RELATED: tk-qnsqhv (double-dispatch race — distinct: that is a bead re-pooled
while its original polecat is still live; this is a completed workflow's dead step
re-offered indefinitely).

EVIDENCE ANCHOR: tk-3b5hv / PR #192 (open, non-draft, headRefOid=846c24a,
check.codex=green@846c24a, reviewDecision=REVIEW_REQUIRED — normal awaiting-
external-approval, not a stall).

Implementation notes

Implemented: witness-patrol now auto-quiesces completed mol-polecat-work husks (clears gc.routed_to AND assignee; never closes a step). Validated against live rig state — 12 steps quiesced across 2 done molecules, both re-offer paths confirmed dead, anchors/PR #203 undisturbed. 11/11 test suites green.
RE-OFFER #1 (resumed session gc-toolkit__polecat-lx-vg082, 2026-07-22T08:14Z): load-context husk tk-fgkgk was re-offered on the hook. Verified NO action needed — this is the bug's own symptom, not new work. Anchor state: merge_result=pre_open_gate, branch polecat/tk-p9ji9 pushed at e9f8270, review bead tk-ghlvg OPEN and routed to gc-toolkit/gc-toolkit.polecat-codex (pre-open codex gate queued, in flight). Deliverable re-verified against the required scope in the 2026-07-22 comment: quiesce-completed-workflows.sh handles BOTH shapes (unassigned -> unset gc.routed_to; affine assignee+session_affinity=require -> clears assignee too), gated on anchor merge_result in {pre_open_gate,pull_request,merged}, never closes a step (DANGER clause honored), wired into mol-witness-patrol.toml with a hermetic test. No re-handoff, no re-implement, no close. Drained.
RE-OFFER #2 (same session slot gc-toolkit__polecat-lx-vg082, 2026-07-22T08:19Z): husk tk-fgkgk re-offered via the ASSIGNED-WORK path (gc hook reason=existing_assignment — the assigned shape, so clearing gc.routed_to alone is a no-op). Re-verified: branch polecat/tk-p9ji9 @ e9f8270, local HEAD == origin; commit carries quiesce-completed-workflows.sh + .test.sh + mol-witness-patrol.toml wiring; no PR (correct for pre_open_gate); review bead tk-ghlvg in_progress with codex polecat lx-07t9t, gate in flight and healthy. No action needed. ESCALATED to gc-toolkit.witness (mail lx-wisp-ousk1) to de-route the husk (clear assignee AND gc.routed_to) and sweep siblings of root tk-zleg5; DANGER clause restated — no step bead closed. Future incarnations: escalation is already filed, verify silently and drain; do NOT re-escalate or re-append.

Refinery handoff

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

…uted_to AND assignee so the pool stops re-offering dead steps (tk-p9ji9)

mol-polecat-work runs its 7 step beads INLINE in one session and no step
closes its own bead. The graph is chained, so while load-context stays open
it is the ONLY ready step — and it still carries gc.routed_to=<rig>/<prefix>
polecat. Open + ready + routed is exactly the pool's offer predicate, so every
idle polecat is handed the same dead step, forever. Each burns a wisp to
re-derive "already done" and drain. The molecule cannot finalize itself
either: under close-on-land its anchor stays OPEN until the refinery lands the
PR, so the churn runs for the whole human-approval wait (~1 wisp/4-5min,
~200 overnight on a single anchor). The witness had been containing this BY
HAND, molecule by molecule — ten of them by 2026-07-22.

Automates that containment as a witness-patrol pass.

Two re-offer shapes need two different levers, and clearing gc.routed_to
fixes only the first. Verified live: the pool work query returns open,
UNASSIGNED, routed, ready beads only, so an assigned step never rides the
pool path at all.

  unassigned  assignee empty + gc.routed_to set
              -> the pool offers it; clearing gc.routed_to removes it.
  assigned    assignee=<polecat session> + gc.session_affinity=require
              -> already invisible to the pool; handed back on the ASSIGNED
                 path, keyed on the assignee. Clearing gc.routed_to here is a
                 NO-OP — which is why the by-hand sweeps kept leaving a
                 residual of sessions still holding their in_progress step.

Both keys go in ONE gc bd update. A two-call sequence is unsafe: clearing the
assignee first would briefly leave the bead open+unassigned+routed — the exact
pool-offer shape — racing a fresh polecat into the husk being retired.

DANGER clause preserved: the pass NEVER closes a step bead. Closing
load-context unblocks workspace-setup and walks the next polecat onto a branch
that is already green-gated and PR'd; any push there stales the anchor's
check.<gate>=green@<oid> marker and BLOCKS the open PR. There is no close path
in the script and the test asserts its absence. It also never writes the
anchor, never rewrites status, and never touches workflow-finalize (routed to
the control dispatcher — the molecule's only escape path). An anchor it cannot
resolve is SKIPPED: quiescing a live molecule would strip the assignee off
steps a running polecat still has to claim, draining it mid-implementation,
whereas an un-quiesced husk only wastes wisps.

Quiescing is containment, not finalization — the molecule is left stranded but
quiet, matching what the manual sweep achieves. Finalizing the step graph at
submit-and-exit time is the durable upstream fix (gascity core / gastown
formula) and is deliberately out of scope.

Validated against live rig state, not just a compile check:
  - dry-run selected exactly the completed molecules, skipping 3 live ones
    (including this bead's own in-flight molecule)
  - real pass quiesced 12 steps across 2 completed workflows, one of which
    (tk-c9ivi) crossed into pre_open_gate mid-session and was caught the
    same pass
  - after: the pool offer no longer contains any quiesced step, and both
    churning sessions hold 0 assigned beads, while genuine work still flows
  - anchors untouched (PR #203 still pull_request, codex green at head);
    step status unchanged, nothing closed; finalize routes intact

Tests: assets/scripts/quiesce-completed-workflows.test.sh — 20 assertions,
hermetic. Full suite 11/11 green (264 assertions). shellcheck clean at
severity>=warning.
@zook-bot

Copy link
Copy Markdown
Contributor Author

Codex signoff (pre-open, comment-only — not an approval):

COMMENT (pass): pre-open branch review completed for polecat/tk-p9ji9 -> main at e9f8270.

No blocking findings.

Verification:

  • Merge base: 6d28a15
  • Reviewed files: assets/scripts/quiesce-completed-workflows.sh; assets/scripts/quiesce-completed-workflows.test.sh; doctor/check-base-artifact-collision/run.sh; formulas/mol-witness-patrol.toml
  • quiesce-completed-workflows.test.sh: 20 passed, 0 failed (run from reviewed commit archive)
  • git diff --check: clean
  • mol-witness-patrol.toml: parsed successfully with Python tomllib
  • live dry-run: selected completed workflow husks, including anchor tk-p9ji9, and left live roots alone; no writes performed

Coverage note: no PR exists yet, so no GitHub review was posted. Harness policy did not allow sub-agent spawning without explicit delegation, so this was a direct sequential branch review.

Review artifact: /tmp/compound-engineering/ce-code-review/20260722-082626-55d30e16/report.md

@zook-bot
zook-bot merged commit 6cc6dbf into main Jul 22, 2026
@zook-bot
zook-bot deleted the polecat/tk-p9ji9 branch July 22, 2026 18:13
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