From 4635d61adad6428816ca2597389a40a3d07c69e0 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 27 May 2026 16:17:30 +0900 Subject: [PATCH] fix(background-agent): defer parent wake during active turns --- .../parent-wake-active-turn-event.test.ts | 52 ++++++++++++++++++- .../background-agent/parent-wake-notifier.ts | 20 ++----- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/src/features/background-agent/parent-wake-active-turn-event.test.ts b/src/features/background-agent/parent-wake-active-turn-event.test.ts index 46fa66a75..4a801d126 100644 --- a/src/features/background-agent/parent-wake-active-turn-event.test.ts +++ b/src/features/background-agent/parent-wake-active-turn-event.test.ts @@ -108,7 +108,7 @@ async function flushPendingParentWakeForTest(manager: BackgroundManager, session } describe("BackgroundManager parent wake active turn events", () => { - test("#when background task completes during active parent turn #then parent gets same-turn no-reply reminder", async () => { + test("#when background task completes during active parent turn #then parent wake stays deferred", async () => { // given const sessionStatuses: Record = { "parent-1": { type: "busy" }, @@ -129,9 +129,57 @@ describe("BackgroundManager parent wake active turn events", () => { await notifyParentSessionForTest(manager, task) await flushPendingParentWakeForTest(manager, "parent-1") + // then + expect(promptAsyncCalls).toHaveLength(0) + expect(getPendingParentWakes(manager).has("parent-1")).toBe(true) + }) + + test("#when duplicate background completions overlap an active parent turn #then one coalesced wake dispatches after idle", async () => { + // given + const sessionStatuses: Record = { + "parent-1": { type: "busy" }, + } + const { manager, promptAsyncCalls } = createManager(sessionStatuses) + managerUnderTest = manager + const taskA = createTask({ + id: "task-a", + parentSessionId: "parent-1", + description: "task A", + status: "completed", + completedAt: new Date("2026-05-20T14:19:14.625Z"), + }) + const taskB = createTask({ + id: "task-b", + parentSessionId: "parent-1", + description: "task B", + status: "completed", + completedAt: new Date("2026-05-20T14:19:15.625Z"), + }) + getTasks(manager).set(taskA.id, taskA) + getTasks(manager).set(taskB.id, taskB) + getPendingByParent(manager).set(taskA.parentSessionId, new Set([taskA.id, taskB.id])) + + // when + await notifyParentSessionForTest(manager, taskA) + await notifyParentSessionForTest(manager, taskB) + await Promise.all([ + flushPendingParentWakeForTest(manager, "parent-1"), + flushPendingParentWakeForTest(manager, "parent-1"), + ]) + + // then + expect(promptAsyncCalls).toHaveLength(0) + expect(getPendingParentWakes(manager).has("parent-1")).toBe(true) + + // when + sessionStatuses["parent-1"] = { type: "idle" } + await Promise.all([ + flushPendingParentWakeForTest(manager, "parent-1"), + flushPendingParentWakeForTest(manager, "parent-1"), + ]) + // then expect(promptAsyncCalls).toHaveLength(1) - expect(promptAsyncCalls[0]?.body.noReply).toBe(true) expect(JSON.stringify(promptAsyncCalls[0]?.body.parts)).toContain("ALL BACKGROUND TASKS COMPLETE") expect(getPendingParentWakes(manager).has("parent-1")).toBe(false) }) diff --git a/src/features/background-agent/parent-wake-notifier.ts b/src/features/background-agent/parent-wake-notifier.ts index b16cf0a5a..c3800ac8d 100644 --- a/src/features/background-agent/parent-wake-notifier.ts +++ b/src/features/background-agent/parent-wake-notifier.ts @@ -77,19 +77,6 @@ type ToolWaitDeferralDecision = { type Unrefable = ReturnType & { unref?: () => unknown } -const ACTIVE_TURN_COMPLETION_NOTIFICATION_MARKERS = [ - "[BACKGROUND TASK COMPLETED]", - "[ALL BACKGROUND TASKS COMPLETE]", -] as const - -function notificationAllowsActiveTurnDelivery(notification: string): boolean { - return ACTIVE_TURN_COMPLETION_NOTIFICATION_MARKERS.some((marker) => notification.includes(marker)) -} - -function pendingWakeAllowsActiveTurnDelivery(wake: PendingParentWake): boolean { - return wake.notifications.length > 0 && wake.notifications.every(notificationAllowsActiveTurnDelivery) -} - function unrefTimerHandle(handle: ReturnType): void { const maybeUnref = (handle as Unrefable).unref if (typeof maybeUnref === "function") { @@ -179,8 +166,7 @@ export class ParentWakeNotifier { if (!latestWake) { return } - const canDeliverDuringActiveTurn = sessionActive && pendingWakeAllowsActiveTurnDelivery(latestWake) - if (sessionActive && !canDeliverDuringActiveTurn) { + if (sessionActive) { this.schedulePendingParentWakeFlush(sessionID) return } @@ -234,12 +220,12 @@ export class ParentWakeNotifier { source: "background-agent-parent-wake", settleMs: 0, queueBehavior: "defer", - checkStatus: !canDeliverDuringActiveTurn, + checkStatus: true, checkToolState: !toolWaitDecision.skipPromptGateToolStateCheck, input: { path: { id: sessionID }, body: { - noReply: canDeliverDuringActiveTurn ? true : !latestWake.shouldReply, + noReply: !latestWake.shouldReply, ...latestWake.promptContext, parts: [createInternalAgentTextPart(notificationContent)], },