Skip to content

Commit ec554fc

Browse files
committed
fix(background-agent): prevent memory leaks on task completion
- Release concurrency key immediately in session.idle handler - Clean up subagentSessions Set on normal completion - Clean up sessionAgentMap on both completion paths - Add documentation for validateSessionHasOutput usage
1 parent d64af50 commit ec554fc

File tree

1 file changed

+28
-13
lines changed

1 file changed

+28
-13
lines changed

src/features/background-agent/manager.ts

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { log } from "../../shared/logger"
99
import { ConcurrencyManager } from "./concurrency"
1010
import type { BackgroundTaskConfig } from "../../config/schema"
1111

12-
import { subagentSessions } from "../claude-code-session-state"
12+
import { subagentSessions, clearSessionAgent } from "../claude-code-session-state"
1313
import { getTaskToastManager } from "../task-toast-manager"
1414

1515
const TASK_TTL_MS = 30 * 60 * 1000
@@ -468,6 +468,17 @@ export class BackgroundManager {
468468
}
469469
task.status = "completed"
470470
task.completedAt = new Date()
471+
472+
// Release concurrency key immediately on completion to free up slots
473+
if (task.concurrencyKey) {
474+
this.concurrencyManager.release(task.concurrencyKey)
475+
task.concurrencyKey = undefined // Prevent double-release
476+
}
477+
478+
// Clean up session tracking to prevent memory leaks
479+
subagentSessions.delete(sessionID)
480+
clearSessionAgent(sessionID)
481+
471482
this.markForNotification(task)
472483
this.notifyParentSession(task).catch(err => {
473484
log("[background-agent] Error notifying parent on completion:", err)
@@ -496,6 +507,7 @@ export class BackgroundManager {
496507
this.tasks.delete(task.id)
497508
this.clearNotificationsForTask(task.id)
498509
subagentSessions.delete(sessionID)
510+
clearSessionAgent(sessionID)
499511
}
500512
}
501513

@@ -516,6 +528,9 @@ export class BackgroundManager {
516528
/**
517529
* Validates that a session has actual assistant/tool output before marking complete.
518530
* Prevents premature completion when session.idle fires before agent responds.
531+
*
532+
* NOTE: This is used in pollRunningTasks() but NOT in session.idle handler.
533+
* Using it in session.idle was causing stuck tasks due to timing issues.
519534
*/
520535
private async validateSessionHasOutput(sessionID: string): Promise<boolean> {
521536
try {
@@ -546,18 +561,18 @@ export class BackgroundManager {
546561
const hasContent = messages.some((m: any) => {
547562
if (m.info?.role !== "assistant" && m.info?.role !== "tool") return false
548563
const parts = m.parts ?? []
549-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
550-
return parts.some((p: any) =>
551-
// Text content (final output)
552-
(p.type === "text" && p.text && p.text.trim().length > 0) ||
553-
// Reasoning content (thinking blocks)
554-
(p.type === "reasoning" && p.text && p.text.trim().length > 0) ||
555-
// Tool calls (indicates work was done)
556-
p.type === "tool" ||
557-
// Tool results (output from executed tools) - important for tool-only tasks
558-
(p.type === "tool_result" && p.content &&
559-
(typeof p.content === "string" ? p.content.trim().length > 0 : p.content.length > 0))
560-
)
564+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
565+
return parts.some((p: any) =>
566+
// Text content (final output)
567+
(p.type === "text" && p.text && p.text.trim().length > 0) ||
568+
// Reasoning content (thinking blocks)
569+
(p.type === "reasoning" && p.text && p.text.trim().length > 0) ||
570+
// Tool calls (indicates work was done)
571+
p.type === "tool" ||
572+
// Tool results (output from executed tools) - important for tool-only tasks
573+
(p.type === "tool_result" && p.content &&
574+
(typeof p.content === "string" ? p.content.trim().length > 0 : p.content.length > 0))
575+
)
561576
})
562577

563578
if (!hasContent) {

0 commit comments

Comments
 (0)