fix(background-agent): track and cancel completion timers to prevent memory leaks (#1058)

Track setTimeout timers in notifyParentSession using a completionTimers Map.
Clear all timers on shutdown() and when tasks are deleted via session.deleted.
This prevents the BackgroundManager instance from being held in memory by
uncancelled timer callbacks.

Fixes #1043

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
This commit is contained in:
Sisyphus
2026-01-31 16:26:01 +09:00
committed by GitHub
parent 8aa2549368
commit bb181ee572
2 changed files with 106 additions and 4 deletions
+14 -4
View File
@@ -83,6 +83,7 @@ export class BackgroundManager {
private queuesByKey: Map<string, QueueItem[]> = new Map()
private processingKeys: Set<string> = new Set()
private completionTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()
constructor(
ctx: PluginInput,
@@ -708,7 +709,11 @@ export class BackgroundManager {
this.concurrencyManager.release(task.concurrencyKey)
task.concurrencyKey = undefined
}
// Clean up pendingByParent to prevent stale entries
const existingTimer = this.completionTimers.get(task.id)
if (existingTimer) {
clearTimeout(existingTimer)
this.completionTimers.delete(task.id)
}
this.cleanupPendingByParent(task)
this.tasks.delete(task.id)
this.clearNotificationsForTask(task.id)
@@ -1073,14 +1078,15 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
}
const taskId = task.id
setTimeout(() => {
// Guard: Only delete if task still exists (could have been deleted by session.deleted event)
const timer = setTimeout(() => {
this.completionTimers.delete(taskId)
if (this.tasks.has(taskId)) {
this.clearNotificationsForTask(taskId)
this.tasks.delete(taskId)
log("[background-agent] Removed completed task from memory:", taskId)
}
}, 5 * 60 * 1000)
this.completionTimers.set(taskId, timer)
}
private formatDuration(start: Date, end?: Date): string {
@@ -1375,7 +1381,11 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
}
}
// Then clear all state (cancels any remaining waiters)
for (const timer of this.completionTimers.values()) {
clearTimeout(timer)
}
this.completionTimers.clear()
this.concurrencyManager.clear()
this.tasks.clear()
this.notifications.clear()