From 359baab4ec90721f1bf8ff997a3d7a422ce6d6f1 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 28 May 2026 18:14:52 +0900 Subject: [PATCH] fix(background-agent): align wake activity windows --- src/features/background-agent/manager.test.ts | 2 + src/features/background-agent/manager.ts | 2 +- .../parent-wake-activity-window.test.ts | 129 ++++++++++++++++++ src/tools/delegate-task/sync-continuation.ts | 1 + 4 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 src/features/background-agent/parent-wake-activity-window.test.ts diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts index 8098cdf26..59d5ba710 100644 --- a/src/features/background-agent/manager.test.ts +++ b/src/features/background-agent/manager.test.ts @@ -5643,6 +5643,7 @@ describe("BackgroundManager.handleEvent - session.error", () => { const client = { session: { status: async () => ({ data: { "parent-session-wake": { type: "idle" } } }), + messages: async () => ({ data: [] }), promptAsync: async (args: { path: { id: string }; body: Record }) => { promptCalls.push(args) return {} @@ -5699,6 +5700,7 @@ describe("BackgroundManager.handleEvent - session.error", () => { const client = { session: { status: async () => ({ data: { "parent-session-alias": { type: "idle" } } }), + messages: async () => ({ data: [] }), promptAsync: async (args: { path: { id: string }; body: Record }) => { promptCalls.push(args) return {} diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index 125b6d352..74cac7b11 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -149,7 +149,7 @@ const PARENT_WAKE_TOOL_CALL_DEFER_MAX_MS = 5_000 * env. See issue #4120. */ const PARENT_WAKE_USER_MESSAGE_IN_PROGRESS_WINDOW_MS = 2_000 -const PARENT_WAKE_SESSION_ACTIVITY_IN_PROGRESS_WINDOW_MS = 2_000 +const PARENT_WAKE_SESSION_ACTIVITY_IN_PROGRESS_WINDOW_MS = PARENT_WAKE_TOOL_CALL_DEFER_MAX_MS interface EventProperties { sessionID?: string diff --git a/src/features/background-agent/parent-wake-activity-window.test.ts b/src/features/background-agent/parent-wake-activity-window.test.ts new file mode 100644 index 000000000..525b8481e --- /dev/null +++ b/src/features/background-agent/parent-wake-activity-window.test.ts @@ -0,0 +1,129 @@ +import { tmpdir } from "node:os" +import { afterEach, describe, expect, test } from "bun:test" +import type { PluginInput } from "@opencode-ai/plugin" +import { unsafeTestValue } from "../../../test-support/unsafe-test-value" +import { releaseAllPromptAsyncReservationsForTesting } from "../../hooks/shared/prompt-async-gate" +import { BackgroundManager } from "./manager" +import type { BackgroundTask } from "./types" + +type PromptAsyncCall = { + readonly path: { readonly id: string } + readonly body: { readonly parts?: readonly unknown[] } +} + +let managerUnderTest: BackgroundManager | undefined + +afterEach(() => { + managerUnderTest?.shutdown() + managerUnderTest = undefined + releaseAllPromptAsyncReservationsForTesting() +}) + +function createTask(): BackgroundTask { + return { + id: "task-a", + parentMessageId: "parent-message-id", + parentSessionId: "parent-1", + description: "task A", + prompt: "Prompt for task A", + agent: "test-agent", + status: "completed", + startedAt: new Date("2026-05-20T14:19:10.000Z"), + completedAt: new Date("2026-05-20T14:19:14.625Z"), + } +} + +function createManager(): { + readonly manager: BackgroundManager + readonly promptAsyncCalls: readonly PromptAsyncCall[] +} { + const promptAsyncCalls: PromptAsyncCall[] = [] + const client = unsafeTestValue({ + session: { + messages: async () => [], + status: async () => ({ data: { "parent-1": { type: "idle" } } }), + prompt: async () => ({}), + promptAsync: async (call: PromptAsyncCall) => { + promptAsyncCalls.push(call) + return {} + }, + abort: async () => ({}), + }, + }) + const manager = new BackgroundManager({ + pluginContext: { + client, + project: {}, + directory: tmpdir(), + worktree: tmpdir(), + serverUrl: new URL("http://localhost"), + $: {}, + }, + config: undefined, + enableParentSessionNotifications: true, + }) + return { manager, promptAsyncCalls } +} + +function getTasks(manager: BackgroundManager): Map { + return unsafeTestValue>(Reflect.get(manager, "tasks")) +} + +function getPendingByParent(manager: BackgroundManager): Map> { + return unsafeTestValue>>(Reflect.get(manager, "pendingByParent")) +} + +function getPendingParentWakes(manager: BackgroundManager): Map { + const notifier = unsafeTestValue<{ + readonly getPendingParentWakes: () => Map + }>(Reflect.get(manager, "parentWakeNotifier")) + return notifier.getPendingParentWakes() +} + +async function notifyParentSessionForTest(manager: BackgroundManager, task: BackgroundTask): Promise { + const notifyParentSession = unsafeTestValue<(task: BackgroundTask) => Promise>(Reflect.get(manager, "notifyParentSession")) + await notifyParentSession.call(manager, task) +} + +async function flushPendingParentWakeForTest(manager: BackgroundManager, sessionID: string): Promise { + const flushPendingParentWake = unsafeTestValue<(sessionID: string) => Promise>(Reflect.get(manager, "flushPendingParentWake")) + await flushPendingParentWake.call(manager, sessionID) +} + +describe("BackgroundManager parent wake activity window", () => { + test("#given parent tool activity is within the tool deferral window #when stale idle flushes a wake #then parent prompt stays deferred", async () => { + // given + const originalDateNow = Date.now + let now = 100_000 + Date.now = () => now + const { manager, promptAsyncCalls } = createManager() + managerUnderTest = manager + manager.handleEvent({ + type: "message.part.updated", + properties: { + sessionID: "parent-1", + part: { + sessionID: "parent-1", + type: "tool", + tool: "todowrite", + }, + }, + }) + now = 104_900 + const task = createTask() + getTasks(manager).set(task.id, task) + getPendingByParent(manager).set(task.parentSessionId, new Set([task.id])) + + try { + // when + await notifyParentSessionForTest(manager, task) + await flushPendingParentWakeForTest(manager, "parent-1") + + // then + expect(promptAsyncCalls).toHaveLength(0) + expect(getPendingParentWakes(manager).has("parent-1")).toBe(true) + } finally { + Date.now = originalDateNow + } + }) +}) diff --git a/src/tools/delegate-task/sync-continuation.ts b/src/tools/delegate-task/sync-continuation.ts index ce6bf0e99..73a52fdd2 100644 --- a/src/tools/delegate-task/sync-continuation.ts +++ b/src/tools/delegate-task/sync-continuation.ts @@ -172,6 +172,7 @@ export async function executeSyncContinuation( }, }, { queueBehavior: "defer", + checkToolState: false, }) } catch (promptError) { if (toastManager) {