From 41fc4558de0feceb25365a0ad8b2f1f280bace04 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 13 Aug 2026 20:26:03 -0700 Subject: [PATCH] fix(agents): queued subagents falsely fail with 'lost active execution context' after 60s in the swarm queue (#123169) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(agents): queued swarm collectors falsely killed after 60s waiting for a slot The subagent registry sweeper's stale-context reap treats 'no live gateway run context + 60s age' as proof a run died. Queued collectors intentionally have no run context until the swarm FIFO dispatches them, so any collector that waited more than a minute for a slot (or any queued row restored after a gateway restart) was terminated with a fabricated 'subagent run lost active execution context' error while the FIFO later launched the real turn anyway — burning the slot and tokens for a result already reported failed to the parent. Queued rows keep their real owners (scheduler dispatch, kill intents, restore settlement); exclude them from the stale-context reap. * refactor: compact the queued-collector guard to fit the sweeper max-lines cap --- .../registry/subagent-registry-sweeper.ts | 5 ++-- .../subagent-registry.archive.e2e.test.ts | 27 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/agents/subagents/registry/subagent-registry-sweeper.ts b/src/agents/subagents/registry/subagent-registry-sweeper.ts index d7de56a7b020..d87c780e835c 100644 --- a/src/agents/subagents/registry/subagent-registry-sweeper.ts +++ b/src/agents/subagents/registry/subagent-registry-sweeper.ts @@ -364,9 +364,10 @@ export function createSubagentRegistrySweeper(params: { continue; } if (typeof entry.execution.endedAt !== "number") { - const hasLiveRunContext = Boolean(getAgentRunContext(runId)); + // Queued collectors have no run context until FIFO dispatch; the scheduler owns them. + const notStale = entry.execution.status === "queued" || getAgentRunContext(runId); const activeAgeMs = now - (entry.execution.startedAt ?? entry.createdAt); - if (!hasLiveRunContext && activeAgeMs >= STALE_ACTIVE_SUBAGENT_GRACE_MS) { + if (!notStale && activeAgeMs >= STALE_ACTIVE_SUBAGENT_GRACE_MS) { const orphanReason = resolveSubagentRunOrphanReason({ entry }); if (orphanReason) { if ( diff --git a/src/agents/subagents/registry/subagent-registry.archive.e2e.test.ts b/src/agents/subagents/registry/subagent-registry.archive.e2e.test.ts index 88c56ebe7b2c..d20d01e5b42e 100644 --- a/src/agents/subagents/registry/subagent-registry.archive.e2e.test.ts +++ b/src/agents/subagents/registry/subagent-registry.archive.e2e.test.ts @@ -277,6 +277,33 @@ describe("subagent registry archive behavior", () => { }); }); + it("does not reap a queued collector waiting for a swarm slot past the stale grace", async () => { + // Queued collectors have no gateway run context until FIFO dispatch; the + // stale-context reap must not fabricate a lost-context failure for them. + const now = Date.now(); + addCanonicalSubagentRunForTests({ + runId: "run-queued-collector", + childSessionKey: "agent:main:subagent:queued-collector", + requesterSessionKey: "agent:main:main", + requesterDisplayKey: "main", + task: "wait for a swarm slot", + cleanup: "keep", + collect: true, + createdAt: now - 120_000, + execution: { status: "queued" }, + }); + + await mod.testing.sweepOnceForTests(); + + expect(mod.listSubagentRunsForRequester("agent:main:main")[0]).toMatchObject({ + runId: "run-queued-collector", + execution: { status: "queued" }, + }); + expect( + mod.listSubagentRunsForRequester("agent:main:main")[0]?.collectorCompletion, + ).toBeUndefined(); + }); + it("does not archive an active run carrying an obsolete persisted deadline", async () => { vi.mocked(getAgentRunContext).mockReturnValue({} as never); const now = Date.now();