fix(background-agent): defer parent wake during active turns
This commit is contained in:
@@ -108,7 +108,7 @@ async function flushPendingParentWakeForTest(manager: BackgroundManager, session
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("BackgroundManager parent wake active turn events", () => {
|
describe("BackgroundManager parent wake active turn events", () => {
|
||||||
test("#when background task completes during active parent turn #then parent gets same-turn no-reply reminder", async () => {
|
test("#when background task completes during active parent turn #then parent wake stays deferred", async () => {
|
||||||
// given
|
// given
|
||||||
const sessionStatuses: Record<string, { type: string }> = {
|
const sessionStatuses: Record<string, { type: string }> = {
|
||||||
"parent-1": { type: "busy" },
|
"parent-1": { type: "busy" },
|
||||||
@@ -129,9 +129,57 @@ describe("BackgroundManager parent wake active turn events", () => {
|
|||||||
await notifyParentSessionForTest(manager, task)
|
await notifyParentSessionForTest(manager, task)
|
||||||
await flushPendingParentWakeForTest(manager, "parent-1")
|
await flushPendingParentWakeForTest(manager, "parent-1")
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(promptAsyncCalls).toHaveLength(0)
|
||||||
|
expect(getPendingParentWakes(manager).has("parent-1")).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#when duplicate background completions overlap an active parent turn #then one coalesced wake dispatches after idle", async () => {
|
||||||
|
// given
|
||||||
|
const sessionStatuses: Record<string, { type: string }> = {
|
||||||
|
"parent-1": { type: "busy" },
|
||||||
|
}
|
||||||
|
const { manager, promptAsyncCalls } = createManager(sessionStatuses)
|
||||||
|
managerUnderTest = manager
|
||||||
|
const taskA = createTask({
|
||||||
|
id: "task-a",
|
||||||
|
parentSessionId: "parent-1",
|
||||||
|
description: "task A",
|
||||||
|
status: "completed",
|
||||||
|
completedAt: new Date("2026-05-20T14:19:14.625Z"),
|
||||||
|
})
|
||||||
|
const taskB = createTask({
|
||||||
|
id: "task-b",
|
||||||
|
parentSessionId: "parent-1",
|
||||||
|
description: "task B",
|
||||||
|
status: "completed",
|
||||||
|
completedAt: new Date("2026-05-20T14:19:15.625Z"),
|
||||||
|
})
|
||||||
|
getTasks(manager).set(taskA.id, taskA)
|
||||||
|
getTasks(manager).set(taskB.id, taskB)
|
||||||
|
getPendingByParent(manager).set(taskA.parentSessionId, new Set([taskA.id, taskB.id]))
|
||||||
|
|
||||||
|
// when
|
||||||
|
await notifyParentSessionForTest(manager, taskA)
|
||||||
|
await notifyParentSessionForTest(manager, taskB)
|
||||||
|
await Promise.all([
|
||||||
|
flushPendingParentWakeForTest(manager, "parent-1"),
|
||||||
|
flushPendingParentWakeForTest(manager, "parent-1"),
|
||||||
|
])
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(promptAsyncCalls).toHaveLength(0)
|
||||||
|
expect(getPendingParentWakes(manager).has("parent-1")).toBe(true)
|
||||||
|
|
||||||
|
// when
|
||||||
|
sessionStatuses["parent-1"] = { type: "idle" }
|
||||||
|
await Promise.all([
|
||||||
|
flushPendingParentWakeForTest(manager, "parent-1"),
|
||||||
|
flushPendingParentWakeForTest(manager, "parent-1"),
|
||||||
|
])
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(promptAsyncCalls).toHaveLength(1)
|
expect(promptAsyncCalls).toHaveLength(1)
|
||||||
expect(promptAsyncCalls[0]?.body.noReply).toBe(true)
|
|
||||||
expect(JSON.stringify(promptAsyncCalls[0]?.body.parts)).toContain("ALL BACKGROUND TASKS COMPLETE")
|
expect(JSON.stringify(promptAsyncCalls[0]?.body.parts)).toContain("ALL BACKGROUND TASKS COMPLETE")
|
||||||
expect(getPendingParentWakes(manager).has("parent-1")).toBe(false)
|
expect(getPendingParentWakes(manager).has("parent-1")).toBe(false)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -77,19 +77,6 @@ type ToolWaitDeferralDecision = {
|
|||||||
|
|
||||||
type Unrefable = ReturnType<typeof setTimeout> & { unref?: () => unknown }
|
type Unrefable = ReturnType<typeof setTimeout> & { unref?: () => unknown }
|
||||||
|
|
||||||
const ACTIVE_TURN_COMPLETION_NOTIFICATION_MARKERS = [
|
|
||||||
"[BACKGROUND TASK COMPLETED]",
|
|
||||||
"[ALL BACKGROUND TASKS COMPLETE]",
|
|
||||||
] as const
|
|
||||||
|
|
||||||
function notificationAllowsActiveTurnDelivery(notification: string): boolean {
|
|
||||||
return ACTIVE_TURN_COMPLETION_NOTIFICATION_MARKERS.some((marker) => notification.includes(marker))
|
|
||||||
}
|
|
||||||
|
|
||||||
function pendingWakeAllowsActiveTurnDelivery(wake: PendingParentWake): boolean {
|
|
||||||
return wake.notifications.length > 0 && wake.notifications.every(notificationAllowsActiveTurnDelivery)
|
|
||||||
}
|
|
||||||
|
|
||||||
function unrefTimerHandle(handle: ReturnType<typeof setTimeout>): void {
|
function unrefTimerHandle(handle: ReturnType<typeof setTimeout>): void {
|
||||||
const maybeUnref = (handle as Unrefable).unref
|
const maybeUnref = (handle as Unrefable).unref
|
||||||
if (typeof maybeUnref === "function") {
|
if (typeof maybeUnref === "function") {
|
||||||
@@ -179,8 +166,7 @@ export class ParentWakeNotifier {
|
|||||||
if (!latestWake) {
|
if (!latestWake) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const canDeliverDuringActiveTurn = sessionActive && pendingWakeAllowsActiveTurnDelivery(latestWake)
|
if (sessionActive) {
|
||||||
if (sessionActive && !canDeliverDuringActiveTurn) {
|
|
||||||
this.schedulePendingParentWakeFlush(sessionID)
|
this.schedulePendingParentWakeFlush(sessionID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -234,12 +220,12 @@ export class ParentWakeNotifier {
|
|||||||
source: "background-agent-parent-wake",
|
source: "background-agent-parent-wake",
|
||||||
settleMs: 0,
|
settleMs: 0,
|
||||||
queueBehavior: "defer",
|
queueBehavior: "defer",
|
||||||
checkStatus: !canDeliverDuringActiveTurn,
|
checkStatus: true,
|
||||||
checkToolState: !toolWaitDecision.skipPromptGateToolStateCheck,
|
checkToolState: !toolWaitDecision.skipPromptGateToolStateCheck,
|
||||||
input: {
|
input: {
|
||||||
path: { id: sessionID },
|
path: { id: sessionID },
|
||||||
body: {
|
body: {
|
||||||
noReply: canDeliverDuringActiveTurn ? true : !latestWake.shouldReply,
|
noReply: !latestWake.shouldReply,
|
||||||
...latestWake.promptContext,
|
...latestWake.promptContext,
|
||||||
parts: [createInternalAgentTextPart(notificationContent)],
|
parts: [createInternalAgentTextPart(notificationContent)],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user