fix(background-agent): queue notifications for idle parent sessions

When a background task completes and the parent session is waiting for
user input, promptAsync() fails with an aborted error. Previously the
notification was silently dropped — lost forever.

Fix: queue the notification text in-memory on the BackgroundManager
when promptAsync fails with an aborted/idle error. On the user's next
message to that session, the queued notifications are injected into the
chat context before the agent sees the message.

- BackgroundManager: add pendingNotifications map + queuePendingNotification()
  and injectPendingNotificationsIntoChatMessage() methods
- background-notification hook: add chat.message handler that calls injection
- chat-message.ts: wire backgroundNotificationHook.chat.message into the
  message processing chain
- Add tests covering queue-on-abort and next-message delivery
This commit is contained in:
YeonGyu-Kim
2026-02-25 16:26:31 +09:00
parent 91ce705b7d
commit b5d92c76a5
5 changed files with 144 additions and 0 deletions
+27
View File
@@ -19,6 +19,7 @@ function createMockHandlerArgs(overrides?: {
},
hooks: {
stopContinuationGuard: null,
backgroundNotificationHook: null,
keywordDetector: null,
claudeCodeHooks: null,
autoSlashCommand: null,
@@ -115,4 +116,30 @@ describe("createChatMessageHandler - TUI variant passthrough", () => {
//#then - gate should still be marked as applied
expect(args._appliedSessions).toContain("test-session")
})
test("injects queued background notifications through chat.message hook", async () => {
//#given
const args = createMockHandlerArgs()
args.hooks.backgroundNotificationHook = {
"chat.message": async (
_input: { sessionID: string },
output: ChatMessageHandlerOutput,
): Promise<void> => {
output.parts.push({
type: "text",
text: "<system-reminder>[BACKGROUND TASK COMPLETED]</system-reminder>",
})
},
}
const handler = createChatMessageHandler(args)
const input = createMockInput("hephaestus", { providerID: "openai", modelID: "gpt-5.3-codex" })
const output = createMockOutput()
//#when
await handler(input, output)
//#then
expect(output.parts).toHaveLength(1)
expect(output.parts[0].text).toContain("[BACKGROUND TASK COMPLETED]")
})
})
+1
View File
@@ -97,6 +97,7 @@ export function createChatMessageHandler(args: {
setSessionModel(input.sessionID, input.model)
}
await hooks.stopContinuationGuard?.["chat.message"]?.(input)
await hooks.backgroundNotificationHook?.["chat.message"]?.(input, output)
await hooks.runtimeFallback?.["chat.message"]?.(input, output)
await hooks.keywordDetector?.["chat.message"]?.(input, output)
await hooks.claudeCodeHooks?.["chat.message"]?.(input, output)