diff --git a/src/create-managers.ts b/src/create-managers.ts index fb8891d21..de4f5de2f 100644 --- a/src/create-managers.ts +++ b/src/create-managers.ts @@ -22,8 +22,9 @@ export function createManagers(args: { pluginConfig: OhMyOpenCodeConfig tmuxConfig: TmuxConfig modelCacheState: ModelCacheState + backgroundNotificationHookEnabled: boolean }): Managers { - const { ctx, pluginConfig, tmuxConfig, modelCacheState } = args + const { ctx, pluginConfig, tmuxConfig, modelCacheState, backgroundNotificationHookEnabled } = args const tmuxSessionManager = new TmuxSessionManager(ctx, tmuxConfig) @@ -57,6 +58,7 @@ export function createManagers(args: { log("[index] tmux cleanup error during shutdown:", error) }) }, + enableParentSessionNotifications: backgroundNotificationHookEnabled, }, ) diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts index 38ade254a..240fd187b 100644 --- a/src/features/background-agent/manager.test.ts +++ b/src/features/background-agent/manager.test.ts @@ -1003,6 +1003,52 @@ describe("BackgroundManager.notifyParentSession - aborted parent", () => { }) }) +describe("BackgroundManager.notifyParentSession - notifications toggle", () => { + test("should skip parent prompt injection when notifications are disabled", async () => { + //#given + let promptCalled = false + const promptMock = async () => { + promptCalled = true + return {} + } + const client = { + session: { + prompt: promptMock, + promptAsync: promptMock, + abort: async () => ({}), + messages: async () => ({ data: [] }), + }, + } + const manager = new BackgroundManager( + { client, directory: tmpdir() } as unknown as PluginInput, + undefined, + { enableParentSessionNotifications: false }, + ) + const task: BackgroundTask = { + id: "task-no-parent-notification", + sessionID: "session-child", + parentSessionID: "session-parent", + parentMessageID: "msg-parent", + description: "task notifications disabled", + prompt: "test", + agent: "explore", + status: "completed", + startedAt: new Date(), + completedAt: new Date(), + } + getPendingByParent(manager).set("session-parent", new Set([task.id])) + + //#when + await (manager as unknown as { notifyParentSession: (task: BackgroundTask) => Promise }) + .notifyParentSession(task) + + //#then + expect(promptCalled).toBe(false) + + manager.shutdown() + }) +}) + function buildNotificationPromptBody( task: BackgroundTask, currentMessage: CurrentMessage | null diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index 95526dcba..0fe24d936 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -92,6 +92,7 @@ export class BackgroundManager { private completionTimers: Map> = new Map() private idleDeferralTimers: Map> = new Map() private notificationQueueByParent: Map> = new Map() + private enableParentSessionNotifications: boolean readonly taskHistory = new TaskHistory() constructor( @@ -101,6 +102,7 @@ export class BackgroundManager { tmuxConfig?: TmuxConfig onSubagentSessionCreated?: OnSubagentSessionCreated onShutdown?: () => void + enableParentSessionNotifications?: boolean } ) { this.tasks = new Map() @@ -113,6 +115,7 @@ export class BackgroundManager { this.tmuxEnabled = options?.tmuxConfig?.enabled ?? false this.onSubagentSessionCreated = options?.onSubagentSessionCreated this.onShutdown = options?.onShutdown + this.enableParentSessionNotifications = options?.enableParentSessionNotifications ?? true this.registerProcessCleanup() } @@ -1203,19 +1206,22 @@ export class BackgroundManager { allComplete = true } - const statusText = task.status === "completed" ? "COMPLETED" : task.status === "interrupt" ? "INTERRUPTED" : "CANCELLED" - const errorInfo = task.error ? `\n**Error:** ${task.error}` : "" - - let notification: string - let completedTasks: BackgroundTask[] = [] - if (allComplete) { - completedTasks = Array.from(this.tasks.values()) + const completedTasks = allComplete + ? Array.from(this.tasks.values()) .filter(t => t.parentSessionID === task.parentSessionID && t.status !== "running" && t.status !== "pending") - const completedTasksText = completedTasks - .map(t => `- \`${t.id}\`: ${t.description}`) - .join("\n") + : [] - notification = ` + if (this.enableParentSessionNotifications) { + const statusText = task.status === "completed" ? "COMPLETED" : task.status === "interrupt" ? "INTERRUPTED" : "CANCELLED" + const errorInfo = task.error ? `\n**Error:** ${task.error}` : "" + + let notification: string + if (allComplete) { + const completedTasksText = completedTasks + .map(t => `- \`${t.id}\`: ${t.description}`) + .join("\n") + + notification = ` [ALL BACKGROUND TASKS COMPLETE] **Completed:** @@ -1223,9 +1229,9 @@ ${completedTasksText || `- \`${task.id}\`: ${task.description}`} Use \`background_output(task_id="")\` to retrieve each result. ` - } else { - // Individual completion - silent notification - notification = ` + } else { + // Individual completion - silent notification + notification = ` [BACKGROUND TASK ${statusText}] **ID:** \`${task.id}\` **Description:** ${task.description} @@ -1236,70 +1242,76 @@ Do NOT poll - continue productive work. Use \`background_output(task_id="${task.id}")\` to retrieve this result when ready. ` - } + } - let agent: string | undefined = task.parentAgent - let model: { providerID: string; modelID: string } | undefined + let agent: string | undefined = task.parentAgent + let model: { providerID: string; modelID: string } | undefined - try { - const messagesResp = await this.client.session.messages({ path: { id: task.parentSessionID } }) - const messages = normalizeSDKResponse(messagesResp, [] as Array<{ - info?: { agent?: string; model?: { providerID: string; modelID: string }; modelID?: string; providerID?: string } - }>) - for (let i = messages.length - 1; i >= 0; i--) { - const info = messages[i].info - if (info?.agent || info?.model || (info?.modelID && info?.providerID)) { - agent = info.agent ?? task.parentAgent - model = info.model ?? (info.providerID && info.modelID ? { providerID: info.providerID, modelID: info.modelID } : undefined) - break + try { + const messagesResp = await this.client.session.messages({ path: { id: task.parentSessionID } }) + const messages = normalizeSDKResponse(messagesResp, [] as Array<{ + info?: { agent?: string; model?: { providerID: string; modelID: string }; modelID?: string; providerID?: string } + }>) + for (let i = messages.length - 1; i >= 0; i--) { + const info = messages[i].info + if (info?.agent || info?.model || (info?.modelID && info?.providerID)) { + agent = info.agent ?? task.parentAgent + model = info.model ?? (info.providerID && info.modelID ? { providerID: info.providerID, modelID: info.modelID } : undefined) + break + } + } + } catch (error) { + if (this.isAbortedSessionError(error)) { + log("[background-agent] Parent session aborted while loading messages; using messageDir fallback:", { + taskId: task.id, + parentSessionID: task.parentSessionID, + }) + } + const messageDir = getMessageDir(task.parentSessionID) + const currentMessage = messageDir ? findNearestMessageWithFields(messageDir) : null + agent = currentMessage?.agent ?? task.parentAgent + model = currentMessage?.model?.providerID && currentMessage?.model?.modelID + ? { providerID: currentMessage.model.providerID, modelID: currentMessage.model.modelID } + : undefined + } + + log("[background-agent] notifyParentSession context:", { + taskId: task.id, + resolvedAgent: agent, + resolvedModel: model, + }) + + try { + await this.client.session.promptAsync({ + path: { id: task.parentSessionID }, + body: { + noReply: !allComplete, + ...(agent !== undefined ? { agent } : {}), + ...(model !== undefined ? { model } : {}), + ...(task.parentTools ? { tools: task.parentTools } : {}), + parts: [{ type: "text", text: notification }], + }, + }) + log("[background-agent] Sent notification to parent session:", { + taskId: task.id, + allComplete, + noReply: !allComplete, + }) + } catch (error) { + if (this.isAbortedSessionError(error)) { + log("[background-agent] Parent session aborted while sending notification; continuing cleanup:", { + taskId: task.id, + parentSessionID: task.parentSessionID, + }) + } else { + log("[background-agent] Failed to send notification:", error) } } - } catch (error) { - if (this.isAbortedSessionError(error)) { - log("[background-agent] Parent session aborted while loading messages; using messageDir fallback:", { - taskId: task.id, - parentSessionID: task.parentSessionID, - }) - } - const messageDir = getMessageDir(task.parentSessionID) - const currentMessage = messageDir ? findNearestMessageWithFields(messageDir) : null - agent = currentMessage?.agent ?? task.parentAgent - model = currentMessage?.model?.providerID && currentMessage?.model?.modelID - ? { providerID: currentMessage.model.providerID, modelID: currentMessage.model.modelID } - : undefined - } - - log("[background-agent] notifyParentSession context:", { - taskId: task.id, - resolvedAgent: agent, - resolvedModel: model, - }) - - try { - await this.client.session.promptAsync({ - path: { id: task.parentSessionID }, - body: { - noReply: !allComplete, - ...(agent !== undefined ? { agent } : {}), - ...(model !== undefined ? { model } : {}), - ...(task.parentTools ? { tools: task.parentTools } : {}), - parts: [{ type: "text", text: notification }], - }, - }) - log("[background-agent] Sent notification to parent session:", { + } else { + log("[background-agent] Parent session notifications disabled, skipping prompt injection:", { taskId: task.id, - allComplete, - noReply: !allComplete, + parentSessionID: task.parentSessionID, }) - } catch (error) { - if (this.isAbortedSessionError(error)) { - log("[background-agent] Parent session aborted while sending notification; continuing cleanup:", { - taskId: task.id, - parentSessionID: task.parentSessionID, - }) - } else { - log("[background-agent] Failed to send notification:", error) - } } if (allComplete) { diff --git a/src/index.ts b/src/index.ts index 747078518..a444e5128 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,6 +44,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { pluginConfig, tmuxConfig, modelCacheState, + backgroundNotificationHookEnabled: isHookEnabled("background-notification"), }) const toolsResult = await createTools({