Skip to content

fix(mol-witness-patrol): liveness lookup is exact-match on assignee, so rig-qualified bead-host owners resolve 'absent' and are false-orphaned (tk-opfsi)#210

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

fix(mol-witness-patrol): liveness lookup is exact-match on assignee, so rig-qualified bead-host owners resolve 'absent' and are false-orphaned (tk-opfsi)#210
zook-bot merged 1 commit into
mainfrom
polecat/tk-opfsi

Conversation

@zook-bot

Copy link
Copy Markdown
Contributor

Summary

Sibling to tk-2l13a — same root cause, DIFFERENT code path, and this one is NOT latent.
tk-2l13a fixes the orphan-sweep.sh path by stamping gc.session_id on owned
beads (that script's session_bead_candidates() reads bead metadata). That
stamp cannot reach this path
, so tk-2l13a alone leaves live bead-hosts exposed.

The defect (verified at file:line, gascity witness + mayor independently)

rigs/gc-toolkit/formulas/mol-witness-patrol.toml, recover-orphaned-beads step:

STATE=$(printf '%s' "$LIVENESS_MAP" | jq -r --arg a "$ASSIGNEE" '.[$a] // "absent"')

That is an exact key lookup of the raw assignee string against a
session-derived map. It never reads bead metadata. Bead-host sessions
register a bare alias while their beads carry a rig-qualified assignee,
so the lookup always falls through to // "absent":

session s-lx-wisp-q5qbl   alias    = gc-toolkit.gc-z0vi2          (map key)
bead    gc-z0vi2.1        assignee = gascity/gc-toolkit.gc-z0vi2  (lookup)

absent is then classified orphaned — "the owning session is gone and will
never come back" — for a session that is active and running.

Why this is worse than tk-2l13a's exposure

  • tk-2l13a's path (orphan-sweep.sh) collects only in_progress beads, so its
    exposure is latent until a bead-host takes work in-progress.
  • This path enumerates BOTH (mol-witness-patrol.toml:135-136):
    gc bd list --status=in_progress --json --limit=0
    gc bd list --status=open --json --limit=0
    So open beads are in scope and the exposure is live right now.
  • CONFIRMED FIRING: gc-z0vi2.1 (status=open, owner active+running) resolved
    absent this cycle. The gascity witness recognised it and did NOT act —
    containment held by judgment, not by code.

Why the existing guards do NOT catch it

  1. The fail-safe at ~:210 trips only on a totally empty liveness map
    ("empty liveness map with N live sessions — skipping orphan recovery").
    Ours is fully populated, just mis-keyed, so it never fires.
  2. The step-2 guard skips infrastructure identities (refinery/witness/etc).
    Bead-hosts are pool/ephemeral work identities, so they are not skipped.

Fix

Normalise identity in the lookup instead of requiring an exact match. Either:
(a) build LIVENESS_MAP with BOTH the bare alias and the rig-qualified form as
keys, or
(b) on an absent result, retry the lookup on the assignee's last path segment
(gascity/gc-toolkit.gc-z0vi2 -> gc-toolkit.gc-z0vi2) before classifying.
(b) is the smaller change and is fail-safe in the right direction — it can only
turn absent into a live state, never the reverse.

Acceptance

With a bead-host live and a bead assigned to it (status open AND again with
in_progress), a recover-orphaned-beads pass must classify it live, not
orphaned. Regression-guard the rig-qualified-vs-bare case explicitly.

Scope note

gc-toolkit-owned formula, so this can land while the gascity fork shed is
running. The durable core fix (normalising live_session_match() in gascity's
orphan-sweep.sh) is deliberately deferred until the shed settles — see
gascity bead gc-uuz79w, which carries the full evidence plus the complementary
FALSE-NEGATIVE in the same sweep (it skips unassigned beads entirely).

Reported by gascity/gc-toolkit.witness on tk-2l13a; re-verified at file:line by mayor.

Implementation notes

Implemented: liveness lookup in mol-witness-patrol recover-orphaned-beads now normalizes identity — exact lookup stays the authoritative first pass, then one retry on the last '/'-separated segment, resolving bare-form collisions toward life. Live replay against the real 69-key map: gascity/gc-toolkit.gc-z0vi2 absent->active, gc-toolkit.furiosa absent->active; exact hits and unknown identities unchanged. New assets/scripts/liveness-lookup.test.sh (30 assertions, extracted verbatim from the formula markers) passes; husk-guard.test.sh still 16/16. Branch polecat/tk-opfsi @ e9731cb.
Re-offer verified (gc-toolkit.furiosa, new session after lx-fjnq1 died): work already submitted — origin/polecat/tk-opfsi @ e9731cb matches local handoff, diff = formulas/mol-witness-patrol.toml (+49/-8) + assets/scripts/liveness-lookup.test.sh (new, regression-guards the rig-qualified-vs-bare case per acceptance). Bead correctly parked with refinery (target=main, gc.routed_to cleared). No re-handoff, no re-implement, no close. Cause of re-offer: graph.v2 husk tk-z57tr's load-context step tk-1ta4k never closed (inline execution), so the pool re-offers it while this bead stays open.
Re-offer #2 verified (gc-toolkit.furiosa): origin/polecat/tk-opfsi @ e9731cb, ahead 1 / behind 0 vs main, files = formulas/mol-witness-patrol.toml (+41/-8) + assets/scripts/liveness-lookup.test.sh (+188). Bead correctly parked with refinery at merge_result=pre_open_gate (no PR yet is CORRECT for pre-open). No re-handoff, no re-implement, no close. ESCALATED ONCE to gc-toolkit.witness (mail lx-wisp-9jsfk) to retire husk root tk-z57tr and de-route never-closed step tk-1ta4k. A future re-offer should verify silently and drain — do NOT re-escalate, do NOT re-implement.
CORRECTION (same session, gc-toolkit.furiosa): the preceding note's claim 'parked with refinery (assignee=refinery)' was a stale snapshot. Dolt history (tk.dolt_history_issues) shows the refinery parked this anchor at 14:55:22 (commit a9gmlp76): assignee CLEARED deliberately, metadata gained check_set=codex + merge_result=pre_open_gate + merged_target=main, after dispatching pre-open codex review bead tk-iqlir (blocks tk-opfsi, claimed 14:55:33 by gc-toolkit__polecat-codex-lx-kz313). Empty assignee + empty gc.routed_to is the CORRECT pre-open park shape here — the anchor is held by the check-set marker and the live review bead's blocks edge, not by an assignee, and it closes on merge via refinery reconcile. I briefly re-assigned it to the refinery at 14:57:31 (misreading the empty assignee as a strand) and have reverted that. Do NOT re-assign this anchor while tk-iqlir is open.

Refinery handoff

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

… rig-qualified assignees stop false-orphaning live sessions (tk-opfsi)

recover-orphaned-beads resolved a bead's assignee against the session
liveness map with a single exact key lookup, `.[$a] // "absent"`. The map
is keyed on session-registered identities, and those routinely differ
from the bead's assignee in nothing but the `<rig>/` prefix, so the
lookup fell through to `absent` — which step 3 classifies as orphaned,
"the owning session is gone and will never come back", for a session that
is active and running. Confirmed firing on gc-z0vi2.1 (status=open, owner
active); containment held only because the witness agent noticed by
judgment. Neither existing guard catches it: the fail-safe trips only on a
totally empty map (ours is fully populated, just mis-keyed) and the
infrastructure skip does not cover pool/ephemeral bead-hosts. Unlike the
sibling orphan-sweep.sh path (tk-2l13a), this step enumerates `open` beads
too, so the exposure was live rather than latent — and tk-2l13a's
`gc.session_id` stamp cannot reach this path, which never reads bead
metadata.

Takes the bead's option (b) — retry on the last path segment — over (a)
rebuilding the map with both key shapes, because it is the smaller change
and only ever turns `absent` into a live state. Two things the report did
not cover, both from live `gc session list` output: the mismatch runs in
BOTH directions in the same city (wisp bead-hosts register a bare alias
while their beads are rig-qualified; pool polecats register a
rig-qualified alias and may be addressed bare), so the retry normalizes
both sides rather than only stripping the assignee's prefix; and stripping
the prefix can collide two cities' sessions on one bare identity
(gc-toolkit/gc-toolkit.furiosa and gascity/gc-toolkit.furiosa), where
picking an arbitrary match could report a dead state for a live agent and
re-open the bug through the fix. The retry therefore resolves a collision
toward life: any matching session that is not closed/archived wins, and
only when EVERY candidate owner is dead does it report a dead state. The
exact lookup stays the authoritative first pass, so a genuinely dead
session that hits a literal map key is still recovered.

Validation: assets/scripts/liveness-lookup.test.sh (new, 30 assertions)
executes the guard extracted verbatim from the formula between
`liveness-lookup` markers — the same drift-proof pattern as
husk-guard.test.sh — covering the confirmed-firing shape, the reverse
shape, exact-match authority over a collision, unknown identities still
resolving absent, order-independent collision preference across every live
state in the classification list, all-dead candidates staying dead,
whole-segment (not suffix) matching, and degenerate inputs. Static checks
assert the formula keeps exactly one exact-match lookup, that the retry is
gated on the exact pass missing, and that the classification prose no
longer authorizes orphaning on the exact lookup alone. Also replayed
against the live 69-key liveness map: gascity/gc-toolkit.gc-z0vi2 goes
absent -> active, gc-toolkit.furiosa goes absent -> active, exact hits and
genuinely unknown identities are unchanged. husk-guard.test.sh still
passes (16/16); shellcheck is clean apart from the accepted info-level
SC2015/SC2016 idioms.

Scope: gc-toolkit's own copy of the formula (not symlinked to other rigs).
The durable core fix — normalizing `live_session_match()` in gascity's
orphan-sweep.sh, plus the complementary false-negative where that sweep
skips unassigned beads — stays deferred to gascity bead gc-uuz79w until
the fork shed settles.
@zook-bot

Copy link
Copy Markdown
Contributor Author

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

COMMENT signoff for pre-open branch review.

Reviewed branch: polecat/tk-opfsi
Reviewed OID: e9731cb
Base: main
Verdict: Ready to merge; no blocking findings.

Scope reviewed:

  • formulas/mol-witness-patrol.toml
  • assets/scripts/liveness-lookup.test.sh

Validation:

  • git diff --check origin/main...origin/polecat/tk-opfsi: passed
  • assets/scripts/liveness-lookup.test.sh from archived reviewed branch: 30 passed, 0 failed
  • assets/scripts/husk-guard.test.sh from archived reviewed branch: 16 passed, 0 failed
  • shellcheck unavailable in this environment

Review artifact: /tmp/compound-engineering/ce-code-review/20260722-150015-b90f0568/report.md

@zook-bot
zook-bot merged commit 0a7fb42 into main Jul 22, 2026
@zook-bot
zook-bot deleted the polecat/tk-opfsi branch July 22, 2026 18:02
zook-bot added a commit that referenced this pull request Jul 22, 2026
…live teardown (tk-z130v.3)

Ground gc-toolkit bead-hosts so an involuntary config-drift drain no longer
kills them. gc-bead-host.sh 'link' sets the work bead's assignee to the host's
session NAME — the assigned-work wake reason the reconciler honors across drains
(compute_awake_set has no Drained gate) — and 'unlink' / gc-helm 'takeaway
--release' clear it so a host can still be stopped. New 'backfill' grounds hosts
linked before this shipped; new/resumed hosts ground automatically via 'up'.

- tools/gc-bead-host.sh: link grounds, unlink ungrounds, + backfill migration verb
- agents/bead-host/{prompt.template.md,agent.toml}: warm-while-live model — a
  grounded host stays warm while its bead has awake-demand; teardown clears the
  wake reason (close/park/release) FIRST, then drains
- assets/scripts/gc-helm.sh: note that --release's assignee-clear also ungrounds
- formulas/mol-witness-patrol.toml: recover-orphaned-beads skips grounded host
  beads (assignee==host_session_name) — safe-by-code, not judgment (cf #206/#210)
- tests: bead-host-binding-fixture.sh grounding assertions; host-bead-skip.test.sh

Guardrails verified (scale-check/keeper/pool/refinery/reconcile key on
gc.routed_to/branch/merge_result/own-identity, never a session-name assignee).
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