fix: improve stability with memory leak fixes and atomic writes

Session Manager:
- Store event handlers for proper cleanup on session stop
- Add resetSessionManager() for test isolation

State Store:
- Use atomic write pattern (temp file + rename) to prevent corruption
- Add tokenStats to AppState interface

Subagent Watcher:
- Clean up pendingToolCalls on agent completion
- Add cleanupStaleAgents() for 24h+ old agents
- Clear all state maps on stop() for clean restart

Web Server:
- Remove listeners from transcript watcher before stopping
- Clean up respawn timers when session is deleted
- Use cleanupSession() in spawn orchestrator for proper resource cleanup

Frontend:
- Close existing EventSource before reconnecting to prevent duplicates
- Close failed connection before scheduling reconnect

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-26 12:35:00 +01:00
co-authored by Claude Opus 4.5
parent 6e4564abd1
commit fe334c2ea6
6 changed files with 131 additions and 37 deletions
+10 -7
View File
@@ -1969,6 +1969,7 @@ export class WebServer extends EventEmitter {
private stopTranscriptWatcher(sessionId: string): void {
const watcher = this.transcriptWatchers.get(sessionId);
if (watcher) {
watcher.removeAllListeners(); // Prevent memory leaks from attached listeners
watcher.stop();
this.transcriptWatchers.delete(sessionId);
}
@@ -2247,6 +2248,12 @@ export class WebServer extends EventEmitter {
controller.removeAllListeners();
this.respawnControllers.delete(session.id);
}
// Also clean up the respawn timer to prevent orphaned timers
const timerInfo = this.respawnTimers.get(session.id);
if (timerInfo) {
clearTimeout(timerInfo.timer);
this.respawnTimers.delete(session.id);
}
} catch (err) {
console.error(`[Server] Error cleaning up respawn controller for ${session.id}:`, err);
}
@@ -2485,13 +2492,9 @@ export class WebServer extends EventEmitter {
return session ? session.totalCost : 0;
},
stopSession: async (sessionId: string) => {
const session = this.sessions.get(sessionId);
if (session) {
await session.stop();
this.sessions.delete(sessionId);
this.broadcast('session:deleted', { id: sessionId });
this.persistSessionState(session);
}
// Use cleanupSession to properly clean up all resources (respawn controllers,
// run summary trackers, file streams, Ralph state, etc.)
await this.cleanupSession(sessionId);
},
onSessionCompletion: (sessionId: string, handler: (phrase: string) => void) => {
const session = this.sessions.get(sessionId);