Merge pull request #3913 from code-yeongyu/fix/bg-task-registry-aborted
fix(background-agent): retain completed tasks via archive fallback after cleanup
This commit is contained in:
@@ -107,7 +107,8 @@ describe("BackgroundManager.cancelTask cleanup", () => {
|
|||||||
expect(cancelled).toBe(true)
|
expect(cancelled).toBe(true)
|
||||||
expect(getPendingByParent(manager).get(task.parentSessionId)).toBeUndefined()
|
expect(getPendingByParent(manager).get(task.parentSessionId)).toBeUndefined()
|
||||||
runScheduledCleanup(manager, task.id)
|
runScheduledCleanup(manager, task.id)
|
||||||
expect(manager.getTask(task.id)).toBeUndefined()
|
expect(getTaskMap(manager).has(task.id)).toBe(false)
|
||||||
|
expect(manager.getTask(task.id)?.sessionId).toBe(task.sessionId)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("#given a running task #when cancelTask called with skipNotification=false #then task is also eventually removed", async () => {
|
test("#given a running task #when cancelTask called with skipNotification=false #then task is also eventually removed", async () => {
|
||||||
@@ -131,7 +132,8 @@ describe("BackgroundManager.cancelTask cleanup", () => {
|
|||||||
// then
|
// then
|
||||||
expect(cancelled).toBe(true)
|
expect(cancelled).toBe(true)
|
||||||
runScheduledCleanup(manager, task.id)
|
runScheduledCleanup(manager, task.id)
|
||||||
expect(manager.getTask(task.id)).toBeUndefined()
|
expect(getTaskMap(manager).has(task.id)).toBe(false)
|
||||||
|
expect(manager.getTask(task.id)?.sessionId).toBe(task.sessionId)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("#given a running task #when cancelTask called with skipNotification=true #then concurrency slot is freed and pending tasks can start", async () => {
|
test("#given a running task #when cancelTask called with skipNotification=true #then concurrency slot is freed and pending tasks can start", async () => {
|
||||||
|
|||||||
@@ -5991,6 +5991,68 @@ describe("BackgroundManager regression fixes - resume and aborted notification",
|
|||||||
|
|
||||||
manager.shutdown()
|
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)
|
||||||
|
const archivedTask = manager.getTask(task.id)
|
||||||
|
expect(archivedTask?.sessionId).toBe(task.sessionId)
|
||||||
|
expect(archivedTask?.prompt).toBe("[redacted]")
|
||||||
|
expect(archivedTask?.startedAt).toEqual(task.startedAt)
|
||||||
|
|
||||||
|
manager.shutdown()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should cap completed task archive size at 100 entries", () => {
|
||||||
|
//#given
|
||||||
|
const manager = createBackgroundManager()
|
||||||
|
|
||||||
|
//#when
|
||||||
|
for (let index = 0; index < 120; index += 1) {
|
||||||
|
const task: BackgroundTask = {
|
||||||
|
id: `task-archive-${index}`,
|
||||||
|
sessionId: `session-archive-${index}`,
|
||||||
|
parentSessionId: "parent-session",
|
||||||
|
parentMessageId: "msg-1",
|
||||||
|
description: "archive cap regression",
|
||||||
|
prompt: `sensitive-${index}`,
|
||||||
|
agent: "explore",
|
||||||
|
status: "completed",
|
||||||
|
startedAt: new Date(),
|
||||||
|
completedAt: new Date(),
|
||||||
|
}
|
||||||
|
;(cast<{ removeTask: (task: BackgroundTask) => void }>(manager)).removeTask(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
//#then
|
||||||
|
const archive = cast<Map<string, unknown>>(Reflect.get(manager, "completedTaskArchive"))
|
||||||
|
expect(archive.size).toBe(100)
|
||||||
|
expect(archive.has("task-archive-0")).toBe(false)
|
||||||
|
expect(archive.has("task-archive-19")).toBe(false)
|
||||||
|
expect(archive.has("task-archive-20")).toBe(true)
|
||||||
|
expect(archive.has("task-archive-119")).toBe(true)
|
||||||
|
|
||||||
|
manager.shutdown()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("BackgroundManager - tool permission spread order", () => {
|
describe("BackgroundManager - tool permission spread order", () => {
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ export interface SubagentSessionCreatedEvent {
|
|||||||
export type OnSubagentSessionCreated = (event: SubagentSessionCreatedEvent) => Promise<void>
|
export type OnSubagentSessionCreated = (event: SubagentSessionCreatedEvent) => Promise<void>
|
||||||
|
|
||||||
const MAX_TASK_REMOVAL_RESCHEDULES = 6
|
const MAX_TASK_REMOVAL_RESCHEDULES = 6
|
||||||
|
const MAX_COMPLETED_TASK_ARCHIVE_SIZE = 100
|
||||||
|
|
||||||
export interface BackgroundManagerConfig {
|
export interface BackgroundManagerConfig {
|
||||||
pluginContext: PluginInput
|
pluginContext: PluginInput
|
||||||
@@ -222,6 +223,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 completedTaskArchive: Map<string, BackgroundTask> = new Map()
|
||||||
private completedTaskSummaries: Map<string, BackgroundTaskNotificationTask[]> = 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()
|
||||||
@@ -347,6 +349,7 @@ export class BackgroundManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private addTask(task: BackgroundTask): void {
|
private addTask(task: BackgroundTask): void {
|
||||||
|
this.completedTaskArchive.delete(task.id)
|
||||||
this.tasks.set(task.id, task)
|
this.tasks.set(task.id, task)
|
||||||
if (!task.parentSessionId) {
|
if (!task.parentSessionId) {
|
||||||
return
|
return
|
||||||
@@ -358,10 +361,47 @@ export class BackgroundManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private removeTask(task: BackgroundTask): void {
|
private removeTask(task: BackgroundTask): void {
|
||||||
|
this.archiveCompletedTask(task)
|
||||||
this.tasks.delete(task.id)
|
this.tasks.delete(task.id)
|
||||||
this.removeTaskFromParentIndex(task.id, task.parentSessionId)
|
this.removeTaskFromParentIndex(task.id, task.parentSessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private archiveCompletedTask(task: BackgroundTask): void {
|
||||||
|
if (!task.sessionId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (task.status === "running" || task.status === "pending") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const archivedTask: BackgroundTask = {
|
||||||
|
id: task.id,
|
||||||
|
parentSessionId: task.parentSessionId,
|
||||||
|
parentMessageId: task.parentMessageId,
|
||||||
|
description: task.description,
|
||||||
|
prompt: "[redacted]",
|
||||||
|
agent: task.agent,
|
||||||
|
sessionId: task.sessionId,
|
||||||
|
status: task.status,
|
||||||
|
queuedAt: task.queuedAt,
|
||||||
|
startedAt: task.startedAt,
|
||||||
|
completedAt: task.completedAt,
|
||||||
|
model: task.model,
|
||||||
|
error: task.error,
|
||||||
|
category: task.category,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.completedTaskArchive.set(task.id, archivedTask)
|
||||||
|
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 {
|
private updateTaskParent(task: BackgroundTask, parentSessionID: string): void {
|
||||||
if (task.parentSessionId === parentSessionID) {
|
if (task.parentSessionId === parentSessionID) {
|
||||||
return
|
return
|
||||||
@@ -830,7 +870,7 @@ The fallback retry session is now created and can be inspected directly.
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTask(id: string): BackgroundTask | undefined {
|
getTask(id: string): BackgroundTask | undefined {
|
||||||
return this.tasks.get(id)
|
return this.tasks.get(id) ?? this.completedTaskArchive.get(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
getTasksByParentSession(sessionID: string): BackgroundTask[] {
|
getTasksByParentSession(sessionID: string): BackgroundTask[] {
|
||||||
|
|||||||
Reference in New Issue
Block a user