diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts index 1b45ccf4d..cef1ef935 100644 --- a/src/features/background-agent/manager.test.ts +++ b/src/features/background-agent/manager.test.ts @@ -875,6 +875,90 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () => }) }) +describe("BackgroundManager.notifyParentSession - aborted parent", () => { + test("should skip notification when parent session is aborted", async () => { + //#given + let promptCalled = false + const client = { + session: { + prompt: async () => { + promptCalled = true + return {} + }, + abort: async () => ({}), + messages: async () => { + const error = new Error("User aborted") + error.name = "MessageAbortedError" + throw error + }, + }, + } + const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput) + const task: BackgroundTask = { + id: "task-aborted-parent", + sessionID: "session-child", + parentSessionID: "session-parent", + parentMessageID: "msg-parent", + description: "task aborted parent", + prompt: "test", + agent: "explore", + status: "completed", + startedAt: new Date(), + completedAt: new Date(), + } + getPendingByParent(manager).set("session-parent", new Set([task.id, "task-remaining"])) + + //#when + await (manager as unknown as { notifyParentSession: (task: BackgroundTask) => Promise }) + .notifyParentSession(task) + + //#then + expect(promptCalled).toBe(false) + + manager.shutdown() + }) + + test("should swallow aborted error from prompt", async () => { + //#given + let promptCalled = false + const client = { + session: { + prompt: async () => { + promptCalled = true + const error = new Error("User aborted") + error.name = "MessageAbortedError" + throw error + }, + abort: async () => ({}), + messages: async () => ({ data: [] }), + }, + } + const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput) + const task: BackgroundTask = { + id: "task-aborted-prompt", + sessionID: "session-child", + parentSessionID: "session-parent", + parentMessageID: "msg-parent", + description: "task aborted prompt", + prompt: "test", + agent: "explore", + status: "completed", + startedAt: new Date(), + completedAt: new Date(), + } + getPendingByParent(manager).set("session-parent", new Set([task.id])) + + //#when + await (manager as unknown as { notifyParentSession: (task: BackgroundTask) => Promise }) + .notifyParentSession(task) + + //#then + expect(promptCalled).toBe(true) + + manager.shutdown() + }) +}) + function buildNotificationPromptBody( task: BackgroundTask, currentMessage: CurrentMessage | null diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index 768b33383..0938a81b6 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -1123,7 +1123,14 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea break } } - } catch { + } catch (error) { + if (this.isAbortedSessionError(error)) { + log("[background-agent] Parent session aborted, skipping notification:", { + taskId: task.id, + parentSessionID: task.parentSessionID, + }) + return + } const messageDir = getMessageDir(task.parentSessionID) const currentMessage = messageDir ? findNearestMessageWithFields(messageDir) : null agent = currentMessage?.agent ?? task.parentAgent @@ -1154,6 +1161,13 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea noReply: !allComplete, }) } catch (error) { + if (this.isAbortedSessionError(error)) { + log("[background-agent] Parent session aborted, skipping notification:", { + taskId: task.id, + parentSessionID: task.parentSessionID, + }) + return + } log("[background-agent] Failed to send notification:", error) } @@ -1192,6 +1206,28 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea return `${seconds}s` } + private isAbortedSessionError(error: unknown): boolean { + const message = this.getErrorText(error) + return message.toLowerCase().includes("aborted") + } + + private getErrorText(error: unknown): string { + if (!error) return "" + if (typeof error === "string") return error + if (error instanceof Error) { + return `${error.name}: ${error.message}` + } + if (typeof error === "object" && error !== null) { + if ("message" in error && typeof error.message === "string") { + return error.message + } + if ("name" in error && typeof error.name === "string") { + return error.name + } + } + return "" + } + private hasRunningTasks(): boolean { for (const task of this.tasks.values()) { if (task.status === "running") return true