From 1859cb0d1a4f53992e746000179a0478d4be7d6f Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Mon, 6 Apr 2026 18:16:32 -0700 Subject: [PATCH 1/2] fix(gsd): treat queued-user-message skip as non-retryable interruption Add isQueuedUserMessageSkip() predicate and extend recordToolInvocationError to catch "Skipped due to queued user message." so auto-mode pauses instead of retrying the same unit until the provider aborts with 3 consecutive validation failures. Fixes #3595 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../extensions/gsd/auto-tool-tracking.ts | 10 +++++++ src/resources/extensions/gsd/auto.ts | 3 +- .../tool-invocation-error-loop-break.test.ts | 30 ++++++++++++++++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/resources/extensions/gsd/auto-tool-tracking.ts b/src/resources/extensions/gsd/auto-tool-tracking.ts index 165c86d65..9e7ffc049 100644 --- a/src/resources/extensions/gsd/auto-tool-tracking.ts +++ b/src/resources/extensions/gsd/auto-tool-tracking.ts @@ -102,3 +102,13 @@ export function isToolInvocationError(errorMsg: string): boolean { if (!errorMsg) return false; return TOOL_INVOCATION_ERROR_RE.test(errorMsg); } + +/** + * Returns true if the error message indicates the tool was skipped because + * a queued user message interrupted the turn (#3595). Retrying will produce + * the same skip, so the unit should be paused rather than retried. + */ +export function isQueuedUserMessageSkip(errorMsg: string): boolean { + if (!errorMsg) return false; + return /^Skipped due to queued user message\.?$/i.test(errorMsg.trim()); +} diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index f21f6330d..081dd493c 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -76,6 +76,7 @@ import { hasInteractiveToolInFlight, clearInFlightTools, isToolInvocationError, + isQueuedUserMessageSkip, } from "./auto-tool-tracking.js"; import { closeoutUnit } from "./auto-unit-closeout.js"; import { recoverTimedOutUnit } from "./auto-timeout-recovery.js"; @@ -397,7 +398,7 @@ export function markToolEnd(toolCallId: string): void { */ export function recordToolInvocationError(toolName: string, errorMsg: string): void { if (!s.active) return; - if (isToolInvocationError(errorMsg)) { + if (isToolInvocationError(errorMsg) || isQueuedUserMessageSkip(errorMsg)) { s.lastToolInvocationError = `${toolName}: ${errorMsg}`; } } diff --git a/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts b/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts index d128ee12f..5a2cdfa58 100644 --- a/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +++ b/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts @@ -44,7 +44,7 @@ describe("#2883: tool invocation error tracking on AutoSession", () => { // ─── isToolInvocationError classifier ──────────────────────────────────── -import { isToolInvocationError } from "../auto-tool-tracking.ts"; +import { isToolInvocationError, isQueuedUserMessageSkip } from "../auto-tool-tracking.ts"; describe("#2883: isToolInvocationError classification", () => { test("detects JSON validation failure pattern", () => { @@ -101,3 +101,31 @@ describe("#2883: isToolInvocationError classification", () => { assert.equal(isToolInvocationError("ECONNRESET"), false); }); }); + +// ─── isQueuedUserMessageSkip classifier (#3595) ───────────────────────── + +describe("#3595: isQueuedUserMessageSkip classification", () => { + test("detects exact skip message with period", () => { + assert.equal(isQueuedUserMessageSkip("Skipped due to queued user message."), true); + }); + + test("detects skip message without period", () => { + assert.equal(isQueuedUserMessageSkip("Skipped due to queued user message"), true); + }); + + test("detects skip message with surrounding whitespace", () => { + assert.equal(isQueuedUserMessageSkip(" Skipped due to queued user message. "), true); + }); + + test("returns false for normal tool errors", () => { + assert.equal(isQueuedUserMessageSkip("Slice S01 is already complete"), false); + }); + + test("returns false for empty string", () => { + assert.equal(isQueuedUserMessageSkip(""), false); + }); + + test("returns false for partial match (substring)", () => { + assert.equal(isQueuedUserMessageSkip("Error: Skipped due to queued user message. Retry later."), false); + }); +}); From c159844b053d04de6a6cea2a0d3f0dfce691bfd6 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Mon, 6 Apr 2026 18:18:44 -0700 Subject: [PATCH 2/2] fix(gsd): show accurate pause message for queued-user-message skip Distinguish between malformed-JSON pauses and queued-user-message pauses in the notification so operators see the correct root cause. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/auto-post-unit.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/resources/extensions/gsd/auto-post-unit.ts b/src/resources/extensions/gsd/auto-post-unit.ts index 3bffee4b8..aa8555b12 100644 --- a/src/resources/extensions/gsd/auto-post-unit.ts +++ b/src/resources/extensions/gsd/auto-post-unit.ts @@ -582,11 +582,14 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV "error", ); } else if (!triggerArtifactVerified) { - // #2883: If the artifact is missing because the tool invocation itself - // failed (malformed/truncated JSON arguments), retrying will produce the - // same failure. Pause auto-mode instead of entering a stuck retry loop. + // #2883/#3595: If the artifact is missing because the tool invocation + // failed (malformed JSON) or was skipped (queued user message), retrying + // will produce the same failure. Pause auto-mode instead of looping. if (s.lastToolInvocationError) { - const errMsg = `Tool invocation failed for ${s.currentUnit.type}: ${s.lastToolInvocationError}. Structured argument generation failed — pausing auto-mode.`; + const isUserSkip = /queued user message/i.test(s.lastToolInvocationError); + const errMsg = isUserSkip + ? `Tool skipped for ${s.currentUnit.type}: ${s.lastToolInvocationError}. Queued user message interrupted the turn — pausing auto-mode.` + : `Tool invocation failed for ${s.currentUnit.type}: ${s.lastToolInvocationError}. Structured argument generation failed — pausing auto-mode.`; debugLog("postUnit", { phase: "tool-invocation-error-pause", unitType: s.currentUnit.type, unitId: s.currentUnit.id, error: s.lastToolInvocationError }); ctx.ui.notify(errMsg, "error"); s.lastToolInvocationError = null;