myrlin-workbook 0.9.15 → 0.9.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/logs/server.pid CHANGED
@@ -1 +1 @@
1
- 65196
1
+ 7280
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myrlin-workbook",
3
- "version": "0.9.15",
3
+ "version": "0.9.16",
4
4
  "description": "Browser-based project manager for Claude Code sessions - session discovery, multi-terminal, cost tracking, docs, and kanban board",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/gui.js CHANGED
@@ -189,6 +189,52 @@ if (!process.env.CWM_NO_OPEN) {
189
189
  }
190
190
  }
191
191
 
192
+ // ─── Memory Watchdog ──────────────────────────────────────
193
+ // Monitor RSS and kill idle PTY sessions when memory exceeds threshold.
194
+ // This prevents the OS from silently OOM-killing the entire process tree.
195
+ const MEMORY_CHECK_INTERVAL = 30000; // Check every 30 seconds
196
+ const MEMORY_WARN_MB = 350; // Warn and kill idle PTYs
197
+ const MEMORY_CRITICAL_MB = 450; // Force-kill all clientless PTYs
198
+
199
+ const _memoryWatchdog = setInterval(() => {
200
+ const rssMB = Math.round(process.memoryUsage().rss / 1024 / 1024);
201
+ if (rssMB < MEMORY_WARN_MB) return;
202
+
203
+ const { logWarning } = require('./crash-logger');
204
+ const ptyManager = getPtyManager();
205
+ if (!ptyManager) return;
206
+
207
+ const sessions = ptyManager.listSessions();
208
+ const isCritical = rssMB >= MEMORY_CRITICAL_MB;
209
+ const label = isCritical ? 'CRITICAL' : 'WARNING';
210
+ try { console.log(`[Memory ${label}] RSS=${rssMB}MB, ${sessions.length} PTY sessions`); } catch (_) {}
211
+ logWarning('server', `Memory ${label}: RSS=${rssMB}MB, ${sessions.length} PTY sessions`);
212
+
213
+ // Kill PTY sessions with zero connected WebSocket clients first
214
+ let killed = 0;
215
+ for (const s of sessions) {
216
+ if (s.clientCount === 0) {
217
+ ptyManager.killSession(s.sessionId);
218
+ killed++;
219
+ }
220
+ }
221
+
222
+ // In critical mode, also kill sessions that have been alive longest
223
+ if (isCritical && killed === 0 && sessions.length > 2) {
224
+ const oldest = sessions.sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0));
225
+ ptyManager.killSession(oldest[0].sessionId);
226
+ killed++;
227
+ }
228
+
229
+ if (killed > 0) {
230
+ try { console.log(`[Memory] Killed ${killed} PTY session(s) to free memory`); } catch (_) {}
231
+ }
232
+
233
+ // Force GC if available (node --expose-gc)
234
+ if (global.gc) global.gc();
235
+ }, MEMORY_CHECK_INTERVAL);
236
+ _memoryWatchdog.unref();
237
+
192
238
  // ─── Graceful Shutdown ─────────────────────────────────────
193
239
 
194
240
  process.on('SIGINT', () => {
package/src/supervisor.js CHANGED
@@ -83,7 +83,7 @@ function startChild() {
83
83
  lastStartTime = Date.now();
84
84
  console.log(`[supervisor] Starting GUI server (attempt ${consecutiveRestarts + 1})...`);
85
85
 
86
- child = spawn(process.execPath, [guiScript, ...guiArgs], {
86
+ child = spawn(process.execPath, ['--max-old-space-size=512', guiScript, ...guiArgs], {
87
87
  stdio: 'inherit',
88
88
  env: { ...process.env, CWM_NO_OPEN: consecutiveRestarts > 0 ? '1' : '' },
89
89
  });
@@ -108,6 +108,11 @@ class PtySession {
108
108
  }
109
109
  }
110
110
 
111
+ // Maximum number of live PTY sessions. Each one is a ConPTY handle + a Claude
112
+ // process (100-200MB each). Beyond this limit, new spawns are rejected to
113
+ // prevent the OS from OOM-killing the server process tree.
114
+ const MAX_PTY_SESSIONS = 10;
115
+
111
116
  class PtySessionManager {
112
117
  constructor() {
113
118
  this.sessions = new Map(); // sessionId -> PtySession
@@ -132,6 +137,13 @@ class PtySessionManager {
132
137
  return existing;
133
138
  }
134
139
 
140
+ // Enforce max live sessions to prevent OOM from too many ConPTY handles
141
+ const aliveCount = [...this.sessions.values()].filter(s => s.alive).length;
142
+ if (aliveCount >= MAX_PTY_SESSIONS) {
143
+ console.log(`[PTY] Rejected spawn for ${sessionId}: ${aliveCount} live sessions (max ${MAX_PTY_SESSIONS})`);
144
+ return null;
145
+ }
146
+
135
147
  // ── Defense-in-depth: validate all user-controlled inputs ──
136
148
  // Primary validation happens at the API/WebSocket boundary (server.js, pty-server.js).
137
149
  // This is a secondary gate to catch any bypass or future code path that skips validation.