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:
commit
11b883da6c
@ -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(
|
function buildNotificationPromptBody(
|
||||||
task: BackgroundTask,
|
task: BackgroundTask,
|
||||||
currentMessage: CurrentMessage | null
|
currentMessage: CurrentMessage | null
|
||||||
|
|||||||
@ -1123,7 +1123,14 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
|||||||
break
|
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 messageDir = getMessageDir(task.parentSessionID)
|
||||||
const currentMessage = messageDir ? findNearestMessageWithFields(messageDir) : null
|
const currentMessage = messageDir ? findNearestMessageWithFields(messageDir) : null
|
||||||
agent = currentMessage?.agent ?? task.parentAgent
|
agent = currentMessage?.agent ?? task.parentAgent
|
||||||
@ -1154,6 +1161,13 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
|||||||
noReply: !allComplete,
|
noReply: !allComplete,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} 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)
|
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`
|
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 {
|
private hasRunningTasks(): boolean {
|
||||||
for (const task of this.tasks.values()) {
|
for (const task of this.tasks.values()) {
|
||||||
if (task.status === "running") return true
|
if (task.status === "running") return true
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user