fix: memory leaks, race conditions, and stability improvements

- SSE event listener cleanup: Track all listeners and remove on reconnect
  to prevent listener accumulation (60+ listeners per connect cycle)
- Floating window cleanup: Remove drag listeners, ResizeObservers on close
- Session creation mutex: Prevent race conditions from concurrent requests
- Screen kill verification: Confirm processes are dead after kill signals
- State store circuit breaker: Prevent cascading failures with backup/recovery
- PTY spawn error handling: Wrap all spawn calls with proper error emission
- SSE graceful shutdown: Notify clients before server stops
- Async file writes: Replace blocking writeFileSync in hot paths
- DocumentFragment optimization: Batch DOM updates for plan rendering
- Timer cleanup: Clear notification/countdown timers on SSE reconnect

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-27 20:24:00 +01:00
co-authored by Claude Opus 4.5
parent de45239fb7
commit e27b473e77
7 changed files with 1624 additions and 313 deletions
+95 -63
View File
@@ -888,15 +888,21 @@ export class Session extends EventEmitter {
}
// Attach to the screen session via PTY
this.ptyProcess = pty.spawn('screen', [
'-x', this._screenSession!.screenName
], {
name: 'xterm-256color',
cols: 120,
rows: 40,
cwd: this.workingDir,
env: { ...process.env, TERM: 'xterm-256color' },
});
try {
this.ptyProcess = pty.spawn('screen', [
'-x', this._screenSession!.screenName
], {
name: 'xterm-256color',
cols: 120,
rows: 40,
cwd: this.workingDir,
env: { ...process.env, TERM: 'xterm-256color' },
});
} catch (spawnErr) {
console.error('[Session] Failed to spawn PTY for screen attachment:', spawnErr);
this.emit('error', `Failed to attach to screen: ${spawnErr}`);
throw spawnErr;
}
// For NEW screens: wait for prompt to appear then clean buffer
// For RESTORED screens: don't do anything - client will fetch buffer on tab switch
@@ -941,23 +947,30 @@ export class Session extends EventEmitter {
// Fallback to direct PTY if screen is not used
if (!this.ptyProcess) {
this.ptyProcess = pty.spawn('claude', [
'--dangerously-skip-permissions'
], {
name: 'xterm-256color',
cols: 120,
rows: 40,
cwd: this.workingDir,
env: {
...process.env,
PATH: getAugmentedPath(),
TERM: 'xterm-256color',
// Inform Claude it's running within Claudeman (helps prevent self-termination)
CLAUDEMAN_SCREEN: '1',
CLAUDEMAN_SESSION_ID: this.id,
CLAUDEMAN_API_URL: process.env.CLAUDEMAN_API_URL || 'http://localhost:3000',
},
});
try {
this.ptyProcess = pty.spawn('claude', [
'--dangerously-skip-permissions'
], {
name: 'xterm-256color',
cols: 120,
rows: 40,
cwd: this.workingDir,
env: {
...process.env,
PATH: getAugmentedPath(),
TERM: 'xterm-256color',
// Inform Claude it's running within Claudeman (helps prevent self-termination)
CLAUDEMAN_SCREEN: '1',
CLAUDEMAN_SESSION_ID: this.id,
CLAUDEMAN_API_URL: process.env.CLAUDEMAN_API_URL || 'http://localhost:3000',
},
});
} catch (spawnErr) {
console.error('[Session] Failed to spawn Claude PTY:', spawnErr);
this._status = 'stopped';
this.emit('error', `Failed to start Claude: ${spawnErr}`);
throw new Error(`Failed to spawn Claude process: ${spawnErr}`);
}
}
this._pid = this.ptyProcess.pid;
@@ -1101,15 +1114,21 @@ export class Session extends EventEmitter {
}
// Attach to the screen session via PTY
this.ptyProcess = pty.spawn('screen', [
'-x', this._screenSession!.screenName
], {
name: 'xterm-256color',
cols: 120,
rows: 40,
cwd: this.workingDir,
env: { ...process.env, TERM: 'xterm-256color' },
});
try {
this.ptyProcess = pty.spawn('screen', [
'-x', this._screenSession!.screenName
], {
name: 'xterm-256color',
cols: 120,
rows: 40,
cwd: this.workingDir,
env: { ...process.env, TERM: 'xterm-256color' },
});
} catch (spawnErr) {
console.error('[Session] Failed to spawn PTY for shell screen attachment:', spawnErr);
this.emit('error', `Failed to attach to screen: ${spawnErr}`);
throw spawnErr;
}
// For NEW screens: clear by sending 'clear' command to the shell
// For RESTORED screens: don't clear - we want to see the existing output
@@ -1130,19 +1149,26 @@ export class Session extends EventEmitter {
// Fallback to direct PTY if screen is not used
if (!this.ptyProcess) {
this.ptyProcess = pty.spawn(shell, [], {
name: 'xterm-256color',
cols: 120,
rows: 40,
cwd: this.workingDir,
env: {
...process.env,
TERM: 'xterm-256color',
CLAUDEMAN_SCREEN: '1',
CLAUDEMAN_SESSION_ID: this.id,
CLAUDEMAN_API_URL: process.env.CLAUDEMAN_API_URL || 'http://localhost:3000',
},
});
try {
this.ptyProcess = pty.spawn(shell, [], {
name: 'xterm-256color',
cols: 120,
rows: 40,
cwd: this.workingDir,
env: {
...process.env,
TERM: 'xterm-256color',
CLAUDEMAN_SCREEN: '1',
CLAUDEMAN_SESSION_ID: this.id,
CLAUDEMAN_API_URL: process.env.CLAUDEMAN_API_URL || 'http://localhost:3000',
},
});
} catch (spawnErr) {
console.error('[Session] Failed to spawn shell PTY:', spawnErr);
this._status = 'stopped';
this.emit('error', `Failed to start shell: ${spawnErr}`);
throw new Error(`Failed to spawn shell process: ${spawnErr}`);
}
}
this._pid = this.ptyProcess.pid;
@@ -1251,21 +1277,27 @@ export class Session extends EventEmitter {
}
args.push(prompt);
this.ptyProcess = pty.spawn('claude', args, {
name: 'xterm-256color',
cols: 120,
rows: 40,
cwd: this.workingDir,
env: {
...process.env,
PATH: getAugmentedPath(),
TERM: 'xterm-256color',
// Inform Claude it's running within Claudeman
CLAUDEMAN_SCREEN: '1',
CLAUDEMAN_SESSION_ID: this.id,
CLAUDEMAN_API_URL: process.env.CLAUDEMAN_API_URL || 'http://localhost:3000',
},
});
try {
this.ptyProcess = pty.spawn('claude', args, {
name: 'xterm-256color',
cols: 120,
rows: 40,
cwd: this.workingDir,
env: {
...process.env,
PATH: getAugmentedPath(),
TERM: 'xterm-256color',
// Inform Claude it's running within Claudeman
CLAUDEMAN_SCREEN: '1',
CLAUDEMAN_SESSION_ID: this.id,
CLAUDEMAN_API_URL: process.env.CLAUDEMAN_API_URL || 'http://localhost:3000',
},
});
} catch (spawnErr) {
console.error('[Session] Failed to spawn Claude PTY for runPrompt:', spawnErr);
this.emit('error', `Failed to spawn Claude: ${spawnErr instanceof Error ? spawnErr.message : String(spawnErr)}`);
throw spawnErr;
}
this._pid = this.ptyProcess.pid;
console.log('[Session] PTY spawned with PID:', this._pid);