diff --git a/src/tools/delegate-task/sync-task.test.ts b/src/tools/delegate-task/sync-task.test.ts index 11d2c4177..03e2d500a 100644 --- a/src/tools/delegate-task/sync-task.test.ts +++ b/src/tools/delegate-task/sync-task.test.ts @@ -173,7 +173,7 @@ describe("executeSyncTask - cleanup on error paths", () => { expect(rollback).toHaveBeenCalledTimes(1) }) - test("recovers from pollSyncSession error when result already exists", async () => { + test("recovers from MessageAbortedError poll error when result already exists", async () => { const mockClient = { session: { create: async () => ({ data: { id: "ses_test_12345678" } }), @@ -185,7 +185,7 @@ describe("executeSyncTask - cleanup on error paths", () => { const deps = { createSyncSession: async () => ({ ok: true, sessionID: "ses_test_12345678" }), sendSyncPrompt: async () => null, - pollSyncSession: async () => "Task aborted.\n\nSession ID: ses_test_12345678", + pollSyncSession: async () => "MessageAbortedError: aborted by user", fetchSyncResult: async () => ({ ok: true as const, textContent: "Result" }), } @@ -210,7 +210,7 @@ describe("executeSyncTask - cleanup on error paths", () => { command: null, } - //#when - executeSyncTask with pollSyncSession failing + //#when - executeSyncTask with MessageAbortedError poll error const result = await executeSyncTask(args, mockCtx, mockExecutorCtx, { sessionID: "parent-session", }, "test-agent", undefined, undefined, undefined, undefined, deps) @@ -224,6 +224,57 @@ describe("executeSyncTask - cleanup on error paths", () => { expect(deleteCalls[0]).toBe("ses_test_12345678") }) + test("does not recover from non-abort poll error containing abort-like words", async () => { + const mockClient = { + session: { + create: async () => ({ data: { id: "ses_test_12345678" } }), + }, + } + + const { executeSyncTask } = require("./sync-task") + let fetchSyncResultCalled = false + + const deps = { + createSyncSession: async () => ({ ok: true, sessionID: "ses_test_12345678" }), + sendSyncPrompt: async () => null, + pollSyncSession: async () => "Task aborted: subagent exceeded 5 assistant turns without completing", + fetchSyncResult: async () => { + fetchSyncResultCalled = true + return { ok: true as const, textContent: "unexpected" } + }, + } + + const mockCtx = { + sessionID: "parent-session", + callID: "call-123", + metadata: () => {}, + } + + const mockExecutorCtx = { + client: mockClient, + directory: "/tmp", + onSyncSessionCreated: null, + } + + const args = { + prompt: "test prompt", + description: "test task", + category: "test", + load_skills: [], + run_in_background: false, + command: null, + } + + //#when + const result = await executeSyncTask(args, mockCtx, mockExecutorCtx, { + sessionID: "parent-session", + }, "test-agent", undefined, undefined, undefined, undefined, deps) + + //#then + expect(result).toBe("Task aborted: subagent exceeded 5 assistant turns without completing") + expect(fetchSyncResultCalled).toBe(false) + }) + test("returns poll error when recovery fetch has no result", async () => { const mockClient = { session: { diff --git a/src/tools/delegate-task/sync-task.ts b/src/tools/delegate-task/sync-task.ts index 762146442..58b52a07b 100644 --- a/src/tools/delegate-task/sync-task.ts +++ b/src/tools/delegate-task/sync-task.ts @@ -16,8 +16,25 @@ import { shouldRetryError } from "../../shared/model-error-classifier" import type { ModelFallbackState } from "../../hooks/model-fallback/hook" function shouldAttemptPollErrorRecovery(pollError: string): boolean { - const normalized = pollError.toLowerCase() - return normalized.includes("aborted") || normalized.includes("abort") + const trimmed = pollError.trim() + + if (trimmed.length === 0) { + return false + } + + if (/\bMessageAbortedError\b/u.test(trimmed)) { + return true + } + + if (/\bDOMException\b/u.test(trimmed) && /\bAbortError\b/u.test(trimmed)) { + return true + } + + if (/\bAbortError\b/u.test(trimmed) && !/\bTask aborted\b/u.test(trimmed)) { + return true + } + + return false } export async function executeSyncTask(