Skip to content

[Bug] PlanOrchestrator leaks Session objects after runPrompt failure #20

@noahwaldner

Description

@noahwaldner

Severity: Fatal
Category: Memory Leak
Files: src/plan-orchestrator.ts

Description

Both runResearchAgent() and runPlannerAgent() create standalone Session objects and call runPrompt(). When runPrompt() throws, the catch block removes the session from runningSessions but never calls session.stop().

The Session class registers listener closures on its internal _taskTracker and _ralphTracker objects during construction (lines 437–461 of session.ts). These closures are only removed by cleanupTrackerListeners(), which is only ever called from session.stop(). Without stop(), the closures remain attached and no explicit resource cleanup runs — relying entirely on the garbage collector to eventually reclaim the session.

Reproduction

  1. Enable the Plan Orchestrator and trigger a plan generation.
  2. While Claude is running, cause runPrompt() to fail (e.g. kill the Claude CLI process, or simulate a network error).
  3. Repeat several times.
  4. Inspect memory usage — Session objects accumulate with each failed agent run.

Code

src/plan-orchestrator.ts, research agent catch block (~line 455):

} catch (err) {
  this.runningSessions.delete(session);
  // session.stop() is never called
  const durationMs = Date.now() - startTime;
  const error = err instanceof Error ? err.message : String(err);
  onSubagent?.({ type: 'failed', ... });
  return { success: false, ... };
} finally {
  clearInterval(progressInterval);
  // only the interval is cleaned up — not the session
}

The same pattern exists in runPlannerAgent() (~line 533).

Note: cancel() correctly calls session.stop() via its own loop, but the per-agent error paths do not.

Impact

Each failed plan agent run leaves a Session object whose cleanup is non-deterministic. Sessions hold terminal buffers up to 2MB each. During high-frequency plan generation with repeated failures, many of these can be in-flight simultaneously, causing heap pressure that delays GC cycles. The fragile reliance on GC also means a single future change to session initialization can silently turn a temporary pressure issue into a permanent leak.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions