fix(background-agent): preserve direct all-complete replies

This commit is contained in:
YeonGyu-Kim
2026-05-12 15:28:35 +09:00
parent 2d20037d35
commit 07797e1975
2 changed files with 42 additions and 186 deletions
@@ -4,6 +4,7 @@ import type { PluginInput } from "@opencode-ai/plugin"
import { TASK_CLEANUP_DELAY_MS } from "./constants"
import { BackgroundManager } from "./manager"
import type { BackgroundTask } from "./types"
import { OMO_INTERNAL_INITIATOR_MARKER } from "../../shared/internal-initiator-marker"
type PromptAsyncCall = {
path: { id: string }
@@ -159,14 +160,6 @@ async function notifyParentSessionForTest(manager: BackgroundManager, task: Back
return notifyParentSession.call(manager, task)
}
function waitForDeferredWake(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 180))
}
function waitForDeferredWakeRetry(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 1_180))
}
function getRequiredTimer(manager: BackgroundManager, taskID: string): ReturnType<typeof setTimeout> {
const timer = getCompletionTimers(manager).get(taskID)
expect(timer).toBeDefined()
@@ -241,6 +234,7 @@ describe("BackgroundManager.notifyParentSession cleanup scheduling", () => {
// 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()
@@ -251,13 +245,14 @@ describe("BackgroundManager.notifyParentSession cleanup scheduling", () => {
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 parent session is busy #then all-complete notification does not start an overlapping parent reply", async () => {
test("#when parent session is busy #then all-complete notification keeps the direct 4.0.0 parent prompt behavior", async () => {
// given
const sessionStatuses: Record<string, { type: string }> = {
"parent-1": { type: "busy" },
@@ -272,10 +267,14 @@ describe("BackgroundManager.notifyParentSession cleanup scheduling", () => {
await notifyParentSessionForTest(manager, task)
// then
expect(promptAsyncCalls).toHaveLength(0)
expect(promptAsyncCalls).toHaveLength(1)
expect(promptAsyncCalls[0]?.body.noReply).toBe(false)
const notificationPayload = JSON.stringify(promptAsyncCalls[0]?.body.parts)
expect(notificationPayload).toContain("ALL BACKGROUND TASKS COMPLETE")
expect(notificationPayload).toContain(OMO_INTERNAL_INITIATOR_MARKER)
})
test("#when deferred parent session becomes idle #then completion notification wakes the parent without a pointer reminder", async () => {
test("#when busy parent later becomes idle #then completion notification is not replayed as a second parent prompt", async () => {
// given
const sessionStatuses: Record<string, { type: string }> = {
"parent-1": { type: "busy" },
@@ -286,21 +285,22 @@ describe("BackgroundManager.notifyParentSession cleanup scheduling", () => {
getTasks(manager).set(task.id, task)
getPendingByParent(manager).set(task.parentSessionId, new Set([task.id]))
await notifyParentSessionForTest(manager, task)
expect(promptAsyncCalls).toHaveLength(1)
// when
sessionStatuses["parent-1"] = { type: "idle" }
manager.handleEvent({ type: "session.idle", properties: { sessionID: "parent-1" } })
await waitForDeferredWake()
await Promise.resolve()
// then
expect(promptAsyncCalls).toHaveLength(1)
expect(promptAsyncCalls[0]?.body.noReply).toBe(false)
const wakePayload = JSON.stringify(promptAsyncCalls[0]?.body.parts)
expect(wakePayload).toContain("ALL BACKGROUND TASKS COMPLETE")
expect(wakePayload).not.toContain("BACKGROUND TASK NOTIFICATION READY")
const notificationPayload = JSON.stringify(promptAsyncCalls[0]?.body.parts)
expect(notificationPayload).toContain("ALL BACKGROUND TASKS COMPLETE")
expect(notificationPayload).not.toContain("BACKGROUND TASK NOTIFICATION READY")
})
test("#when a single background task finishes during a stale busy parent status #then completion notification is retried after the parent becomes idle", async () => {
test("#when a single background task finishes during a stale busy parent status #then no deferred wake is scheduled", async () => {
// given
const sessionStatuses: Record<string, { type: string }> = {
"parent-1": { type: "busy" },
@@ -314,22 +314,23 @@ describe("BackgroundManager.notifyParentSession cleanup scheduling", () => {
// when
await notifyParentSessionForTest(manager, task)
sessionStatuses["parent-1"] = { type: "idle" }
await waitForDeferredWakeRetry()
await new Promise((resolve) => setTimeout(resolve, 1_180))
// then
expect(promptAsyncCalls).toHaveLength(1)
expect(promptAsyncCalls[0]?.body.noReply).toBe(false)
const wakePayload = JSON.stringify(promptAsyncCalls[0]?.body.parts)
expect(wakePayload).toContain("ALL BACKGROUND TASKS COMPLETE")
expect(wakePayload).not.toContain("BACKGROUND TASK NOTIFICATION READY")
const notificationPayload = JSON.stringify(promptAsyncCalls[0]?.body.parts)
expect(notificationPayload).toContain("ALL BACKGROUND TASKS COMPLETE")
expect(notificationPayload).not.toContain("BACKGROUND TASK NOTIFICATION READY")
})
test("#when deferred completion notification send fails #then notification is queued for the next user message", async () => {
test("#when completion notification send is aborted #then notification is queued for the next user message", async () => {
// given
const sessionStatuses: Record<string, { type: string }> = {
"parent-1": { type: "busy" },
}
const promptError = new Error("promptAsync failed")
const promptError = new Error("Request aborted while waiting for input")
promptError.name = "MessageAbortedError"
const { manager, promptAsyncCalls } = createManager(true, sessionStatuses, async () => {
throw promptError
})
@@ -337,12 +338,9 @@ describe("BackgroundManager.notifyParentSession cleanup scheduling", () => {
const task = createTask({ id: "task-a", parentSessionId: "parent-1", description: "task A", status: "completed", completedAt: new Date("2026-03-11T00:01:00.000Z") })
getTasks(manager).set(task.id, task)
getPendingByParent(manager).set(task.parentSessionId, new Set([task.id]))
await notifyParentSessionForTest(manager, task)
// when
sessionStatuses["parent-1"] = { type: "idle" }
manager.handleEvent({ type: "session.idle", properties: { sessionID: "parent-1" } })
await waitForDeferredWake()
await notifyParentSessionForTest(manager, task)
// then
expect(promptAsyncCalls).toHaveLength(1)