- Issue #514: Documented analysis of orphaned observer session files, including root cause, evidence, and recommended fixes. - Issue #517: Analyzed PowerShell escaping issues in cleanupOrphanedProcesses() on Windows, with recommended fixes using WMIC. - Issue #520: Confirmed resolution of stuck messages issue through architectural changes to a claim-and-delete pattern. - Issue #527: Identified detection failure of uv on Apple Silicon Macs with Homebrew installation, proposed path updates for detection. - Issue #532: Analyzed memory leak issues in SessionManager, detailing session cleanup and conversationHistory growth concerns, with recommended fixes.
11 KiB
Issue #532: Memory Leak in SessionManager - Analysis Report
Date: 2026-01-04 Issue: Memory leak causing 54GB+ VS Code memory consumption after several days of use Reported Root Causes:
- Sessions never auto-cleanup after SDK agent completes
conversationHistoryarray grows unbounded (never trimmed)
Executive Summary
This analysis confirms both issues exist in the current codebase (v8.5.7). While v8.5.7 included a major modular refactor, it did not address either memory leak issue. The SessionManager holds sessions indefinitely in memory with no TTL/cleanup mechanism, and conversationHistory arrays grow unbounded within each session (with only OpenRouter implementing partial mitigation).
1. SessionManager Session Storage Analysis
Location
/Users/alexnewman/Scripts/claude-mem/src/services/worker/SessionManager.ts
Current Implementation
export class SessionManager {
private sessions: Map<number, ActiveSession> = new Map();
private sessionQueues: Map<number, EventEmitter> = new Map();
// ...
}
Sessions are stored in an in-memory Map<number, ActiveSession> with the session database ID as the key.
Session Lifecycle
| Event | Method | Behavior |
|---|---|---|
| Session created | initializeSession() |
Added to this.sessions Map (line 152) |
| Session deleted | deleteSession() |
Removed from this.sessions Map (line 293) |
| Worker shutdown | shutdownAll() |
Calls deleteSession() on all sessions |
The Problem: No Automatic Cleanup
Looking at /Users/alexnewman/Scripts/claude-mem/src/services/worker/http/routes/SessionRoutes.ts (lines 213-216), the session completion handling has this comment:
// NOTE: We do NOT delete the session here anymore.
// The generator waits for events, so if it exited, it's either aborted or crashed.
// Idle sessions stay in memory (ActiveSession is small) to listen for future events.
Critical Finding: Sessions are intentionally never deleted after the SDK agent completes. They persist indefinitely "to listen for future events."
When Sessions ARE Deleted
Sessions are only deleted when:
- Explicit
DELETE /sessions/:sessionDbIdHTTP request (manual cleanup) POST /sessions/:sessionDbId/completeHTTP request (cleanup-hook callback)- Worker service shutdown (
shutdownAll())
There is NO automatic cleanup mechanism based on:
- Session age/TTL
- Session inactivity timeout
- Memory pressure
- Completed/failed status
2. conversationHistory Analysis
Location
/Users/alexnewman/Scripts/claude-mem/src/services/worker-types.ts (line 34)
Type Definition
export interface ActiveSession {
// ...
conversationHistory: ConversationMessage[]; // Shared conversation history for provider switching
// ...
}
Usage Pattern
The conversationHistory array is populated by three agent implementations:
-
SDKAgent (
/Users/alexnewman/Scripts/claude-mem/src/services/worker/SDKAgent.ts)- Adds user messages at lines 247, 280, 302
- Assistant responses added via
ResponseProcessor
-
GeminiAgent (
/Users/alexnewman/Scripts/claude-mem/src/services/worker/GeminiAgent.ts)- Adds user messages at lines 143, 196, 232
- Adds assistant responses at lines 148, 202, 238
-
OpenRouterAgent (
/Users/alexnewman/Scripts/claude-mem/src/services/worker/OpenRouterAgent.ts)- Adds user messages at lines 103, 155, 191
- Adds assistant responses at lines 108, 161, 197
- Implements truncation: See
truncateHistory()at lines 262-301
-
ResponseProcessor (
/Users/alexnewman/Scripts/claude-mem/src/services/worker/agents/ResponseProcessor.ts)- Adds assistant responses at line 57
The Problem: Unbounded Growth
For Claude SDK and Gemini agents, there is no limit or trimming of conversationHistory. Every message is push()ed without checking array size.
OpenRouter ONLY has mitigation via truncateHistory():
private truncateHistory(history: ConversationMessage[]): ConversationMessage[] {
const MAX_CONTEXT_MESSAGES = parseInt(settings.CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES) || 20;
const MAX_ESTIMATED_TOKENS = parseInt(settings.CLAUDE_MEM_OPENROUTER_MAX_TOKENS) || 100000;
// Sliding window: keep most recent messages within limits
// ...
}
However, this only truncates the copy sent to OpenRouter API - it does NOT truncate the actual session.conversationHistory array. The original array still grows unbounded.
Memory Impact Calculation
Each ConversationMessage contains:
role: 'user' | 'assistant' (small string)content: string (can be very large - full prompts/responses)
A typical session with 100 tool uses could have:
- 1 init prompt (~2KB)
- 100 observation prompts (~5KB each = 500KB)
- 100 responses (~1KB each = 100KB)
- 1 summary prompt + response (~5KB)
Per session: ~600KB in conversationHistory alone
After several days with many sessions, this adds up to gigabytes.
3. v8.5.7 Refactor Assessment
The v8.5.7 release (2026-01-04) focused on modular architecture refactoring:
What v8.5.7 DID:
- Extracted SQLite repositories into
/src/services/sqlite/ - Extracted worker agents into
/src/services/worker/agents/ - Extracted search strategies into
/src/services/worker/search/ - Extracted context generation into
/src/services/context/ - Extracted infrastructure into
/src/services/infrastructure/ - Added 595 tests across 36 test files
What v8.5.7 DID NOT address:
- No session TTL or automatic cleanup mechanism
- No
conversationHistorysize limits for Claude SDK or Gemini - No memory pressure monitoring for sessions
- The "sessions stay in memory" design comment was already present
Relevant v8.5.2 Note: There was a related fix for SDK Agent child process memory leak (orphaned Claude processes), but that addressed process cleanup, not in-memory session state.
4. Specific Code Locations Requiring Fixes
Fix Location 1: SessionManager needs cleanup mechanism
File: /Users/alexnewman/Scripts/claude-mem/src/services/worker/SessionManager.ts
Add automatic session cleanup based on:
- Session completion (when generator finishes and no pending work)
- Session age TTL (e.g., 1 hour after last activity)
- Memory pressure (configurable max sessions)
Fix Location 2: conversationHistory needs bounds
Files:
/Users/alexnewman/Scripts/claude-mem/src/services/worker/SDKAgent.ts/Users/alexnewman/Scripts/claude-mem/src/services/worker/GeminiAgent.ts/Users/alexnewman/Scripts/claude-mem/src/services/worker/agents/ResponseProcessor.ts
Apply sliding window truncation similar to OpenRouterAgent's approach, but mutate the original array.
Fix Location 3: Session cleanup on completion
File: /Users/alexnewman/Scripts/claude-mem/src/services/worker/http/routes/SessionRoutes.ts
Remove the design decision to keep idle sessions in memory. Add cleanup timer after generator completes.
5. Recommended Fixes
Fix 1: Add Session TTL and Cleanup Timer
// In SessionManager.ts
private readonly SESSION_TTL_MS = 60 * 60 * 1000; // 1 hour
private cleanupTimers: Map<number, NodeJS.Timeout> = new Map();
/**
* Schedule automatic cleanup for idle sessions
*/
scheduleSessionCleanup(sessionDbId: number): void {
// Clear existing timer if any
const existingTimer = this.cleanupTimers.get(sessionDbId);
if (existingTimer) {
clearTimeout(existingTimer);
}
// Schedule cleanup after TTL
const timer = setTimeout(() => {
const session = this.sessions.get(sessionDbId);
if (session && !session.generatorPromise) {
// Only delete if no active generator
this.deleteSession(sessionDbId);
logger.info('SESSION', 'Session auto-cleaned due to TTL', { sessionDbId });
}
}, this.SESSION_TTL_MS);
this.cleanupTimers.set(sessionDbId, timer);
}
/**
* Cancel cleanup timer (call when session receives new work)
*/
cancelSessionCleanup(sessionDbId: number): void {
const timer = this.cleanupTimers.get(sessionDbId);
if (timer) {
clearTimeout(timer);
this.cleanupTimers.delete(sessionDbId);
}
}
Fix 2: Add conversationHistory Bounds
// In src/services/worker/SessionManager.ts or new utility file
const MAX_CONVERSATION_HISTORY_LENGTH = 50; // Configurable
/**
* Trim conversation history to prevent unbounded growth
* Keeps the most recent messages
*/
export function trimConversationHistory(session: ActiveSession): void {
if (session.conversationHistory.length > MAX_CONVERSATION_HISTORY_LENGTH) {
const toRemove = session.conversationHistory.length - MAX_CONVERSATION_HISTORY_LENGTH;
session.conversationHistory.splice(0, toRemove);
logger.debug('SESSION', 'Trimmed conversation history', {
sessionDbId: session.sessionDbId,
removed: toRemove,
remaining: session.conversationHistory.length
});
}
}
Then call this after each message is added in SDKAgent, GeminiAgent, and ResponseProcessor.
Fix 3: Update SessionRoutes Generator Completion
// In SessionRoutes.ts, update the finally block (around line 164)
.finally(() => {
const sessionDbId = session.sessionDbId;
const wasAborted = session.abortController.signal.aborted;
if (wasAborted) {
logger.info('SESSION', `Generator aborted`, { sessionId: sessionDbId });
} else {
logger.info('SESSION', `Generator completed naturally`, { sessionId: sessionDbId });
}
session.generatorPromise = null;
session.currentProvider = null;
this.workerService.broadcastProcessingStatus();
// Check for pending work
const pendingStore = this.sessionManager.getPendingMessageStore();
const pendingCount = pendingStore.getPendingCount(sessionDbId);
if (pendingCount > 0 && !wasAborted) {
// Restart for pending work
// ... existing restart logic ...
} else {
// No pending work - schedule cleanup instead of keeping forever
this.sessionManager.scheduleSessionCleanup(sessionDbId);
}
});
6. Configuration Recommendations
Add these to settings.json defaults:
{
"CLAUDE_MEM_SESSION_TTL_MINUTES": 60,
"CLAUDE_MEM_MAX_CONVERSATION_HISTORY": 50,
"CLAUDE_MEM_MAX_ACTIVE_SESSIONS": 100
}
7. Testing Recommendations
Add tests for:
- Session cleanup after TTL expires
conversationHistorytrimming at various sizes- Memory monitoring under sustained load
- Cleanup timer cancellation on new work
Summary
| Issue | Status in v8.5.7 | Fix Required |
|---|---|---|
| Sessions never auto-cleanup | NOT FIXED | Yes - add TTL/cleanup mechanism |
| conversationHistory unbounded | NOT FIXED (except partial OpenRouter mitigation) | Yes - add trimming to all agents |
Both memory leaks are confirmed to exist in the current codebase and require the fixes outlined above.