From 6a561446ec746a646582533c0068174b81983d2a Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 26 May 2026 01:30:02 +0900 Subject: [PATCH 1/2] fix(background-agent): deliver completion wakes during active parent turns Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../parent-wake-active-turn-event.test.ts | 55 +++++++++++++++++++ .../background-agent/parent-wake-notifier.ts | 37 +++++++++---- 2 files changed, 82 insertions(+), 10 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 fcc4631fa..46fa66a75 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,6 +108,61 @@ 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 () => { + // given + const sessionStatuses: Record = { + "parent-1": { type: "busy" }, + } + const { manager, promptAsyncCalls } = createManager(sessionStatuses) + managerUnderTest = manager + const task = createTask({ + id: "task-a", + parentSessionId: "parent-1", + description: "task A", + status: "completed", + completedAt: new Date("2026-05-20T14:19:14.625Z"), + }) + getTasks(manager).set(task.id, task) + getPendingByParent(manager).set(task.parentSessionId, new Set([task.id])) + + // when + await notifyParentSessionForTest(manager, task) + await 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) + }) + + test("#when background task fails during active parent turn #then parent wake stays deferred", async () => { + // given + const sessionStatuses: Record = { + "parent-1": { type: "busy" }, + } + const { manager, promptAsyncCalls } = createManager(sessionStatuses) + managerUnderTest = manager + const task = createTask({ + id: "task-a", + parentSessionId: "parent-1", + description: "task A", + status: "error", + error: "UnknownError: UnknownError", + completedAt: new Date("2026-05-20T14:19:14.625Z"), + }) + getTasks(manager).set(task.id, task) + getPendingByParent(manager).set(task.parentSessionId, new Set([task.id])) + + // when + await notifyParentSessionForTest(manager, task) + await flushPendingParentWakeForTest(manager, "parent-1") + + // then + expect(promptAsyncCalls).toHaveLength(0) + expect(getPendingParentWakes(manager).has("parent-1")).toBe(true) + }) + test("#when parent reasoning delta is newer than stale idle state #then background completion does not fork a reply", async () => { // given const sessionStatuses: Record = { diff --git a/src/features/background-agent/parent-wake-notifier.ts b/src/features/background-agent/parent-wake-notifier.ts index 2cf97b413..eeaa7f683 100644 --- a/src/features/background-agent/parent-wake-notifier.ts +++ b/src/features/background-agent/parent-wake-notifier.ts @@ -77,6 +77,19 @@ 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") { @@ -151,23 +164,26 @@ export class ParentWakeNotifier { return } - if (await this.isSessionActive(sessionID)) { - this.schedulePendingParentWakeFlush(sessionID) - return - } - + const sessionActive = await this.isSessionActive(sessionID) this.clearPendingParentWakeTimer(sessionID) - await settleAfterSessionIdle() + if (!sessionActive) { + await settleAfterSessionIdle() - if (await this.isSessionActive(sessionID)) { - this.schedulePendingParentWakeFlush(sessionID) - return + if (await this.isSessionActive(sessionID)) { + this.schedulePendingParentWakeFlush(sessionID) + return + } } const latestWake = this.pendingParentWakes.get(sessionID) if (!latestWake) { return } + const canDeliverDuringActiveTurn = sessionActive && pendingWakeAllowsActiveTurnDelivery(latestWake) + if (sessionActive && !canDeliverDuringActiveTurn) { + this.schedulePendingParentWakeFlush(sessionID) + return + } if (this.hasRecentParentSessionActivity(sessionID)) { this.schedulePendingParentWakeFlush(sessionID) @@ -218,11 +234,12 @@ export class ParentWakeNotifier { source: "background-agent-parent-wake", settleMs: 0, queueBehavior: "defer", + checkStatus: !canDeliverDuringActiveTurn, checkToolState: !toolWaitDecision.skipPromptGateToolStateCheck, input: { path: { id: sessionID }, body: { - noReply: !latestWake.shouldReply, + noReply: canDeliverDuringActiveTurn ? true : !latestWake.shouldReply, ...latestWake.promptContext, parts: [createInternalAgentTextPart(notificationContent)], }, From ec7ce51ea63dad1089adcceea1a603caa6b448e3 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 26 May 2026 01:30:10 +0900 Subject: [PATCH 2/2] fix(background-agent): debounce active parent wake flushes Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/features/background-agent/manager.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index e13e58aae..7b3820a97 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -2449,8 +2449,14 @@ The task was re-queued on a fallback model after a retryable failure. const shouldDeferNotification = await this.isSessionActive(task.parentSessionId) if (shouldDeferNotification) { - this.queuePendingParentWake(task.parentSessionId, notification, parentPromptContext, shouldReply) - log("[background-agent] Deferred notification until parent session is idle:", { + this.queuePendingParentWake( + task.parentSessionId, + notification, + parentPromptContext, + shouldReply, + PENDING_PARENT_WAKE_DEBOUNCE_MS, + ) + log("[background-agent] Queued notification while parent session is active:", { taskId: task.id, allComplete, isTaskFailure,