From dbaea82b739f3f268d155cc6721cedb5c93bfac3 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 10 May 2026 14:56:28 +0900 Subject: [PATCH] fix(background-agent): retain completed tasks via archive fallback after cleanup MessageAbortedError/worker shutdown could race with scheduled removeTask, leaving background_output's manager.getTask returning 'Task not found' even though the task had completed cleanly. Fix: add completedTaskArchive (max 500, FIFO eviction). On removeTask, archive non-running/pending tasks with sessionId. getTask falls back to archive on active-map miss. addTask clears stale archive entries on re-registration. Fixes #3895 --- src/features/background-agent/manager.test.ts | 27 +++++++++++++++++++ src/features/background-agent/manager.ts | 25 ++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts index 5f622466b..9470d12a7 100644 --- a/src/features/background-agent/manager.test.ts +++ b/src/features/background-agent/manager.test.ts @@ -5991,6 +5991,33 @@ describe("BackgroundManager regression fixes - resume and aborted notification", manager.shutdown() }) + + test("should keep completed task retrievable after scheduled removal", () => { + //#given + const manager = createBackgroundManager() + const task: BackgroundTask = { + id: "task-archive-regression", + sessionId: "session-archive-regression", + parentSessionId: "parent-session", + parentMessageId: "msg-1", + description: "archive regression", + prompt: "test", + agent: "explore", + status: "completed", + startedAt: new Date(), + completedAt: new Date(), + } + getTaskMap(manager).set(task.id, task) + + //#when + ;(cast<{ removeTask: (task: BackgroundTask) => void }>(manager)).removeTask(task) + + //#then + expect(getTaskMap(manager).has(task.id)).toBe(false) + expect(manager.getTask(task.id)?.sessionId).toBe(task.sessionId) + + manager.shutdown() + }) }) describe("BackgroundManager - tool permission spread order", () => { diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index 85df81f1a..a5c988a7b 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -188,6 +188,7 @@ export interface SubagentSessionCreatedEvent { export type OnSubagentSessionCreated = (event: SubagentSessionCreatedEvent) => Promise const MAX_TASK_REMOVAL_RESCHEDULES = 6 +const MAX_COMPLETED_TASK_ARCHIVE_SIZE = 500 export interface BackgroundManagerConfig { pluginContext: PluginInput @@ -222,6 +223,7 @@ export class BackgroundManager { private queuesByKey: Map = new Map() private processingKeys: Set = new Set() private completionTimers: Map> = new Map() + private completedTaskArchive: Map = new Map() private completedTaskSummaries: Map = new Map() private idleDeferralTimers: Map> = new Map() private notificationQueueByParent: Map> = new Map() @@ -347,6 +349,7 @@ export class BackgroundManager { } private addTask(task: BackgroundTask): void { + this.completedTaskArchive.delete(task.id) this.tasks.set(task.id, task) if (!task.parentSessionId) { return @@ -358,10 +361,30 @@ export class BackgroundManager { } private removeTask(task: BackgroundTask): void { + this.archiveCompletedTask(task) this.tasks.delete(task.id) this.removeTaskFromParentIndex(task.id, task.parentSessionId) } + private archiveCompletedTask(task: BackgroundTask): void { + if (!task.sessionId) { + return + } + if (task.status === "running" || task.status === "pending") { + return + } + + this.completedTaskArchive.set(task.id, task) + if (this.completedTaskArchive.size <= MAX_COMPLETED_TASK_ARCHIVE_SIZE) { + return + } + + const oldestTaskID = this.completedTaskArchive.keys().next().value + if (typeof oldestTaskID === "string") { + this.completedTaskArchive.delete(oldestTaskID) + } + } + private updateTaskParent(task: BackgroundTask, parentSessionID: string): void { if (task.parentSessionId === parentSessionID) { return @@ -830,7 +853,7 @@ The fallback retry session is now created and can be inspected directly. } getTask(id: string): BackgroundTask | undefined { - return this.tasks.get(id) + return this.tasks.get(id) ?? this.completedTaskArchive.get(id) } getTasksByParentSession(sessionID: string): BackgroundTask[] {