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) <noreply@anthropic.com>
This commit is contained in:
Tibsfox 2026-04-06 18:16:32 -07:00
parent b4c6229360
commit 1859cb0d1a
3 changed files with 41 additions and 2 deletions

View file

@ -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());
}

View file

@ -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}`;
}
}

View file

@ -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);
});
});