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:
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user