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
@@ -2087,3 +2087,95 @@ describe("BackgroundManager.shutdown session abort", () => {
})
})
describe("BackgroundManager.completionTimers - Memory Leak Fix", () => {
function getCompletionTimers(manager: BackgroundManager): Map<string, ReturnType<typeof setTimeout>> {
return (manager as unknown as { completionTimers: Map<string, ReturnType<typeof setTimeout>> }).completionTimers
}
function setCompletionTimer(manager: BackgroundManager, taskId: string): void {
const completionTimers = getCompletionTimers(manager)
const timer = setTimeout(() => {
completionTimers.delete(taskId)
}, 5 * 60 * 1000)
completionTimers.set(taskId, timer)
}
test("should have completionTimers Map initialized", () => {
// #given
const manager = createBackgroundManager()
// #when
const completionTimers = getCompletionTimers(manager)
// #then
expect(completionTimers).toBeDefined()
expect(completionTimers).toBeInstanceOf(Map)
expect(completionTimers.size).toBe(0)
manager.shutdown()
})
test("should clear all completion timers on shutdown", () => {
// #given
const manager = createBackgroundManager()
setCompletionTimer(manager, "task-1")
setCompletionTimer(manager, "task-2")
const completionTimers = getCompletionTimers(manager)
expect(completionTimers.size).toBe(2)
// #when
manager.shutdown()
// #then
expect(completionTimers.size).toBe(0)
})
test("should cancel timer when task is deleted via session.deleted", () => {
// #given
const manager = createBackgroundManager()
const task: BackgroundTask = {
id: "task-timer-4",
sessionID: "session-timer-4",
parentSessionID: "parent-session",
parentMessageID: "msg-1",
description: "Test task",
prompt: "test",
agent: "explore",
status: "completed",
startedAt: new Date(),
}
getTaskMap(manager).set(task.id, task)
setCompletionTimer(manager, task.id)
const completionTimers = getCompletionTimers(manager)
expect(completionTimers.size).toBe(1)
// #when
manager.handleEvent({
type: "session.deleted",
properties: {
info: { id: "session-timer-4" },
},
})
// #then
expect(completionTimers.has(task.id)).toBe(false)
manager.shutdown()
})
test("should not leak timers across multiple shutdown calls", () => {
// #given
const manager = createBackgroundManager()
setCompletionTimer(manager, "task-1")
// #when
manager.shutdown()
manager.shutdown()
// #then
const completionTimers = getCompletionTimers(manager)
expect(completionTimers.size).toBe(0)
})
})