diff --git a/src/features/background-agent/background-task-notification-template.test.ts b/src/features/background-agent/background-task-notification-template.test.ts new file mode 100644 index 000000000..5555e909a --- /dev/null +++ b/src/features/background-agent/background-task-notification-template.test.ts @@ -0,0 +1,130 @@ +import { describe, expect, test } from "bun:test" +import { buildBackgroundTaskNotificationText } from "./background-task-notification-template" + +describe("buildBackgroundTaskNotificationText", () => { + describe("#given one task still running after a completed task notification", () => { + test("#when building the partial notification #then it preserves the existing completed-task format", () => { + // given + const notification = buildBackgroundTaskNotificationText({ + task: { + id: "task-1", + description: "Index repo", + status: "completed", + }, + duration: "42s", + statusText: "COMPLETED", + allComplete: false, + remainingCount: 1, + completedTasks: [], + }) + + // when + const expectedNotification = ` +[BACKGROUND TASK COMPLETED] +**ID:** \`task-1\` +**Description:** Index repo +**Duration:** 42s + +**1 task still in progress.** You WILL be notified when ALL complete. +Do NOT poll - continue productive work. + +Use \`background_output(task_id="task-1")\` to retrieve this result when ready. +` + + // then + expect(notification).toBe(expectedNotification) + }) + }) + + describe("#given one task still running after a failed task notification", () => { + test("#when building the partial notification #then it preserves the existing failure format", () => { + // given + const notification = buildBackgroundTaskNotificationText({ + task: { + id: "task-2", + description: "Summarize logs", + status: "error", + error: "Timed out", + }, + duration: "3m 4s", + statusText: "ERROR", + allComplete: false, + remainingCount: 2, + completedTasks: [], + }) + + // when + const expectedNotification = ` +[BACKGROUND TASK ERROR] +**ID:** \`task-2\` +**Description:** Summarize logs +**Duration:** 3m 4s +**Error:** Timed out + +**2 tasks still in progress.** You WILL be notified when ALL complete. +**ACTION REQUIRED:** This task failed. Check the error and decide whether to retry, cancel remaining tasks, or continue. + +Use \`background_output(task_id="task-2")\` to retrieve this result when ready. +` + + // then + expect(notification).toBe(expectedNotification) + }) + }) + + describe("#given all sibling tasks completed with mixed outcomes", () => { + test("#when building the final notification #then it preserves the existing summary format", () => { + // given + const notification = buildBackgroundTaskNotificationText({ + task: { + id: "task-3", + description: "Fallback task", + status: "error", + error: "Denied", + }, + duration: "10s", + statusText: "ERROR", + allComplete: true, + remainingCount: 0, + completedTasks: [ + { + id: "task-1", + description: "Index repo", + status: "completed", + }, + { + id: "task-2", + description: "Summarize logs", + status: "cancelled", + error: "User aborted", + }, + { + id: "task-3", + description: "Fallback task", + status: "error", + error: "Denied", + }, + ], + }) + + // when + const expectedNotification = ` +[ALL BACKGROUND TASKS FINISHED - 2 FAILED] + +**Completed:** +- \`task-1\`: Index repo + +**Failed:** +- \`task-2\`: Summarize logs [CANCELLED] - User aborted +- \`task-3\`: Fallback task [ERROR] - Denied + +Use \`background_output(task_id="")\` to retrieve each result. + +**ACTION REQUIRED:** 2 task(s) failed. Check errors above and decide whether to retry or proceed. +` + + // then + expect(notification).toBe(expectedNotification) + }) + }) +}) diff --git a/src/features/background-agent/background-task-notification-template.ts b/src/features/background-agent/background-task-notification-template.ts index e2e74cc78..240efe9c0 100644 --- a/src/features/background-agent/background-task-notification-template.ts +++ b/src/features/background-agent/background-task-notification-template.ts @@ -1,14 +1,21 @@ -import type { BackgroundTask } from "./types" +import type { BackgroundTaskStatus } from "./types" export type BackgroundTaskNotificationStatus = "COMPLETED" | "CANCELLED" | "INTERRUPTED" | "ERROR" +export interface BackgroundTaskNotificationTask { + id: string + description: string + status: BackgroundTaskStatus + error?: string +} + export function buildBackgroundTaskNotificationText(input: { - task: BackgroundTask + task: BackgroundTaskNotificationTask duration: string statusText: BackgroundTaskNotificationStatus allComplete: boolean remainingCount: number - completedTasks: BackgroundTask[] + completedTasks: BackgroundTaskNotificationTask[] }): string { const { task, duration, statusText, allComplete, remainingCount, completedTasks } = input @@ -50,14 +57,12 @@ Use \`background_output(task_id="")\` to retrieve each result.${hasFailures ` } - const agentInfo = task.category ? `${task.agent} (${task.category})` : task.agent const isFailure = statusText !== "COMPLETED" return ` [BACKGROUND TASK ${statusText}] **ID:** \`${task.id}\` **Description:** ${task.description} -**Agent:** ${agentInfo} **Duration:** ${duration}${errorInfo} **${remainingCount} task${remainingCount === 1 ? "" : "s"} still in progress.** You WILL be notified when ALL complete. diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index efbed503f..621700030 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -34,6 +34,10 @@ import { import { subagentSessions } from "../claude-code-session-state" import { getTaskToastManager } from "../task-toast-manager" import { formatDuration } from "./duration-formatter" +import { + buildBackgroundTaskNotificationText, + type BackgroundTaskNotificationTask, +} from "./background-task-notification-template" import { isAbortedSessionError, extractErrorName, @@ -151,7 +155,7 @@ export class BackgroundManager { private queuesByKey: Map = new Map() private processingKeys: Set = new Set() private completionTimers: Map> = new Map() - private completedTaskSummaries: Map> = new Map() + private completedTaskSummaries: Map = new Map() private idleDeferralTimers: Map> = new Map() private notificationQueueByParent: Map> = new Map() private rootDescendantCounts: Map @@ -1632,56 +1636,14 @@ export class BackgroundManager { : task.status === "error" ? "ERROR" : "CANCELLED" - const errorInfo = task.error ? `\n**Error:** ${task.error}` : "" - - let notification: string - if (allComplete) { - const succeededTasks = completedTasks.filter(t => t.status === "completed") - const failedTasks = completedTasks.filter(t => t.status !== "completed") - - const succeededText = succeededTasks.length > 0 - ? succeededTasks.map(t => `- \`${t.id}\`: ${t.description}`).join("\n") - : "" - const failedText = failedTasks.length > 0 - ? failedTasks.map(t => `- \`${t.id}\`: ${t.description} [${t.status.toUpperCase()}]${t.error ? ` - ${t.error}` : ""}`).join("\n") - : "" - - const hasFailures = failedTasks.length > 0 - const header = hasFailures - ? `[ALL BACKGROUND TASKS FINISHED - ${failedTasks.length} FAILED]` - : "[ALL BACKGROUND TASKS COMPLETE]" - - let body = "" - if (succeededText) { - body += `**Completed:**\n${succeededText}\n` - } - if (failedText) { - body += `\n**Failed:**\n${failedText}\n` - } - if (!body) { - body = `- \`${task.id}\`: ${task.description} [${task.status.toUpperCase()}]${task.error ? ` - ${task.error}` : ""}\n` - } - - notification = ` -${header} - -${body.trim()} - -Use \`background_output(task_id="")\` to retrieve each result.${hasFailures ? `\n\n**ACTION REQUIRED:** ${failedTasks.length} task(s) failed. Check errors above and decide whether to retry or proceed.` : ""} -` - } else { - notification = ` -[BACKGROUND TASK ${statusText}] -**ID:** \`${task.id}\` -**Description:** ${task.description} -**Duration:** ${duration}${errorInfo} - -**${remainingCount} task${remainingCount === 1 ? "" : "s"} still in progress.** You WILL be notified when ALL complete. -${statusText === "COMPLETED" ? "Do NOT poll - continue productive work." : "**ACTION REQUIRED:** This task failed. Check the error and decide whether to retry, cancel remaining tasks, or continue."} - -Use \`background_output(task_id="${task.id}")\` to retrieve this result when ready. -` - } + const notification = buildBackgroundTaskNotificationText({ + task, + duration, + statusText, + allComplete, + remainingCount, + completedTasks, + }) let agent: string | undefined = task.parentAgent let model: { providerID: string; modelID: string } | undefined