From 15acb1243cf54227a8d1c1e6a54f6de21b1e7bf2 Mon Sep 17 00:00:00 2001 From: John Fawcett Date: Fri, 10 Apr 2026 21:45:31 +0000 Subject: [PATCH 1/2] fix(gastown): prevent triage batch bead dispatch loop with wrong system prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Option A: Mark triage batch bead as in_progress immediately after hookBead() in maybeDispatchTriageAgent(), before awaiting startAgentInContainer(). This prevents reconciler Rule 2 (idle agent + open hooked bead → dispatch_agent) from re-dispatching the triage bead with the generic polecat prompt on the next tick when container start fails. Rule 3 (stale in_progress, 5-min timeout) resets it to open for a clean retry via maybeDispatchTriageAgent. Option B (defense-in-depth): In applyActionCtx.dispatchAgent, detect triage batch beads (gt:triage label + created_by='patrol') and inject the triage system prompt, ensuring the polecat gets the correct tools even if Rule 2 somehow fires against an open triage batch bead. Fixes #1958 --- services/gastown/src/dos/Town.do.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/services/gastown/src/dos/Town.do.ts b/services/gastown/src/dos/Town.do.ts index 75a654ccf1..a99bae7e77 100644 --- a/services/gastown/src/dos/Town.do.ts +++ b/services/gastown/src/dos/Town.do.ts @@ -343,6 +343,17 @@ export class TownDO extends DurableObject { }); } + // Option B (defense-in-depth): If the reconciler re-dispatches an + // open triage batch bead (gt:triage, created_by='patrol') — e.g. + // because Option A's in_progress transition was somehow bypassed — + // inject the triage system prompt so the polecat gets the correct + // tools and instructions instead of the generic polecat prompt. + if (bead.labels.includes(patrol.TRIAGE_BATCH_LABEL) && bead.created_by === 'patrol') { + const pendingRequests = patrol.listPendingTriageRequests(this.sql); + const { buildTriageSystemPrompt } = await import('../prompts/triage-system.prompt'); + systemPromptOverride = buildTriageSystemPrompt(pendingRequests); + } + return scheduling.dispatchAgent(schedulingCtx, agent, bead, { systemPromptOverride, }); @@ -4076,6 +4087,14 @@ export class TownDO extends DurableObject { const triageAgent = agents.getOrCreateAgent(this.sql, 'polecat', rigId, this.townId); agents.hookBead(this.sql, triageAgent.id, triageBead.bead_id); + // Option A: Immediately mark the triage batch bead as in_progress so + // the reconciler's Rule 2 (idle agent + open hooked bead → dispatch_agent) + // does not re-fire on the next tick if the container start fails. Rule 3 + // (stale in_progress bead + no working agent + 5-min timeout) will reset + // it back to open if the dispatch fails, allowing a clean retry via + // maybeDispatchTriageAgent with the correct triage system prompt. + beadOps.updateBeadStatus(this.sql, triageBead.bead_id, 'in_progress', triageAgent.id); + const started = await dispatch.startAgentInContainer(this.env, this.ctx.storage, { townId: this.townId, rigId, From e5b1d8d1ea66b67c16f66cf333b4c711997866eb Mon Sep 17 00:00:00 2001 From: John Fawcett Date: Fri, 10 Apr 2026 21:53:27 +0000 Subject: [PATCH 2/2] fix(gastown): set rig_id on triage batch bead so reconciler Rule 1 can re-dispatch after timeout Without rig_id, when Rule 3 resets an abandoned in_progress triage batch bead to 'open', Rule 1 skips it (requires rig_id IS NOT NULL). This left the bead permanently 'open', blocking maybeDispatchTriageAgent from creating a replacement. Setting rig_id ensures Rule 1 can re-dispatch the existing bead (with triage system prompt injected via Option B). --- services/gastown/src/dos/Town.do.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/gastown/src/dos/Town.do.ts b/services/gastown/src/dos/Town.do.ts index a99bae7e77..e54be7fa36 100644 --- a/services/gastown/src/dos/Town.do.ts +++ b/services/gastown/src/dos/Town.do.ts @@ -4075,6 +4075,9 @@ export class TownDO extends DurableObject { const systemPrompt = buildTriageSystemPrompt(pendingRequests); // Only now create the synthetic bead — preconditions are verified. + // Set rig_id so that if Rule 3 resets this bead to 'open' after a + // dispatch timeout, Rule 1 of the reconciler can pick it up and + // re-dispatch it (with the correct triage system prompt via Option B). const triageBead = beadOps.createBead(this.sql, { type: 'issue', title: `Triage batch: ${pendingCount} request(s)`, @@ -4082,6 +4085,7 @@ export class TownDO extends DurableObject { priority: 'high', labels: [patrol.TRIAGE_BATCH_LABEL], created_by: 'patrol', + rig_id: rigId, }); const triageAgent = agents.getOrCreateAgent(this.sql, 'polecat', rigId, this.townId);