Merge pull request #1500 from code-yeongyu/fix/background-abort-tui-crash

fix(background-agent): gracefully handle aborted parent session in notifyParentSession
This commit is contained in:
YeonGyu-Kim 2026-02-05 11:39:16 +09:00 committed by GitHub
commit 11b883da6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 121 additions and 1 deletions

View File

@ -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<void> })
.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<void> })
.notifyParentSession(task)
//#then
expect(promptCalled).toBe(true)
manager.shutdown()
})
})
function buildNotificationPromptBody(
task: BackgroundTask,
currentMessage: CurrentMessage | null

View File

@ -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