fix(delegate-task): narrow abort recovery to canonical abort errors

This commit is contained in:
YeonGyu-Kim
2026-05-11 08:46:14 +09:00
parent a064e13676
commit 65913023c5
2 changed files with 73 additions and 5 deletions
+54 -3
View File
@@ -173,7 +173,7 @@ describe("executeSyncTask - cleanup on error paths", () => {
expect(rollback).toHaveBeenCalledTimes(1) 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 = { const mockClient = {
session: { session: {
create: async () => ({ data: { id: "ses_test_12345678" } }), create: async () => ({ data: { id: "ses_test_12345678" } }),
@@ -185,7 +185,7 @@ describe("executeSyncTask - cleanup on error paths", () => {
const deps = { const deps = {
createSyncSession: async () => ({ ok: true, sessionID: "ses_test_12345678" }), createSyncSession: async () => ({ ok: true, sessionID: "ses_test_12345678" }),
sendSyncPrompt: async () => null, 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" }), fetchSyncResult: async () => ({ ok: true as const, textContent: "Result" }),
} }
@@ -210,7 +210,7 @@ describe("executeSyncTask - cleanup on error paths", () => {
command: null, command: null,
} }
//#when - executeSyncTask with pollSyncSession failing //#when - executeSyncTask with MessageAbortedError poll error
const result = await executeSyncTask(args, mockCtx, mockExecutorCtx, { const result = await executeSyncTask(args, mockCtx, mockExecutorCtx, {
sessionID: "parent-session", sessionID: "parent-session",
}, "test-agent", undefined, undefined, undefined, undefined, deps) }, "test-agent", undefined, undefined, undefined, undefined, deps)
@@ -224,6 +224,57 @@ describe("executeSyncTask - cleanup on error paths", () => {
expect(deleteCalls[0]).toBe("ses_test_12345678") 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 () => { test("returns poll error when recovery fetch has no result", async () => {
const mockClient = { const mockClient = {
session: { session: {
+19 -2
View File
@@ -16,8 +16,25 @@ import { shouldRetryError } from "../../shared/model-error-classifier"
import type { ModelFallbackState } from "../../hooks/model-fallback/hook" import type { ModelFallbackState } from "../../hooks/model-fallback/hook"
function shouldAttemptPollErrorRecovery(pollError: string): boolean { function shouldAttemptPollErrorRecovery(pollError: string): boolean {
const normalized = pollError.toLowerCase() const trimmed = pollError.trim()
return normalized.includes("aborted") || normalized.includes("abort")
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( export async function executeSyncTask(