feat(background-task): render retry timelines and links

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Choi Kijin / 최 기진 / チョイ キジン
2026-04-28 15:29:49 +09:00
parent 25548f2561
commit 79054ea3e5
7 changed files with 952 additions and 5 deletions
@@ -154,6 +154,76 @@ Use \`background_output(task_id="<id>")\` to retrieve each result.
})
})
describe("#given a completed task with retry attempt history", () => {
test("#when building the final notification #then it renders the spec-aligned balanced attempt timeline", () => {
// given
const notification = buildBackgroundTaskNotificationText({
task: {
id: "task-3",
description: "Fallback task",
status: "completed",
attempts: [
{
attemptID: "att-1",
attemptNumber: 1,
sessionID: "ses-primary",
providerID: "genai-proxy-openai",
modelID: "gpt-5.4-mini",
status: "error",
error: "Forbidden: Selected provider is forbidden",
},
{
attemptID: "att-2",
attemptNumber: 2,
sessionID: "ses-fallback",
providerID: "anthropic",
modelID: "claude-haiku-4.5",
status: "completed",
},
],
},
duration: "10s",
statusText: "COMPLETED",
allComplete: true,
remainingCount: 0,
completedTasks: [
{
id: "task-3",
description: "Fallback task",
status: "completed",
attempts: [
{
attemptID: "att-1",
attemptNumber: 1,
sessionID: "ses-primary",
providerID: "genai-proxy-openai",
modelID: "gpt-5.4-mini",
status: "error",
error: "Forbidden: Selected provider is forbidden",
},
{
attemptID: "att-2",
attemptNumber: 2,
sessionID: "ses-fallback",
providerID: "anthropic",
modelID: "claude-haiku-4.5",
status: "completed",
},
],
},
],
})
// then
expect(notification).toContain("[ALL BACKGROUND TASKS COMPLETE]")
expect(notification).toContain("- `task-3`: Fallback task")
expect(notification).toContain("Background task attempts:")
expect(notification).toContain(" - Attempt 1 — ERROR — genai-proxy-openai/gpt-5.4-mini — ses-primary")
expect(notification).toContain(" Error: Forbidden: Selected provider is forbidden")
expect(notification).toContain(" - Attempt 2 — COMPLETED — anthropic/claude-haiku-4.5 — ses-fallback")
})
})
describe("#given a single task notification with undefined description", () => {
test("#when building the partial notification #then it uses task ID as fallback", () => {
// given
@@ -1,4 +1,4 @@
import type { BackgroundTaskStatus } from "./types"
import type { BackgroundTaskAttempt, BackgroundTaskStatus } from "./types"
export type BackgroundTaskNotificationStatus = "COMPLETED" | "CANCELLED" | "INTERRUPTED" | "ERROR"
@@ -7,6 +7,55 @@ export interface BackgroundTaskNotificationTask {
description: string
status: BackgroundTaskStatus
error?: string
attempts?: BackgroundTaskAttempt[]
}
function formatAttemptModel(attempt: BackgroundTaskAttempt): string {
if (attempt.providerID && attempt.modelID) {
return `${attempt.providerID}/${attempt.modelID}`
}
if (attempt.modelID) {
return attempt.modelID
}
if (attempt.providerID) {
return attempt.providerID
}
return "unknown-model"
}
function formatAttemptTimeline(task: BackgroundTaskNotificationTask): string {
if (!task.attempts || task.attempts.length <= 1) {
return ""
}
const lines = task.attempts
.map((attempt) => {
const attemptLines = [
` - Attempt ${attempt.attemptNumber}${attempt.status.toUpperCase()}${formatAttemptModel(attempt)}${attempt.sessionID ?? "unknown"}`,
]
if (attempt.status !== "completed" && attempt.error) {
attemptLines.push(` Error: ${attempt.error}`)
}
return attemptLines.join("\n")
})
.join("\n")
return `Background task attempts:\n${lines}`
}
function formatTaskSummaryLine(task: BackgroundTaskNotificationTask): string {
const baseLine = `- \`${task.id}\`: ${task.description || task.id}`
const statusSuffix = task.status === "completed"
? ""
: ` [${task.status.toUpperCase()}]${task.error ? ` - ${task.error}` : ""}`
const timeline = formatAttemptTimeline(task)
return `${baseLine}${statusSuffix}${timeline ? `\n${timeline}` : ""}`
}
export function buildBackgroundTaskNotificationText(input: {
@@ -27,10 +76,10 @@ export function buildBackgroundTaskNotificationText(input: {
const failedTasks = completedTasks.filter((t) => t.status !== "completed")
const succeededText = succeededTasks.length > 0
? succeededTasks.map((t) => `- \`${t.id}\`: ${safeDescription(t)}`).join("\n")
? succeededTasks.map((t) => formatTaskSummaryLine(t)).join("\n")
: ""
const failedText = failedTasks.length > 0
? failedTasks.map((t) => `- \`${t.id}\`: ${safeDescription(t)} [${t.status.toUpperCase()}]${t.error ? ` - ${t.error}` : ""}`).join("\n")
? failedTasks.map((t) => formatTaskSummaryLine(t)).join("\n")
: ""
const hasFailures = failedTasks.length > 0
@@ -46,7 +95,7 @@ export function buildBackgroundTaskNotificationText(input: {
body += `\n**Failed:**\n${failedText}\n`
}
if (!body) {
body = `- \`${task.id}\`: ${safeDescription(task)} [${task.status.toUpperCase()}]${task.error ? ` - ${task.error}` : ""}\n`
body = `${formatTaskSummaryLine(task)}\n`
}
return `<system-reminder>