fix(background-agent): gate parent wake prompts

This commit is contained in:
YeonGyu-Kim
2026-05-15 11:29:48 +09:00
parent b2fdd728d0
commit 174cbd0fbd
2 changed files with 92 additions and 34 deletions
+51 -5
View File
@@ -17,7 +17,6 @@ import {
resolveInheritedPromptTools, resolveInheritedPromptTools,
createInternalAgentTextPart, createInternalAgentTextPart,
messagesInDirectory, messagesInDirectory,
promptAsyncInDirectory,
promptWithRetryInDirectory, promptWithRetryInDirectory,
} from "../../shared" } from "../../shared"
import { resolveMessageEventSessionID, resolveSessionEventID } from "../../shared/event-session-id" import { resolveMessageEventSessionID, resolveSessionEventID } from "../../shared/event-session-id"
@@ -67,6 +66,7 @@ import {
isSessionActive as isOpenCodeSessionActive, isSessionActive as isOpenCodeSessionActive,
settleAfterSessionIdle, settleAfterSessionIdle,
} from "../../hooks/shared/session-idle-settle" } from "../../hooks/shared/session-idle-settle"
import { promptAsyncAfterSessionIdle } from "../../hooks/shared/prompt-async-gate"
import { import {
findNearestMessageExcludingCompaction, findNearestMessageExcludingCompaction,
resolvePromptContextFromSessionMessages, resolvePromptContextFromSessionMessages,
@@ -1161,7 +1161,13 @@ The fallback retry session is now created and can be inspected directly.
applySessionPromptParams(existingTask.sessionId!, existingTask.model) applySessionPromptParams(existingTask.sessionId!, existingTask.model)
} }
promptAsyncInDirectory(this.client, { promptAsyncAfterSessionIdle({
client: this.client,
sessionID: existingTask.sessionId,
source: "background-agent-resume",
settleMs: 0,
postDispatchHoldMs: 0,
input: {
path: { id: existingTask.sessionId }, path: { id: existingTask.sessionId },
body: { body: {
agent: existingTask.agent, agent: existingTask.agent,
@@ -1181,7 +1187,20 @@ The fallback retry session is now created and can be inspected directly.
})(), })(),
parts: [createInternalAgentTextPart(input.prompt)], parts: [createInternalAgentTextPart(input.prompt)],
}, },
}, this.directory).catch(async (error) => { query: { directory: this.directory },
},
}).then((promptResult) => {
if (promptResult.status === "failed") {
throw promptResult.error
}
if (promptResult.status !== "dispatched") {
log("[background-agent] resume prompt skipped by promptAsync gate:", {
taskId: existingTask.id,
sessionID: existingTask.sessionId,
status: promptResult.status,
})
}
}).catch(async (error) => {
log("[background-agent] resume prompt error:", error) log("[background-agent] resume prompt error:", error)
const errorInfo = { const errorInfo = {
name: extractErrorName(error), name: extractErrorName(error),
@@ -2328,14 +2347,41 @@ The task was re-queued on a fallback model after a retryable failure.
const notificationContent = latestWake.notifications.join("\n\n") const notificationContent = latestWake.notifications.join("\n\n")
try { try {
await promptAsyncInDirectory(this.client, { const promptResult = await promptAsyncAfterSessionIdle({
client: this.client,
sessionID,
source: "background-agent-parent-wake",
settleMs: 0,
postDispatchHoldMs: 250,
input: {
path: { id: sessionID }, path: { id: sessionID },
body: { body: {
noReply: !latestWake.shouldReply, noReply: !latestWake.shouldReply,
...latestWake.promptContext, ...latestWake.promptContext,
parts: [createInternalAgentTextPart(notificationContent)], parts: [createInternalAgentTextPart(notificationContent)],
}, },
}, this.directory) query: { directory: this.directory },
},
})
if (promptResult.status === "failed") {
throw promptResult.error
}
if (promptResult.status !== "dispatched") {
const pendingWake = this.pendingParentWakes.get(sessionID)
if (pendingWake) {
pendingWake.notifications.unshift(...latestWake.notifications)
pendingWake.shouldReply = pendingWake.shouldReply || latestWake.shouldReply
pendingWake.promptContext = latestWake.promptContext
} else {
this.pendingParentWakes.set(sessionID, latestWake)
}
this.schedulePendingParentWakeFlush(sessionID)
log("[background-agent] Deferred parent wake skipped by promptAsync gate:", {
sessionID,
status: promptResult.status,
})
return
}
log("[background-agent] Sent deferred parent wake:", { sessionID }) log("[background-agent] Sent deferred parent wake:", { sessionID })
} catch (error) { } catch (error) {
this.queuePendingNotification(sessionID, notificationContent) this.queuePendingNotification(sessionID, notificationContent)
@@ -4,6 +4,7 @@ import type { PluginInput } from "@opencode-ai/plugin"
import { TASK_CLEANUP_DELAY_MS } from "./constants" import { TASK_CLEANUP_DELAY_MS } from "./constants"
import { BackgroundManager } from "./manager" import { BackgroundManager } from "./manager"
import type { BackgroundTask } from "./types" import type { BackgroundTask } from "./types"
import { releaseAllPromptAsyncReservationsForTesting } from "../../hooks/shared/prompt-async-gate"
import { OMO_INTERNAL_INITIATOR_MARKER } from "../../shared/internal-initiator-marker" import { OMO_INTERNAL_INITIATOR_MARKER } from "../../shared/internal-initiator-marker"
type PromptAsyncCall = { type PromptAsyncCall = {
@@ -29,6 +30,7 @@ let fakeTimers: FakeTimers | undefined
afterEach(() => { afterEach(() => {
managerUnderTest?.shutdown() managerUnderTest?.shutdown()
fakeTimers?.restore() fakeTimers?.restore()
releaseAllPromptAsyncReservationsForTesting()
managerUnderTest = undefined managerUnderTest = undefined
fakeTimers = undefined fakeTimers = undefined
}) })
@@ -163,8 +165,18 @@ async function notifyParentSessionForTest(manager: BackgroundManager, task: Back
return notifyParentSession.call(manager, task) return notifyParentSession.call(manager, task)
} }
function waitForDeferredWake(): Promise<void> { async function waitUntil(predicate: () => boolean, timeoutMs: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 180)) const startedAt = Date.now()
while (!predicate()) {
if (Date.now() - startedAt >= timeoutMs) {
return
}
await new Promise((resolve) => setTimeout(resolve, 10))
}
}
function waitForDeferredWake(promptAsyncCalls: PromptAsyncCall[]): Promise<void> {
return waitUntil(() => promptAsyncCalls.length > 0, 600)
} }
function waitForDeferredWakeRetry(): Promise<void> { function waitForDeferredWakeRetry(): Promise<void> {
@@ -341,7 +353,7 @@ describe("BackgroundManager.notifyParentSession cleanup scheduling", () => {
// when // when
sessionStatuses["parent-1"] = { type: "idle" } sessionStatuses["parent-1"] = { type: "idle" }
manager.handleEvent({ type: "session.idle", properties: { sessionID: "parent-1" } }) manager.handleEvent({ type: "session.idle", properties: { sessionID: "parent-1" } })
await waitForDeferredWake() await waitForDeferredWake(promptAsyncCalls)
// then // then
expect(promptAsyncCalls).toHaveLength(1) expect(promptAsyncCalls).toHaveLength(1)
@@ -377,7 +389,7 @@ describe("BackgroundManager.notifyParentSession cleanup scheduling", () => {
// when // when
sessionStatuses["parent-1"] = { type: "idle" } sessionStatuses["parent-1"] = { type: "idle" }
manager.handleEvent({ type: "session.idle", properties: { sessionID: "parent-1" } }) manager.handleEvent({ type: "session.idle", properties: { sessionID: "parent-1" } })
await waitForDeferredWake() await waitForDeferredWake(promptAsyncCalls)
// then // then
expect(promptAsyncCalls).toHaveLength(1) expect(promptAsyncCalls).toHaveLength(1)
@@ -424,7 +436,7 @@ describe("BackgroundManager.notifyParentSession cleanup scheduling", () => {
// when // when
sessionStatuses["parent-1"] = { type: "idle" } sessionStatuses["parent-1"] = { type: "idle" }
manager.handleEvent({ type: "session.idle", properties: { sessionID: "parent-1" } }) manager.handleEvent({ type: "session.idle", properties: { sessionID: "parent-1" } })
await waitForDeferredWake() await waitForDeferredWake(promptAsyncCalls)
// then // then
expect(promptAsyncCalls).toHaveLength(1) expect(promptAsyncCalls).toHaveLength(1)
@@ -477,7 +489,7 @@ describe("BackgroundManager.notifyParentSession cleanup scheduling", () => {
await notifyParentSessionForTest(manager, task) await notifyParentSessionForTest(manager, task)
sessionStatuses["parent-1"] = { type: "idle" } sessionStatuses["parent-1"] = { type: "idle" }
manager.handleEvent({ type: "session.idle", properties: { sessionID: "parent-1" } }) manager.handleEvent({ type: "session.idle", properties: { sessionID: "parent-1" } })
await waitForDeferredWake() await waitForDeferredWake(promptAsyncCalls)
// then // then
expect(promptAsyncCalls).toHaveLength(1) expect(promptAsyncCalls).toHaveLength(1)