fix(background-agent): preserve parent activity across idle events

Cubic caught that clearing recent parent activity on session.idle could reopen the stale-idle race. Keep the fresh activity marker until its normal expiry so background completions still defer while the parent turn is actively reasoning.

Red: parent idle after fresh reasoning delta dispatched an unexpected parent wake. Green: focused parent-wake regression suite passes.
This commit is contained in:
YeonGyu-Kim
2026-05-21 12:07:40 +09:00
parent a8201726ef
commit 0ad4d974f1
3 changed files with 35 additions and 5 deletions
-1
View File
@@ -1625,7 +1625,6 @@ The fallback retry session is now created and can be inspected directly.
if (!props || typeof props !== "object") return if (!props || typeof props !== "object") return
const sessionID = resolveSessionEventID(props) const sessionID = resolveSessionEventID(props)
if (sessionID) { if (sessionID) {
this.parentWakeNotifier.clearParentSessionActivity(sessionID)
void this.enqueueNotificationForParent(sessionID, () => this.flushPendingParentWake(sessionID)).catch((error) => { void this.enqueueNotificationForParent(sessionID, () => this.flushPendingParentWake(sessionID)).catch((error) => {
log("[background-agent] Failed to flush pending parent wake:", { sessionID, error }) log("[background-agent] Failed to flush pending parent wake:", { sessionID, error })
}) })
@@ -141,4 +141,39 @@ describe("BackgroundManager parent wake active turn events", () => {
expect(promptAsyncCalls).toHaveLength(0) expect(promptAsyncCalls).toHaveLength(0)
expect(getPendingParentWakes(manager).has("parent-1")).toBe(true) expect(getPendingParentWakes(manager).has("parent-1")).toBe(true)
}) })
test("#when parent idle event follows fresh reasoning delta #then background completion still does not fork a reply", async () => {
// given
const sessionStatuses: Record<string, { type: string }> = {
"parent-1": { type: "idle" },
}
const { manager, promptAsyncCalls } = createManager(sessionStatuses)
managerUnderTest = manager
manager.handleEvent({
type: "message.part.delta",
properties: {
sessionID: "parent-1",
field: "reasoning",
delta: "still thinking",
},
})
const task = createTask({
id: "task-a",
parentSessionId: "parent-1",
description: "task A",
status: "completed",
completedAt: new Date("2026-05-20T14:19:14.625Z"),
})
getTasks(manager).set(task.id, task)
getPendingByParent(manager).set(task.parentSessionId, new Set([task.id]))
// when
await notifyParentSessionForTest(manager, task)
manager.handleEvent({ type: "session.idle", properties: { sessionID: "parent-1" } })
await flushPendingParentWakeForTest(manager, "parent-1")
// then
expect(promptAsyncCalls).toHaveLength(0)
expect(getPendingParentWakes(manager).has("parent-1")).toBe(true)
})
}) })
@@ -122,10 +122,6 @@ export class ParentWakeNotifier {
this.recentParentSessionActivity.set(sessionID, Date.now()) this.recentParentSessionActivity.set(sessionID, Date.now())
} }
clearParentSessionActivity(sessionID: string): void {
this.recentParentSessionActivity.delete(sessionID)
}
queuePendingParentWake( queuePendingParentWake(
sessionID: string, sessionID: string,
notification: string, notification: string,