From b124b79a1252e4cf5f79ad3555ae2d995b4f3f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Fri, 20 Mar 2026 14:29:38 -0600 Subject: [PATCH] fix: cancel trailing async jobs on session switch to prevent wasted LLM turns (#1643) When a unit spawns background jobs via async_bash, job completion callbacks fire follow-up messages after agent_end has resolved. The auto-loop has moved on but the previous session's LLM processes these follow-ups, adding 12-45s of wasted time and ~14 unnecessary turns per unit. Two complementary fixes: 1. Cancel all running background jobs on session_before_switch so completion callbacks never fire for the old session. 2. Clear the follow-up queue after runUnit() completes as defense-in-depth, discarding any already-queued notifications before the next session starts. Closes #1642 Co-authored-by: Claude Opus 4.6 (1M context) --- src/resources/extensions/async-jobs/index.ts | 11 +++++++++++ src/resources/extensions/gsd/auto-loop.ts | 14 ++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/resources/extensions/async-jobs/index.ts b/src/resources/extensions/async-jobs/index.ts index 24c379d1c..62cd4bbb4 100644 --- a/src/resources/extensions/async-jobs/index.ts +++ b/src/resources/extensions/async-jobs/index.ts @@ -78,6 +78,17 @@ export default function AsyncJobs(pi: ExtensionAPI) { }); }); + pi.on("session_before_switch", async () => { + if (manager) { + // Cancel all running background jobs — their results are no longer + // relevant to the new session and would produce wasteful follow-up + // notifications that trigger empty LLM turns (#1642). + for (const job of manager.getRunningJobs()) { + manager.cancel(job.id); + } + } + }); + pi.on("session_shutdown", async () => { if (manager) { manager.shutdown(); diff --git a/src/resources/extensions/gsd/auto-loop.ts b/src/resources/extensions/gsd/auto-loop.ts index 93220ee43..080d92451 100644 --- a/src/resources/extensions/gsd/auto-loop.ts +++ b/src/resources/extensions/gsd/auto-loop.ts @@ -287,6 +287,20 @@ export async function runUnit( status: result.status, }); + // Discard trailing follow-up messages (e.g. async_job_result notifications) + // from the completed unit. Without this, queued follow-ups trigger wasteful + // LLM turns before the next session can start (#1642). + // clearQueue() lives on AgentSession but isn't part of the typed + // ExtensionCommandContext interface — call it via runtime check. + try { + const cmdCtxAny = s.cmdCtx as Record | null; + if (typeof cmdCtxAny?.clearQueue === "function") { + (cmdCtxAny.clearQueue as () => unknown)(); + } + } catch { + // Non-fatal — clearQueue may not be available in all contexts + } + return result; }