You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Agent assignment (hooking) is eager — it happens at bead creation time, not when the bead is ready to be worked on. slingBead() and slingConvoy() both call getOrCreateAgent() + hookBead() immediately for every bead, including beads blocked by dependencies.
For a 5-bead convoy (A → B → C → D → E), this creates 5 polecats at sling time. Only A can run — B through E sit idle with agents hooked but not dispatched, consuming name pool slots and unable to be reused for other work.
The scheduler (schedulePendingWork in scheduling.ts:250-321) never assigns agents to beads — it only dispatches agents that are already hooked. It's a retry mechanism, not a scheduling mechanism.
Why This Is Wrong
Wasted agent slots — idle polecats hooked to blocked beads can't accept other work. A convoy of 10 beads ties up 10 agents even though only 1-3 are running at any time.
Name pool exhaustion — the 20-name polecat pool fills up with idle agents. After 20 concurrent hooked beads (across all rigs), new polecats get generic Polecat-N names.
Premature commitment — if the convoy is cancelled after bead A completes, 4 agents were created for nothing. If bead priorities change, the hooked agent can't be reassigned without unhooking.
1. Remove eager assignment from slingBead and slingConvoy
slingBead() should:
Create the bead with status = 'open', assignee_agent_bead_id = null
Arm the alarm to trigger schedulePendingWork on the next tick
Not call getOrCreateAgent or hookBead
slingConvoy() (non-staged) should follow the same pattern as staged — create all beads with no agents.
2. Extend schedulePendingWork to assign unhooked beads
Currently, the scheduler only queries for idle agents with current_hook_bead_id IS NOT NULL. Add a second query:
SELECT b.*FROM beads b
WHEREb.status='open'ANDb.assignee_agent_bead_id IS NULLANDb.typeIN ('issue', 'molecule')
AND NOT EXISTS (
SELECT1FROM bead_dependencies bd
JOIN beads blocker ONbd.depends_on_bead_id=blocker.bead_idWHEREbd.bead_id=b.bead_idANDbd.dependency_type='blocks'ANDblocker.status NOT IN ('closed', 'failed')
)
ORDER BYb.priorityDESC, b.created_atASC
For each unblocked, unassigned bead:
getOrCreateAgent() — find or create a polecat
hookBead() — assign the agent
dispatchAgent() — start the work
This is the same hook + dispatch that slingBead does today, just moved to the scheduler where it belongs.
3. Extend dispatchUnblockedBeads to handle unhooked beads
When a blocker closes and beads become unblocked, dispatchUnblockedBeads() currently assumes the bead already has an agent (checks assignee_agent_bead_id). It should also handle the case where the bead has no agent — call getOrCreateAgent + hookBead before dispatching.
4. Keep feedStrandedConvoys as a safety net
The patrol function that catches beads with no agent should remain — it's the catch-all for any edge case where the scheduler misses a bead.
Benefits
Agents only created when needed — a 10-bead serial convoy creates 1 agent at a time, not 10
Name pool preserved — polecats aren't wasted on blocked beads
Consistent with staged convoys — all creation paths behave the same way
Cancellation is clean — cancel a convoy and no unnecessary agents exist
Acceptance Criteria
slingBead() creates beads with no agent — no getOrCreateAgent or hookBead
slingConvoy() (non-staged) creates beads with no agent
schedulePendingWork() picks up unblocked, unassigned beads and assigns + dispatches them
dispatchUnblockedBeads() handles beads with no pre-assigned agent
Serial convoy beads get agents one at a time as they become unblocked
No regression: beads still get dispatched promptly (within one alarm tick of becoming unblocked)
feedStrandedConvoys safety net retained
Notes
No data migration needed
The alarm interval (5s active, 30s idle) means beads may wait up to one tick before getting an agent. This is acceptable — the current fire-and-forget dispatch already has this latency when it fails.
The slingBead fire-and-forget dispatch (Town.do.ts:1609) is an optimization to avoid waiting for the next alarm tick. We could keep an optional "dispatch immediately if possible" path while still making the scheduler the primary assignment mechanism.
Preserving the "Toast is on it!" UX: For single beads with no blockers, the gt_sling handler can run the scheduler inline — assign an agent and dispatch synchronously, returning the agent info in the response. The Mayor gets the agent name immediately and can say "Toast is on it!" without any async delay. For convoy beads and blocked beads, the response returns agent: null and the Mayor announces assignments as they happen via hooked events. This gives us the best of both worlds: lazy assignment as the architectural pattern, but instant feedback for the common single-bead case.
Agent name uniqueness is cosmetic, not functional: allocatePolecatName() enforces unique names per rig, but the real identifier is the bead_id UUID. There's no technical reason you can't have multiple "Toasts" — names could be display names rather than unique identifiers. This becomes more interesting with the personas concept in Cloud Gastown: Future ideas — capabilities unique to or enhanced by the cloud model #447, where "Toast the Frontend Master" is meaningful and you might want 3 of them running in parallel.
Parent
Part of #204 (Phase 4: Hardening)
Problem
Agent assignment (hooking) is eager — it happens at bead creation time, not when the bead is ready to be worked on.
slingBead()andslingConvoy()both callgetOrCreateAgent()+hookBead()immediately for every bead, including beads blocked by dependencies.For a 5-bead convoy (A → B → C → D → E), this creates 5 polecats at sling time. Only A can run — B through E sit idle with agents hooked but not dispatched, consuming name pool slots and unable to be reused for other work.
The scheduler (
schedulePendingWorkinscheduling.ts:250-321) never assigns agents to beads — it only dispatches agents that are already hooked. It's a retry mechanism, not a scheduling mechanism.Why This Is Wrong
Polecat-Nnames.staged: true) correctly defer agent assignment. Non-staged convoys andslingBeaddon't.Current Assignment Points
slingBead()(Town.do.ts:1600-1601)slingConvoy()non-staged (Town.do.ts:2269-2270)slingConvoy()staged (Town.do.ts:2258-2264)agent: nullstartConvoy()(Town.do.ts:2349-2355)schedulePendingWork()(scheduling.ts:250-321)dispatchUnblockedBeads()(scheduling.ts:214-237)feedStrandedConvoys()(patrol.ts:564-565)Solution: Lazy Assignment via the Scheduler
1. Remove eager assignment from
slingBeadandslingConvoyslingBead()should:status = 'open',assignee_agent_bead_id = nullschedulePendingWorkon the next tickgetOrCreateAgentorhookBeadslingConvoy()(non-staged) should follow the same pattern as staged — create all beads with no agents.2. Extend
schedulePendingWorkto assign unhooked beadsCurrently, the scheduler only queries for
idleagents withcurrent_hook_bead_id IS NOT NULL. Add a second query:For each unblocked, unassigned bead:
getOrCreateAgent()— find or create a polecathookBead()— assign the agentdispatchAgent()— start the workThis is the same hook + dispatch that
slingBeaddoes today, just moved to the scheduler where it belongs.3. Extend
dispatchUnblockedBeadsto handle unhooked beadsWhen a blocker closes and beads become unblocked,
dispatchUnblockedBeads()currently assumes the bead already has an agent (checksassignee_agent_bead_id). It should also handle the case where the bead has no agent — callgetOrCreateAgent+hookBeadbefore dispatching.4. Keep
feedStrandedConvoysas a safety netThe patrol function that catches beads with no agent should remain — it's the catch-all for any edge case where the scheduler misses a bead.
Benefits
Acceptance Criteria
slingBead()creates beads with no agent — nogetOrCreateAgentorhookBeadslingConvoy()(non-staged) creates beads with no agentschedulePendingWork()picks up unblocked, unassigned beads and assigns + dispatches themdispatchUnblockedBeads()handles beads with no pre-assigned agentfeedStrandedConvoyssafety net retainedNotes
slingBeadfire-and-forget dispatch (Town.do.ts:1609) is an optimization to avoid waiting for the next alarm tick. We could keep an optional "dispatch immediately if possible" path while still making the scheduler the primary assignment mechanism.gt_slinghandler can run the scheduler inline — assign an agent and dispatch synchronously, returning the agent info in the response. The Mayor gets the agent name immediately and can say "Toast is on it!" without any async delay. For convoy beads and blocked beads, the response returnsagent: nulland the Mayor announces assignments as they happen viahookedevents. This gives us the best of both worlds: lazy assignment as the architectural pattern, but instant feedback for the common single-bead case.allocatePolecatName()enforces unique names per rig, but the real identifier is thebead_idUUID. There's no technical reason you can't have multiple "Toasts" — names could be display names rather than unique identifiers. This becomes more interesting with the personas concept in Cloud Gastown: Future ideas — capabilities unique to or enhanced by the cloud model #447, where "Toast the Frontend Master" is meaningful and you might want 3 of them running in parallel.