Merge pull request #3056 from code-yeongyu/fix/p2-12-notification-template-consolidation
Consolidate background task notification template
This commit is contained in:
@@ -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 = `<system-reminder>
|
||||||
|
[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.
|
||||||
|
</system-reminder>`
|
||||||
|
|
||||||
|
// 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 = `<system-reminder>
|
||||||
|
[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.
|
||||||
|
</system-reminder>`
|
||||||
|
|
||||||
|
// 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 = `<system-reminder>
|
||||||
|
[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="<id>")\` to retrieve each result.
|
||||||
|
|
||||||
|
**ACTION REQUIRED:** 2 task(s) failed. Check errors above and decide whether to retry or proceed.
|
||||||
|
</system-reminder>`
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(notification).toBe(expectedNotification)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,14 +1,21 @@
|
|||||||
import type { BackgroundTask } from "./types"
|
import type { BackgroundTaskStatus } from "./types"
|
||||||
|
|
||||||
export type BackgroundTaskNotificationStatus = "COMPLETED" | "CANCELLED" | "INTERRUPTED" | "ERROR"
|
export type BackgroundTaskNotificationStatus = "COMPLETED" | "CANCELLED" | "INTERRUPTED" | "ERROR"
|
||||||
|
|
||||||
|
export interface BackgroundTaskNotificationTask {
|
||||||
|
id: string
|
||||||
|
description: string
|
||||||
|
status: BackgroundTaskStatus
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
export function buildBackgroundTaskNotificationText(input: {
|
export function buildBackgroundTaskNotificationText(input: {
|
||||||
task: BackgroundTask
|
task: BackgroundTaskNotificationTask
|
||||||
duration: string
|
duration: string
|
||||||
statusText: BackgroundTaskNotificationStatus
|
statusText: BackgroundTaskNotificationStatus
|
||||||
allComplete: boolean
|
allComplete: boolean
|
||||||
remainingCount: number
|
remainingCount: number
|
||||||
completedTasks: BackgroundTask[]
|
completedTasks: BackgroundTaskNotificationTask[]
|
||||||
}): string {
|
}): string {
|
||||||
const { task, duration, statusText, allComplete, remainingCount, completedTasks } = input
|
const { task, duration, statusText, allComplete, remainingCount, completedTasks } = input
|
||||||
|
|
||||||
@@ -50,14 +57,12 @@ Use \`background_output(task_id="<id>")\` to retrieve each result.${hasFailures
|
|||||||
</system-reminder>`
|
</system-reminder>`
|
||||||
}
|
}
|
||||||
|
|
||||||
const agentInfo = task.category ? `${task.agent} (${task.category})` : task.agent
|
|
||||||
const isFailure = statusText !== "COMPLETED"
|
const isFailure = statusText !== "COMPLETED"
|
||||||
|
|
||||||
return `<system-reminder>
|
return `<system-reminder>
|
||||||
[BACKGROUND TASK ${statusText}]
|
[BACKGROUND TASK ${statusText}]
|
||||||
**ID:** \`${task.id}\`
|
**ID:** \`${task.id}\`
|
||||||
**Description:** ${task.description}
|
**Description:** ${task.description}
|
||||||
**Agent:** ${agentInfo}
|
|
||||||
**Duration:** ${duration}${errorInfo}
|
**Duration:** ${duration}${errorInfo}
|
||||||
|
|
||||||
**${remainingCount} task${remainingCount === 1 ? "" : "s"} still in progress.** You WILL be notified when ALL complete.
|
**${remainingCount} task${remainingCount === 1 ? "" : "s"} still in progress.** You WILL be notified when ALL complete.
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ import {
|
|||||||
import { subagentSessions } from "../claude-code-session-state"
|
import { subagentSessions } from "../claude-code-session-state"
|
||||||
import { getTaskToastManager } from "../task-toast-manager"
|
import { getTaskToastManager } from "../task-toast-manager"
|
||||||
import { formatDuration } from "./duration-formatter"
|
import { formatDuration } from "./duration-formatter"
|
||||||
|
import {
|
||||||
|
buildBackgroundTaskNotificationText,
|
||||||
|
type BackgroundTaskNotificationTask,
|
||||||
|
} from "./background-task-notification-template"
|
||||||
import {
|
import {
|
||||||
isAbortedSessionError,
|
isAbortedSessionError,
|
||||||
extractErrorName,
|
extractErrorName,
|
||||||
@@ -151,7 +155,7 @@ export class BackgroundManager {
|
|||||||
private queuesByKey: Map<string, QueueItem[]> = new Map()
|
private queuesByKey: Map<string, QueueItem[]> = new Map()
|
||||||
private processingKeys: Set<string> = new Set()
|
private processingKeys: Set<string> = new Set()
|
||||||
private completionTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()
|
private completionTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()
|
||||||
private completedTaskSummaries: Map<string, Array<{id: string, description: string, status: string, error?: string}>> = new Map()
|
private completedTaskSummaries: Map<string, BackgroundTaskNotificationTask[]> = new Map()
|
||||||
private idleDeferralTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()
|
private idleDeferralTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()
|
||||||
private notificationQueueByParent: Map<string, Promise<void>> = new Map()
|
private notificationQueueByParent: Map<string, Promise<void>> = new Map()
|
||||||
private rootDescendantCounts: Map<string, number>
|
private rootDescendantCounts: Map<string, number>
|
||||||
@@ -1632,56 +1636,14 @@ export class BackgroundManager {
|
|||||||
: task.status === "error"
|
: task.status === "error"
|
||||||
? "ERROR"
|
? "ERROR"
|
||||||
: "CANCELLED"
|
: "CANCELLED"
|
||||||
const errorInfo = task.error ? `\n**Error:** ${task.error}` : ""
|
const notification = buildBackgroundTaskNotificationText({
|
||||||
|
task,
|
||||||
let notification: string
|
duration,
|
||||||
if (allComplete) {
|
statusText,
|
||||||
const succeededTasks = completedTasks.filter(t => t.status === "completed")
|
allComplete,
|
||||||
const failedTasks = completedTasks.filter(t => t.status !== "completed")
|
remainingCount,
|
||||||
|
completedTasks,
|
||||||
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 = `<system-reminder>
|
|
||||||
${header}
|
|
||||||
|
|
||||||
${body.trim()}
|
|
||||||
|
|
||||||
Use \`background_output(task_id="<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.` : ""}
|
|
||||||
</system-reminder>`
|
|
||||||
} else {
|
|
||||||
notification = `<system-reminder>
|
|
||||||
[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.
|
|
||||||
</system-reminder>`
|
|
||||||
}
|
|
||||||
|
|
||||||
let agent: string | undefined = task.parentAgent
|
let agent: string | undefined = task.parentAgent
|
||||||
let model: { providerID: string; modelID: string } | undefined
|
let model: { providerID: string; modelID: string } | undefined
|
||||||
|
|||||||
Reference in New Issue
Block a user