Files
oh-my-opencode/src/features/background-agent/task-history.ts
T
YeonGyu-Kim 30323d025c fix(background-agent): fix 3 memory leaks in task lifecycle management
H3: cancelTask(skipNotification=true) now schedules task removal.
Previously the early return path skipped cleanup, leaking task objects
in this.tasks Map permanently. Extracted scheduleTaskRemoval() helper
called from both skipNotification and normal paths.

H2: Per-task completion cleanup timer decoupled from allComplete check.
Previously cleanup timer only ran when ALL sibling tasks completed. Now
each finished task gets its own removal timer regardless of siblings.

H1+C2: TaskHistory.clearAll() added and wired into shutdown(). Added
clearSession() calls on session error/deletion and prune cycles.
taskHistory was the only data structure missed by shutdown().

Tests: 10 pass (3 cancel + 3 completion + 4 history)
2026-03-12 01:37:03 +09:00

80 lines
2.3 KiB
TypeScript

import type { BackgroundTaskStatus } from "./types"
const MAX_ENTRIES_PER_PARENT = 100
export interface TaskHistoryEntry {
id: string
sessionID?: string
agent: string
description: string
status: BackgroundTaskStatus
category?: string
startedAt?: Date
completedAt?: Date
}
export class TaskHistory {
private entries: Map<string, TaskHistoryEntry[]> = new Map()
record(parentSessionID: string | undefined, entry: TaskHistoryEntry): void {
if (!parentSessionID) return
const list = this.entries.get(parentSessionID) ?? []
const existing = list.findIndex((e) => e.id === entry.id)
if (existing !== -1) {
const current = list[existing]
list[existing] = {
...current,
...(entry.sessionID !== undefined ? { sessionID: entry.sessionID } : {}),
...(entry.agent !== undefined ? { agent: entry.agent } : {}),
...(entry.description !== undefined ? { description: entry.description } : {}),
...(entry.status !== undefined ? { status: entry.status } : {}),
...(entry.category !== undefined ? { category: entry.category } : {}),
...(entry.startedAt !== undefined ? { startedAt: entry.startedAt } : {}),
...(entry.completedAt !== undefined ? { completedAt: entry.completedAt } : {}),
}
} else {
if (list.length >= MAX_ENTRIES_PER_PARENT) {
list.shift()
}
list.push({ ...entry })
}
this.entries.set(parentSessionID, list)
}
getByParentSession(parentSessionID: string): TaskHistoryEntry[] {
const list = this.entries.get(parentSessionID)
if (!list) return []
return list.map((e) => ({ ...e }))
}
clearSession(parentSessionID: string): void {
this.entries.delete(parentSessionID)
}
clearAll(): void {
this.entries.clear()
}
formatForCompaction(parentSessionID: string): string | null {
const list = this.getByParentSession(parentSessionID)
if (list.length === 0) return null
const lines = list.map((e) => {
const desc = e.description?.replace(/[\n\r]+/g, " ").trim() ?? ""
const parts = [
`- **${e.agent}**`,
e.category ? `[${e.category}]` : null,
`(${e.status})`,
`: ${desc}`,
e.sessionID ? ` | session: \`${e.sessionID}\`` : null,
]
return parts.filter(Boolean).join("")
})
return lines.join("\n")
}
}