fix(#2791): await session.abort() in all subagent completion/cancel paths

Fire-and-forget session.abort() calls during subagent completion left
dangling promises that raced with parent session teardown. In Bun on
WSL2/Linux, this triggered a StringImplShape assertion (SIGABRT) as
WebKit GC collected string data still referenced by the inflight request.

Fix: await session.abort() in all four completion/error paths:
- startTask promptAsync error handler (launch path)
- resume promptAsync error handler (resume path)
- cancelTask (explicit cancel path)
- tryCompleteTask (normal completion path)

Also marks the two .catch() error callbacks as async so the await is
valid.

Test: update session.deleted cascade test to flush two microtask rounds
since cancelTask now awaits abort before cleanupPendingByParent.
This commit is contained in:
YeonGyu-Kim
2026-03-27 19:57:57 +09:00
parent ab0b084199
commit 324dbb119c
2 changed files with 13 additions and 6 deletions

View File

@@ -3728,6 +3728,9 @@ describe("BackgroundManager.handleEvent - session.deleted cascade", () => {
properties: { info: { id: parentSessionID } },
})
// Flush twice: cancelTask now awaits session.abort() before cleanupPendingByParent,
// so we need additional microtask ticks to let the cascade complete fully.
await flushBackgroundNotifications()
await flushBackgroundNotifications()
// then

View File

@@ -538,7 +538,7 @@ export class BackgroundManager {
})(),
parts: [createInternalAgentTextPart(input.prompt)],
},
}).catch((error) => {
}).catch(async (error) => {
log("[background-agent] promptAsync error:", error)
const existingTask = this.findBySession(sessionID)
if (existingTask) {
@@ -561,7 +561,8 @@ export class BackgroundManager {
removeTaskToastTracking(existingTask.id)
// Abort the session to prevent infinite polling hang
this.client.session.abort({
// Awaited to prevent dangling promise during subagent teardown (Bun/WebKit SIGABRT)
await this.client.session.abort({
path: { id: sessionID },
}).catch(() => {})
@@ -823,7 +824,7 @@ export class BackgroundManager {
})(),
parts: [createInternalAgentTextPart(input.prompt)],
},
}).catch((error) => {
}).catch(async (error) => {
log("[background-agent] resume prompt error:", error)
existingTask.status = "interrupt"
const errorMessage = error instanceof Error ? error.message : String(error)
@@ -842,8 +843,9 @@ export class BackgroundManager {
removeTaskToastTracking(existingTask.id)
// Abort the session to prevent infinite polling hang
// Awaited to prevent dangling promise during subagent teardown (Bun/WebKit SIGABRT)
if (existingTask.sessionID) {
this.client.session.abort({
await this.client.session.abort({
path: { id: existingTask.sessionID },
}).catch(() => {})
}
@@ -1392,7 +1394,8 @@ export class BackgroundManager {
}
if (abortSession && task.sessionID) {
this.client.session.abort({
// Awaited to prevent dangling promise during subagent teardown (Bun/WebKit SIGABRT)
await this.client.session.abort({
path: { id: task.sessionID },
}).catch(() => {})
@@ -1510,7 +1513,8 @@ export class BackgroundManager {
}
if (task.sessionID) {
this.client.session.abort({
// Awaited to prevent dangling promise during subagent teardown (Bun/WebKit SIGABRT)
await this.client.session.abort({
path: { id: task.sessionID },
}).catch(() => {})