diff --git a/bun.lock b/bun.lock index 39e341566..3357c17c0 100644 --- a/bun.lock +++ b/bun.lock @@ -10,8 +10,8 @@ "@clack/prompts": "^0.11.0", "@code-yeongyu/comment-checker": "^0.7.0", "@modelcontextprotocol/sdk": "^1.29.0", - "@opencode-ai/plugin": "1.4.0", - "@opencode-ai/sdk": "1.4.0", + "@opencode-ai/plugin": "^1.4.0", + "@opencode-ai/sdk": "^1.4.0", "commander": "^14.0.3", "detect-libc": "^2.1.2", "diff": "^8.0.4", @@ -31,17 +31,17 @@ "zod": "^4.4.3", }, "optionalDependencies": { - "oh-my-opencode-darwin-arm64": "4.1.1", - "oh-my-opencode-darwin-x64": "4.1.1", - "oh-my-opencode-darwin-x64-baseline": "4.1.1", - "oh-my-opencode-linux-arm64": "4.1.1", - "oh-my-opencode-linux-arm64-musl": "4.1.1", - "oh-my-opencode-linux-x64": "4.1.1", - "oh-my-opencode-linux-x64-baseline": "4.1.1", - "oh-my-opencode-linux-x64-musl": "4.1.1", - "oh-my-opencode-linux-x64-musl-baseline": "4.1.1", - "oh-my-opencode-windows-x64": "4.1.1", - "oh-my-opencode-windows-x64-baseline": "4.1.1", + "oh-my-opencode-darwin-arm64": "4.1.2", + "oh-my-opencode-darwin-x64": "4.1.2", + "oh-my-opencode-darwin-x64-baseline": "4.1.2", + "oh-my-opencode-linux-arm64": "4.1.2", + "oh-my-opencode-linux-arm64-musl": "4.1.2", + "oh-my-opencode-linux-x64": "4.1.2", + "oh-my-opencode-linux-x64-baseline": "4.1.2", + "oh-my-opencode-linux-x64-musl": "4.1.2", + "oh-my-opencode-linux-x64-musl-baseline": "4.1.2", + "oh-my-opencode-windows-x64": "4.1.2", + "oh-my-opencode-windows-x64-baseline": "4.1.2", }, "peerDependencies": { "zod": "^4.0.0", @@ -267,27 +267,27 @@ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@4.1.1", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-nciXDvGDWRFvz3OsZo+6IVUp2GwtOOUxZdIlsZklLI+7KC9MMm5FAYbFq2rokk1hkggLCSRROeEd7A4Jm/cYSw=="], + "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@4.1.2", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-zX0txRnCdBhDxvlMEcxfIhpEEVEJ/Jgi83G7Mbs7OhzGbr3MkVrdviy164yirO2uo2LVww0cuHvgKP0Mz+YijQ=="], - "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@4.1.1", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-vk8w7NveZp1JLN0zBe2mcYFaBMPPZ9BvehqSJjYwGnhxzfrnAFopksan0v7N2yr1y0ccOaF31F2XNAglv4rXBA=="], + "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@4.1.2", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Nh6VccQJ3kRlgLqefBXA2eLlS86qBlAGASeg0Tf+1suvSk3NgppfaYJFTctAStD5FuDdrq0HNdFA6e3TAEFsrA=="], - "oh-my-opencode-darwin-x64-baseline": ["oh-my-opencode-darwin-x64-baseline@4.1.1", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-0H5npMFOwrmfI6q1Sw+tlucp4VYlT5wAx6PgDrGV1WrF/g1c/f/lkTcXfJApEhmqmSv9uTX0mWy5JRb1FQZ+Nw=="], + "oh-my-opencode-darwin-x64-baseline": ["oh-my-opencode-darwin-x64-baseline@4.1.2", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-IwJlyPyxYpy1KOh8nJWkEZOOh312gYhzP+VtDyGBNXaPI+7sCdP0upZZXvEmekgytNpfOxnl/I9UAvaZnVX3/A=="], - "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@4.1.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-b8kQbbuEYGOZbhtetS9O8xAHbmIZfLwBQVw7lxgloIPU1qietfBxMrRY2ebF3j/UNvK3tE817Xwv01KW0X71Kw=="], + "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@4.1.2", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-EgBWpLmgVlq5R9X69QJ5NTrVwVDckj/OTN90hQ/6+msFh3PCX3wU/PyebNivUka5XP0oOhZ1UeXI0qgMIwPbsw=="], - "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@4.1.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-ZD/4FCEca82NshK0nNLJrsSYrx55RWOn0nhu3ZKKVyj4hMKWroAqBRvCWde3HxM/e5xUlDHsMJsj7MwvyLpcLg=="], + "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@4.1.2", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-eKnCi2AKoe6+pvvIqyxkAVYfnGeyuUD7+JKfSkvBHMe/ZbNhB26IeqvYKVRu2/U0zHECUUdHSJcruHi5fCefTA=="], - "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@4.1.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-0tgDMdodXuuOomxPvcRFABLX2xte2J4Q2LL1SwTFPw2/JaRa1GYcIX0GzX3PDWfH19jrfO+0RVX/oeXUWbG79w=="], + "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@4.1.2", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Mf3QH8amxwadqoQDcwQx0Nfg92hmCs0pPGskC1MdlS0WS05UMKol4grS2iTCxfoh2BSOZXnnFty4Q4NhGENjUQ=="], - "oh-my-opencode-linux-x64-baseline": ["oh-my-opencode-linux-x64-baseline@4.1.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Qb7pnsMU9tZYl/YKYnYOArRNhBDfX6PdkiLuL9onpMGWqxTCV1OsdIIG+LjSU0xJolKpDYMbJesew9XqUshG5w=="], + "oh-my-opencode-linux-x64-baseline": ["oh-my-opencode-linux-x64-baseline@4.1.2", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-pgzt8kb/+puDp7tXG7GKyyiI2FCYznWdjCbqQ/tzV+ITyWtnim0L8eHFcmDLjUTMVNlCF1KA2b9372pV+cE/YQ=="], - "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@4.1.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-2lbTzaBnlTeG5Y/pBzZT7KF82PXdxYcrwM0H03pboNZOZdDFf7KG7XXzeVV+9GDfDd7T3VfeRED0MHqcoTXipA=="], + "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@4.1.2", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-UtmHVqvxlloKFlA6/TdmJDptZJErD6cn4KZlNN7CEoZsGoE7qdxqCmIsfR8gFjvNbdUDA4+90Wju80ix3mOaig=="], - "oh-my-opencode-linux-x64-musl-baseline": ["oh-my-opencode-linux-x64-musl-baseline@4.1.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-QWmDD93TrPTD6DHnYmqeZWSjHel8oU3BxvN7XLk/elSqH/GUgC+ml/w6H02V7/59MU9fc557kgGzz6r/5g66IA=="], + "oh-my-opencode-linux-x64-musl-baseline": ["oh-my-opencode-linux-x64-musl-baseline@4.1.2", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-5QCIuVRRlhR+IR8ui+TVLPE9jLOIU3lLYGvH6RaLhENjSwabDdgB4zusDsHsmlp1MqTSX2Mn4mUsDy8OQwHg6A=="], - "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@4.1.1", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-zu8Qsb9k9u034tnrf5QABS/u9HdLPc15/n4BrKg4k06DLzqI2pVhK1olvATQsz9W8QdOO+eZ0NY1/Acp5V3Sog=="], + "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@4.1.2", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-lVLKB7v5h/hse6mIAXwNqhIxct71PXX0Z/pCCD8q9qll1x4wJxsLIUBNa+CtY46IS0JDYrx+Ik+bnJui0IprxQ=="], - "oh-my-opencode-windows-x64-baseline": ["oh-my-opencode-windows-x64-baseline@4.1.1", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-dBeCsO8kLC8cUSfBcMvprKoxLaTGKHuSg+otDdgy53nSN+3pHRxszM9iIQ0YplZsWmTxgpRlyAoL6hMZzZfdsw=="], + "oh-my-opencode-windows-x64-baseline": ["oh-my-opencode-windows-x64-baseline@4.1.2", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-mpiREgs2fFTQJxNGDBh99FBi4JTODbPO5PYR4fqM3vCEYS/BRbn7n1w79Ddsh+GKcDG+VPl6Dzi5cwescdyEwA=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts index c28abfcad..6c076d963 100644 --- a/src/features/background-agent/manager.test.ts +++ b/src/features/background-agent/manager.test.ts @@ -278,6 +278,10 @@ async function flushBackgroundNotifications(): Promise { } } +function waitForCoalescedFlush(): Promise { + return new Promise((resolve) => setTimeout(resolve, 400)) +} + function createToastRemoveTaskTracker(): { removeTaskCalls: string[]; resetToastManager: () => void } { _resetTaskToastManagerForTesting() const toastManager = initTaskToastManager(cast({ @@ -1303,6 +1307,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () => //#when await (cast<{ notifyParentSession: (value: BackgroundTask) => Promise }>(manager)) .notifyParentSession(task) + await waitForCoalescedFlush() //#then expect(capturedBody?.agent).toBe("sisyphus") @@ -1459,6 +1464,7 @@ describe("BackgroundManager.notifyParentSession - aborted parent", () => { //#when await (cast<{ notifyParentSession: (task: BackgroundTask) => Promise }>(manager)) .notifyParentSession(task) + await waitForCoalescedFlush() //#then expect(promptCalled).toBe(true) @@ -1501,6 +1507,7 @@ describe("BackgroundManager.notifyParentSession - aborted parent", () => { //#when await (cast<{ notifyParentSession: (task: BackgroundTask) => Promise }>(manager)) .notifyParentSession(task) + await waitForCoalescedFlush() //#then expect(promptCalled).toBe(true) @@ -1541,6 +1548,7 @@ describe("BackgroundManager.notifyParentSession - aborted parent", () => { //#when await (cast<{ notifyParentSession: (task: BackgroundTask) => Promise }>(manager)) .notifyParentSession(task) + await waitForCoalescedFlush() //#then const queuedNotifications = getPendingNotifications(manager).get("session-parent") ?? [] @@ -1652,6 +1660,7 @@ describe("BackgroundManager.notifyParentSession - variant propagation", () => { //#when await (cast<{ notifyParentSession: (task: BackgroundTask) => Promise }>(manager)) .notifyParentSession(task) + await waitForCoalescedFlush() //#then expect(promptCalls).toHaveLength(1) @@ -1693,6 +1702,7 @@ describe("BackgroundManager.notifyParentSession - variant propagation", () => { //#when await (cast<{ notifyParentSession: (task: BackgroundTask) => Promise }>(manager)) .notifyParentSession(task) + await waitForCoalescedFlush() //#then expect(promptCalls).toHaveLength(1) @@ -2117,7 +2127,7 @@ describe("BackgroundManager.tryCompleteTask", () => { // then expect(rejectedCount).toBe(0) - expect(promptBodies.length).toBe(2) + expect(promptBodies.length).toBe(1) expect(promptBodies.filter((body) => body.noReply === false)).toHaveLength(1) }) }) @@ -5410,6 +5420,7 @@ describe("BackgroundManager.pruneStaleTasksAndNotifications - removes pruned tas //#when pruneStaleTasksAndNotificationsForTest(manager) await flushBackgroundNotifications() + await waitForCoalescedFlush() //#then const retainedTask = getTaskMap(manager).get(staleTask.id) diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index ede06d093..2a58b67ce 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -111,6 +111,7 @@ type PendingParentWake = { } const PENDING_PARENT_WAKE_RETRY_MS = 1_000 +const PENDING_PARENT_WAKE_DEBOUNCE_MS = 100 interface MessagePartInfo { id?: string @@ -2247,32 +2248,19 @@ The task was re-queued on a fallback model after a retryable failure. shouldReply, }) } else { - try { - await promptAsyncInDirectory(this.client, { - path: { id: task.parentSessionId }, - body: { - noReply: !shouldReply, - ...parentPromptContext, - parts: [createInternalAgentTextPart(notification)], - }, - }, this.directory) - log("[background-agent] Sent notification to parent session:", { - taskId: task.id, - allComplete, - isTaskFailure, - noReply: !shouldReply, - }) - } catch (error) { - if (isAbortedSessionError(error)) { - log("[background-agent] Parent session aborted while sending notification; continuing cleanup:", { - taskId: task.id, - parentSessionID: task.parentSessionId, - }) - this.queuePendingNotification(task.parentSessionId, notification) - } else { - log("[background-agent] Failed to send notification:", error) - } - } + this.queuePendingParentWake( + task.parentSessionId, + notification, + parentPromptContext, + shouldReply, + PENDING_PARENT_WAKE_DEBOUNCE_MS, + ) + log("[background-agent] Queued notification for short-debounce flush to idle parent:", { + taskId: task.id, + allComplete, + isTaskFailure, + shouldReply, + }) } } else { log("[background-agent] Parent session notifications disabled, skipping prompt injection:", { @@ -2295,6 +2283,7 @@ The task was re-queued on a fallback model after a retryable failure. notification: string, promptContext: ParentWakePromptContext, shouldReply: boolean, + delayMs?: number, ): void { const pendingWake = this.pendingParentWakes.get(sessionID) if (pendingWake) { @@ -2308,12 +2297,11 @@ The task was re-queued on a fallback model after a retryable failure. shouldReply, }) } - this.schedulePendingParentWakeFlush(sessionID) + this.schedulePendingParentWakeFlush(sessionID, delayMs) } private async flushPendingParentWake(sessionID: string): Promise { - const pendingWake = this.pendingParentWakes.get(sessionID) - if (!pendingWake) { + if (!this.pendingParentWakes.has(sessionID)) { this.clearPendingParentWakeTimer(sessionID) return } @@ -2323,24 +2311,28 @@ The task was re-queued on a fallback model after a retryable failure. return } - this.pendingParentWakes.delete(sessionID) this.clearPendingParentWakeTimer(sessionID) await settleAfterSessionIdle() if (await this.isSessionActive(sessionID)) { - this.pendingParentWakes.set(sessionID, pendingWake) this.schedulePendingParentWakeFlush(sessionID) return } - const notificationContent = pendingWake.notifications.join("\n\n") + const latestWake = this.pendingParentWakes.get(sessionID) + if (!latestWake) { + return + } + this.pendingParentWakes.delete(sessionID) + + const notificationContent = latestWake.notifications.join("\n\n") try { await promptAsyncInDirectory(this.client, { path: { id: sessionID }, body: { - noReply: !pendingWake.shouldReply, - ...pendingWake.promptContext, + noReply: !latestWake.shouldReply, + ...latestWake.promptContext, parts: [createInternalAgentTextPart(notificationContent)], }, }, this.directory) @@ -2351,7 +2343,7 @@ The task was re-queued on a fallback model after a retryable failure. } } - private schedulePendingParentWakeFlush(sessionID: string): void { + private schedulePendingParentWakeFlush(sessionID: string, delayMs?: number): void { if (this.pendingParentWakeTimers.has(sessionID)) { return } @@ -2361,7 +2353,7 @@ The task was re-queued on a fallback model after a retryable failure. void this.enqueueNotificationForParent(sessionID, () => this.flushPendingParentWake(sessionID)).catch((error) => { log("[background-agent] Failed to retry pending parent wake:", { sessionID, error }) }) - }, PENDING_PARENT_WAKE_RETRY_MS) + }, delayMs ?? PENDING_PARENT_WAKE_RETRY_MS) this.pendingParentWakeTimers.set(sessionID, timer) } diff --git a/src/features/background-agent/task-completion-cleanup.test.ts b/src/features/background-agent/task-completion-cleanup.test.ts index 327c25ffc..43a75fa20 100644 --- a/src/features/background-agent/task-completion-cleanup.test.ts +++ b/src/features/background-agent/task-completion-cleanup.test.ts @@ -171,6 +171,10 @@ function waitForDeferredWakeRetry(): Promise { return new Promise((resolve) => setTimeout(resolve, 1_180)) } +function waitForCoalescedFlush(): Promise { + return new Promise((resolve) => setTimeout(resolve, 400)) +} + function getRequiredTimer(manager: BackgroundManager, taskID: string): ReturnType { const timer = getCompletionTimers(manager).get(taskID) expect(timer).toBeDefined() @@ -225,11 +229,10 @@ describe("BackgroundManager.notifyParentSession cleanup scheduling", () => { }) describe("#given background tasks for same parent", () => { - test("#when the second completion notification is sent #then ALL BACKGROUND TASKS COMPLETE notification still works correctly", async () => { + test("#when two completions arrive back-to-back while parent is idle #then one batched notification is sent with both tasks", async () => { // given const { manager, promptAsyncCalls } = createManager(true) managerUnderTest = manager - fakeTimers = installFakeTimers() const taskA = createTask({ id: "task-a", parentSessionId: "parent-1", description: "task A", status: "completed", completedAt: new Date("2026-03-11T00:01:00.000Z") }) const taskB = createTask({ id: "task-b", parentSessionId: "parent-1", description: "task B", status: "running" }) getTasks(manager).set(taskA.id, taskA) @@ -242,25 +245,60 @@ describe("BackgroundManager.notifyParentSession cleanup scheduling", () => { // when await notifyParentSessionForTest(manager, taskB) + await waitForCoalescedFlush() // then - expect(promptAsyncCalls).toHaveLength(2) - expect(promptAsyncCalls[0]?.body.noReply).toBe(true) - expect(getCompletionTimers(manager).size).toBe(2) - const allCompleteCall = promptAsyncCalls[1] - expect(allCompleteCall).toBeDefined() - if (!allCompleteCall) { - throw new Error("Missing all-complete notification call") + expect(promptAsyncCalls).toHaveLength(1) + const batchedCall = promptAsyncCalls[0] + if (!batchedCall) { + throw new Error("Missing batched notification call") } + expect(batchedCall.body.noReply).toBe(false) + const batchedPayload = JSON.stringify(batchedCall.body.parts) + expect(batchedPayload).toContain("ALL BACKGROUND TASKS COMPLETE") + expect(batchedPayload).toContain(OMO_INTERNAL_INITIATOR_MARKER) + expect(batchedPayload).toContain(taskA.id) + expect(batchedPayload).toContain(taskB.id) + expect(batchedPayload).toContain(taskA.description) + expect(batchedPayload).toContain(taskB.description) + }) - expect(allCompleteCall.body.noReply).toBe(false) - const allCompletePayload = JSON.stringify(allCompleteCall.body.parts) - expect(allCompletePayload).toContain("ALL BACKGROUND TASKS COMPLETE") - expect(allCompletePayload).toContain(OMO_INTERNAL_INITIATOR_MARKER) - expect(allCompletePayload).toContain(taskA.id) - expect(allCompletePayload).toContain(taskB.id) - expect(allCompletePayload).toContain(taskA.description) - expect(allCompletePayload).toContain(taskB.description) + test("#when many completions arrive in rapid succession while parent is idle #then a single coalesced notification is sent", async () => { + // given + const { manager, promptAsyncCalls } = createManager(true) + managerUnderTest = manager + const taskIds = ["task-1", "task-2", "task-3", "task-4", "task-5"] + const tasks = taskIds.map((id, index) => createTask({ + id, + parentSessionId: "parent-1", + description: `description ${id}`, + status: "completed", + completedAt: new Date(`2026-03-11T00:01:0${index}.000Z`), + })) + for (const task of tasks) { + getTasks(manager).set(task.id, task) + } + getPendingByParent(manager).set("parent-1", new Set(taskIds)) + + // when + for (const task of tasks) { + await notifyParentSessionForTest(manager, task) + } + await waitForCoalescedFlush() + + // then + expect(promptAsyncCalls).toHaveLength(1) + const batchedCall = promptAsyncCalls[0] + if (!batchedCall) { + throw new Error("Missing batched notification call") + } + expect(batchedCall.body.noReply).toBe(false) + const batchedPayload = JSON.stringify(batchedCall.body.parts) + expect(batchedPayload).toContain("ALL BACKGROUND TASKS COMPLETE") + for (const task of tasks) { + expect(batchedPayload).toContain(task.id) + expect(batchedPayload).toContain(task.description) + } }) test("#when parent session is busy #then all-complete notification does not start an overlapping parent reply", async () => { @@ -362,6 +400,7 @@ describe("BackgroundManager.notifyParentSession cleanup scheduling", () => { // when await notifyParentSessionForTest(manager, task) + await waitForCoalescedFlush() // then expect(promptAsyncCalls).toHaveLength(1)