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
+74 -28
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,27 +1161,46 @@ 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({
path: { id: existingTask.sessionId }, client: this.client,
body: { sessionID: existingTask.sessionId,
agent: existingTask.agent, source: "background-agent-resume",
...(resumeModel ? { model: resumeModel } : {}), settleMs: 0,
...(resumeVariant ? { variant: resumeVariant } : {}), postDispatchHoldMs: 0,
tools: (() => { input: {
const tools = { path: { id: existingTask.sessionId },
task: false, body: {
call_omo_agent: true, agent: existingTask.agent,
question: false, ...(resumeModel ? { model: resumeModel } : {}),
...getAgentToolRestrictions(existingTask.agent, { ...(resumeVariant ? { variant: resumeVariant } : {}),
includeTeamToolDenylist: existingTask.teamRunId === undefined, tools: (() => {
}), const tools = {
} task: false,
setSessionTools(existingTask.sessionId!, tools) call_omo_agent: true,
return tools question: false,
})(), ...getAgentToolRestrictions(existingTask.agent, {
parts: [createInternalAgentTextPart(input.prompt)], includeTeamToolDenylist: existingTask.teamRunId === undefined,
}),
}
setSessionTools(existingTask.sessionId!, tools)
return tools
})(),
parts: [createInternalAgentTextPart(input.prompt)],
},
query: { directory: this.directory },
}, },
}, this.directory).catch(async (error) => { }).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({
path: { id: sessionID }, client: this.client,
body: { sessionID,
noReply: !latestWake.shouldReply, source: "background-agent-parent-wake",
...latestWake.promptContext, settleMs: 0,
parts: [createInternalAgentTextPart(notificationContent)], postDispatchHoldMs: 250,
input: {
path: { id: sessionID },
body: {
noReply: !latestWake.shouldReply,
...latestWake.promptContext,
parts: [createInternalAgentTextPart(notificationContent)],
},
query: { directory: this.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)