fix(agents): queued subagents falsely fail with 'lost active execution context' after 60s in the swarm queue (#123169)

* 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
This commit is contained in:
Peter Steinberger
2026-08-13 20:26:03 -07:00
committed by GitHub
parent f3db7fe511
commit 41fc4558de
2 changed files with 30 additions and 2 deletions
@@ -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 (
@@ -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();