Supervisor drains a polecat within a minute of its claim: assigned work is read from the beads cache, demand from the store, so a fresh claim is invisible to one and already gone from the other for one reconcile cadence #18
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What happened
gc-scratch, 2026-09-06 10:41Z. The celilo polecat pool had two sessions with work (furiosa on ce-013r, nux on ce-1h2o) and one unclaimed routed bead, ce-ilt6. The supervisor spawned
celilo--gastown__rictusfor it (start outcome=success at 10:41:0x). rictus claimed ce-ilt6. Within the same minute the supervisor drained rictus withno-wake-reason, the claim was released, and ce-ilt6 went back to open and unassigned with rictus'swork_dirandgc.session_nameleft on it.The raw lines from
~/.gc/supervisor.log, in order, with the routine assignee rows for named sessions removed:Read it as two queries in one tick that disagree:
assignedWorkBeadslists only two in-progress pool beads. rictus's claim on ce-ilt6 is not there. Theupdates=1in the cache reconcile that follows is that claim arriving.bd ready ... --unassignedprobe against the store) had already stopped counting ce-ilt6, because at the store it was assigned.So for one reconcile cadence the claim is invisible to the instrument that counts sessions with work and already gone from the instrument that counts demand.
poolDesiredcomes out as 2 with three sessions running, and the supervisor drains the one it cannot attribute work to, which is exactly the one that just claimed.How often
Tonight, three fresh polecat claims in a row: two survived (their claims happened to land in the cache before the next
poolDesired), one was drained. The same session name was drained the same way once earlier (celilo--gastown__rictuson ce-ymot at 10:07Z, after an hour of work, with a committed and unpushed fix in its worktree that the mayor had to recover by hand). Across the last 200k log lines,no-wake-reasondrains: rictus 4, furiosa 2, nux 1, slit 1.Cost
Every hit costs a full polecat start (about 15s plus a worktree and a provider session), the polecat's context, and in the ce-ymot case nearly a lost fix. The bead is left carrying
work_dirandgc.session_namefrom the dead claim, which is the input to celilo/gascity#7's evidence check, so the two defects feed each other: the drained claim re-arms #7 on the next spawn.What would fix it
Compute
poolDesiredfrom one snapshot. Either read both the assigned set and the demand set from the beads cache, or both from the store, in the same tick. Or treat a session younger than one reconcile cadence as having work until the cache catches up. A drain decision should never rest on two reads taken across a reconcile boundary.Not related to #7
#7 is the worktree-evidence validation on pool triggers. This one fires with no
evidence invalidline in the tick that drains. Recorded separately so a fix for #7 is not read as covering it.Update, 11:10Z: it is not only at claim time. A successful spawn drains live neighbours, and the trigger-bead mapping is wrong.
The spawn and no-wake-reason drain events for the celilo polecat pool tonight, in log order (
~/.gc/supervisor.log, line numbers):Four spawns, three followed within a tick by a drain. At the 11:03Z tick the
assignedWorkBeadsblock listed bothce-1h2o assignee=celilo/gastown.nux status=in_progressandce-013r assignee=celilo/gastown.furiosa status=in_progress, so the cache-lag explanation in the body does not cover this one. What the log shows instead:gs-mguyjzis capable, which held ce-kg5o. The supervisor is validating worktree evidence against a trigger bead that is not the session's claim, and the #7 rejection then removes a live session from the desired set. So #7 and this issue are one mechanism seen from two sides: #7 blocks a spawn when the trigger bead carries a barework_dir; here the same check, applied to a running session with a mismapped trigger, drains it.Whether the bead survives is a coin flip. furiosa kept ce-013r because the assignee is the agent name and the respawned session matched it. nux's claim on ce-1h2o was released, and its kept e2e stack (nine containers) stayed up under cele2e's flock with a dead runner pid, which is the stranded-rig shape from a new door.
Operating consequence recorded in gc-scratch CLAUDE.md: lifting a hold is not free while polecats are working, because the spawn it causes may kill one of them.
12:50Z to 12:58Z, a batch into an empty pool. Three beads lifted at once into a pool with zero sessions. Three polecats spawned (nux, rictus, slit). nux was drained
no-wake-reasonbefore it claimed anything and respawned. rictus and slit spent five minutes reading (slow provider) without claiming. The respawned nux claimed ce-cbr3 at 12:57Z, and on the next tick both rictus and slit were drainedno-wake-reason, still unclaimed, with four routed unheld beads available to them andpoolDesiredreading 2 and 3 in alternation.So the drain fires on any pool membership change, not only on a spawn: a neighbour's claim is enough. Sessions that have not yet claimed are drained as surplus even while demand exceeds the running count. gc-scratch is now operating the celilo pool at size one until this is fixed.
Another instance, measured 2026-09-06T17:53Z to 17:58Z by the celilo refinery, with an unusually clean trigger.
A mistaken
gc sling celilo/gastown.polecat 0(the literal text0, which sling turns into an auto-created task bead) created ce-y1e4, open, routed, unassigned. Supervisor log, in order:furiosa was mid-suite on ce-21hp (aspect-fanout-new-systems,
--keep, holding the city's e2e lock). Its claim was released: ce-21hp went back to open, unassigned, routed, withgc.e2e_lock=heldstill on it, while the cele2e run lock stayed BUSY on furiosa's kept stack (pid 25007, heartbeat live). So this is the "claim released" side of the coin flip the issue describes, and the trigger bead was junk that was closed two minutes after creation. One spawn, one drained worker, one orphaned kept stack.The refinery put
hold:mayoron ce-21hp to stop the released claim reading as demand for a second spawn (which would have drained nux).2026-09-07 19:55Z: the shed picked the session that HAD the claim, and kept the one that did not.
This one does not fit either explanation already on the issue. The body blames cache lag, where the fresh claim is invisible. Comment 6988 blames a trigger bead mismapped to the wrong session. Here the bookkeeping was correct in the drain tick itself, and the supervisor still drained the working session.
The sequence from
~/.gc/supervisor.log, routine rows removed:The
assignedWorkBeadsblock in the draining tick listed nux's claim correctly:So at the moment of the drain:
ce-5kg5in_progress, correctly routed, and the supervisor listed it.postCreateProtectionTimeout(2m,cmd/gc/city_runtime.go:342), and had no claim in that tick'sassignedWorkBeadsat all.poolDesiredfell from 2 to 1 across arig=cereconcile reportingadds=0 updates=0 removes=0, so no bead in the celilo store changed between the tick that said 2 and the tick that said 1.Why this matters more than the count being wrong. Even granting that
poolDesiredshould have been 1, the choice of victim is the damaging part. A pool that must shed a session should shed one that is idle before one that holds in-progress work, and the resume tier already knows which is which: it is built from exactly the beads whose assignee resolves to a live session (computePoolDesiredStatesAt,cmd/gc/pool_desired_state.go:290). The information needed to pick correctly is computed in the same function that produces the number.What it cost here. nux's session died with commit
10a32ed8sitting unpushed in its worktree. The claim itself survived, because the assignee is the agent namecelilo/gastown.nuxand the respawn matched it, so this is the lucky side of the coin flip described in comment 6988. The worktree is per agent name, so the commit was still there afterwards. What was lost is the session and its context, plus a 20s restart. Had the operator not looked, the unpushed commit would have been one more drain away from a hand recovery, which is what happened to rictus on ce-ymot.What I could not determine from the log, and I am not going to guess: why
poolDesireddropped to 1. Nothing in the celilo store changed in that reconcile, and furiosa was inside its protection window, so neither the cache-lag path nor the protection-window expiry explains it on its face.recordReconcileTraceInputs(cmd/gc/city_runtime.go:2586) already capturesdesiredState,poolDesiredandworkSetper tick. Dumping that trace for one drain tick would settle both halves at once: what the count was built from, and which request the accepted slot named.Filed from gc-scratch, which continues to operate the celilo polecat pool at size one to two for this reason.
Counter-measurement, 2026-09-09: five simultaneous releases into an empty pool drained nothing
Run deliberately at peba's request to test the "never batch" rule this issue produced. One trial, reported with its limits.
Method
Baseline at 01:37:35Z, chosen to reproduce the condition the earlier finding called worst — an empty pool:
At 01:38:06Z, five celilo beads were released together (
hold:mayorremoved,gc.routed_toset tocelilo/gastown.polecat):ce-tgz7,ce-vchn,ce-0f5p,ce-cq3i,ce-6t2s. Demand was confirmed with the supervisor's own exclusions (--unassigned --exclude-label hold:mayor --exclude-label hold:external), not a naive ready query, which ignores the hold labels and overcounts.Result
All five seats came up:
First claim 01:39:56Z, all five holding claims by 01:47:51Z:
Zero drains cost a claim. Over the next 30 minutes the pool cycled normally: polecats finished, handed branches to the refinery, respawned, and took the next queued bead. Three beads went from release to pushed-and-in-the-merge-gate inside the window.
The one drain that did touch live work, and what triggered it
At ~01:41
rictuswas drained mid-claim. The cause was mine: I routed a sixth bead (ce-prr5) at 01:40:50, about ten seconds earlier. I had assumed a sixth bead was harmless because the pool caps at 5 — wrong, because it is the spawn request that drains, not the seat count.rictusrespawned in 20.5s and keptce-vchn, consistent with this issue's note that the assignee is the agent name so a respawn under the same name re-matches the claim.A second drain,
furiosaat ~02:09, held no claim at all — it had already handed off, idled, and drained. Benign. Counting all drains rather than drains that cost work will overstate this issue's impact.What this suggests, and what it does not
Suggests: the trigger is adding demand while polecats are mid-work, not batch size at release. Releasing N into an idle pool behaved fine at N=5. If that holds, the one-at-a-time throttle described in this issue's earlier comments costs four of five seats to avoid a hazard that fires on a different action.
Does not establish: this is a single trial. It should be repeated before the operating rule changes.
A likely confounder in the earlier measurements, worth checking
The 2026-09-06 observations were taken on a city that may have been in a different state. Tonight, before this run, celilo's pool was spawning nothing at all because an unrelated rig with an uninitialised beads store made the demand read PARTIAL city-wide and
buildDesiredStaterefuses fresh creates on a partial read (filed as #24). Spawn behaviour measured while that interlock was tripping intermittently would not be comparable. Anyone re-testing should first confirm~/.gc/supervisor.logcarries nodemand read partialorfresh create blockedlines for the window.gc worktree ensurewrites, so a pack using worktree-setup.sh stalls its pool permanently #7Measured again on 2026-09-11: three drains of polecats holding claims, in 16 minutes. Two of them held uncommitted work. Times are UTC.
Context. The OpenRouter account ran out of credits at 18:58 and every pi agent stalled. Credits came back at 20:21, and the mayor typed a resume message into the refinery and four polecat panes at 20:21:12.
furiosa, a fresh spawn (gs-ouxuc7, transcript starts 20:22:46). It claimed ce-d5q5 at 20:23:40 and read the claim back at 20:23:48 (in_progress, assignee celilo/gastown.furiosa). supervisor.log then has
Draining session 'celilo--gastown__furiosa': no-wake-reason, and ce-d5q5 was open and unassigned again at 20:24:48. About a minute from claim to release, the shape in this issue's title.nux claimed ce-wlrd at 20:22:04 and worked it for seven minutes. Its last transcript entry is a file edit at 20:29:19. supervisor.log has
Draining session 'celilo--gastown__nux': no-wake-reason, and ce-wlrd was open and unassigned at 20:29:31. Two new pool sessions (slit gs-mjbr3n, furiosa gs-nmjwdj) were live by about 20:31. nux left four modified files and two new ones uncommitted, and had pushed nothing.slit (gs-mjbr3n) claimed ce-d5q5 at about 20:30. Its last transcript entry starts a typecheck at 20:37:15, right after an edit. supervisor.log has
Draining session 'celilo--gastown__slit': no-wake-reason. The pool read one live polecat at 20:39:09, a new furiosa session's transcript starts at 20:39:18, and that session held ce-d5q5 by 20:39:50. slit left eight files uncommitted (four modified, four new).No work was lost, and only because the next polecat chose to salvage it. One furiosa finished nux's work inside nux's worktree. The next copied slit's eight files into its own worktree, then ran
git worktree remove --forceon slit's worktree. That removal stopped partway and left 846 MB at slit's path, which worktree-setup.sh stages aside on the next spawn (the non-empty-directory branch at line 99).Every surface read healthy throughout. The only records are the pi transcripts under ~/.pi/agent/sessions/ and ~/.gc/supervisor.log, which needs
rg -abecause the file holds NUL bytes.(mayor, 2026-09-11)
gc order checkreads "ready" for 185 hours while the order never fires #32gc order checkreads "ready" for 185 hours while the order never fires #32