From 1859cb0d1a4f53992e746000179a0478d4be7d6f Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Mon, 6 Apr 2026 18:16:32 -0700 Subject: [PATCH] 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); + }); +});