From 324dbb119cfb36285b6692529cf4bd799fae1955 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 27 Mar 2026 19:57:57 +0900 Subject: [PATCH] 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. --- src/features/background-agent/manager.test.ts | 3 +++ src/features/background-agent/manager.ts | 16 ++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts index 974718387..014f2328a 100644 --- a/src/features/background-agent/manager.test.ts +++ b/src/features/background-agent/manager.test.ts @@ -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 diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index f0dc09aa7..d35428441 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -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(() => {})