myrlin-workbook 0.9.20 → 0.9.22

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
- 45572
1
+ 67796
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myrlin-workbook",
3
- "version": "0.9.20",
3
+ "version": "0.9.22",
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,15 +189,8 @@ 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
- // ConPTY on Windows allocates significant native memory (outside V8 heap)
195
- // so RSS can spike well beyond --max-old-space-size.
196
- const MEMORY_CHECK_INTERVAL = 15000; // Check every 15 seconds
197
- const MEMORY_WARN_MB = 200; // Start killing idle (zero-client) PTYs
198
- const MEMORY_CRITICAL_MB = 350; // Aggressively kill PTYs to survive
199
-
200
- // Periodic RSS logging so we can trace memory trajectory in server.log
192
+ // ─── RSS Logger ───────────────────────────────────────────
193
+ // Periodic RSS logging so we can trace memory in server.log
201
194
  const _rssLogger = setInterval(() => {
202
195
  const mem = process.memoryUsage();
203
196
  const ptyManager = getPtyManager();
@@ -208,45 +201,6 @@ const _rssLogger = setInterval(() => {
208
201
  }, 60000);
209
202
  _rssLogger.unref();
210
203
 
211
- const _memoryWatchdog = setInterval(() => {
212
- const rssMB = Math.round(process.memoryUsage().rss / 1024 / 1024);
213
- if (rssMB < MEMORY_WARN_MB) return;
214
-
215
- const { logWarning } = require('./crash-logger');
216
- const ptyManager = getPtyManager();
217
- if (!ptyManager) return;
218
-
219
- const sessions = ptyManager.listSessions();
220
- const isCritical = rssMB >= MEMORY_CRITICAL_MB;
221
- const label = isCritical ? 'CRITICAL' : 'WARNING';
222
- try { console.log(`[Memory ${label}] RSS=${rssMB}MB, ${sessions.length} PTY sessions`); } catch (_) {}
223
- logWarning('server', `Memory ${label}: RSS=${rssMB}MB, ${sessions.length} PTY sessions`);
224
-
225
- // Kill PTY sessions with zero connected WebSocket clients first
226
- let killed = 0;
227
- for (const s of sessions) {
228
- if (s.clientCount === 0) {
229
- ptyManager.killSession(s.sessionId);
230
- killed++;
231
- }
232
- }
233
-
234
- // In critical mode, also kill sessions that have been alive longest
235
- if (isCritical && killed === 0 && sessions.length > 1) {
236
- const oldest = [...sessions].sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0));
237
- ptyManager.killSession(oldest[0].sessionId);
238
- killed++;
239
- }
240
-
241
- if (killed > 0) {
242
- try { console.log(`[Memory] Killed ${killed} PTY session(s) to free memory`); } catch (_) {}
243
- }
244
-
245
- // Force GC if available (node --expose-gc)
246
- if (global.gc) global.gc();
247
- }, MEMORY_CHECK_INTERVAL);
248
- _memoryWatchdog.unref();
249
-
250
204
  // ─── Graceful Shutdown ─────────────────────────────────────
251
205
 
252
206
  process.on('SIGINT', () => {
package/src/supervisor.js CHANGED
@@ -46,7 +46,7 @@ if (process.argv.includes('--daemon')) {
46
46
  // Windows kills the entire job group. Use cmd.exe /c start to create a
47
47
  // process in a completely new console session, then redirect its output.
48
48
  const { execSync } = require('child_process');
49
- const cmd = `cmd.exe /c start /b "" "${nodeExe}" --max-old-space-size=1024 ${scriptArgs} >> "${logFile}" 2>&1`;
49
+ const cmd = `cmd.exe /c start /b "" "${nodeExe}" --max-old-space-size=4096 ${scriptArgs} >> "${logFile}" 2>&1`;
50
50
  execSync(cmd, { stdio: 'ignore', windowsHide: true });
51
51
 
52
52
  // The PID isn't directly available from start /b. Write a marker so we
@@ -73,7 +73,7 @@ if (process.argv.includes('--daemon')) {
73
73
  // Unix: standard detach with file descriptors
74
74
  const out = fs.openSync(logFile, 'a');
75
75
  const err = fs.openSync(logFile, 'a');
76
- const child = spawn(nodeExe, ['--max-old-space-size=1024', __filename, ...childArgs], {
76
+ const child = spawn(nodeExe, ['--max-old-space-size=4096', __filename, ...childArgs], {
77
77
  stdio: ['ignore', out, err],
78
78
  detached: true,
79
79
  env: { ...process.env },
@@ -115,7 +115,7 @@ function startChild() {
115
115
  lastStartTime = Date.now();
116
116
  console.log(`[supervisor] Starting GUI server (attempt ${consecutiveRestarts + 1})...`);
117
117
 
118
- child = spawn(process.execPath, ['--max-old-space-size=1024', guiScript, ...guiArgs], {
118
+ child = spawn(process.execPath, ['--max-old-space-size=4096', guiScript, ...guiArgs], {
119
119
  stdio: 'inherit',
120
120
  env: { ...process.env, CWM_NO_OPEN: consecutiveRestarts > 0 ? '1' : '' },
121
121
  });
@@ -108,11 +108,6 @@ 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 = 5;
115
-
116
111
  class PtySessionManager {
117
112
  constructor() {
118
113
  this.sessions = new Map(); // sessionId -> PtySession
@@ -137,13 +132,6 @@ class PtySessionManager {
137
132
  return existing;
138
133
  }
139
134
 
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
-
147
135
  // ── Defense-in-depth: validate all user-controlled inputs ──
148
136
  // Primary validation happens at the API/WebSocket boundary (server.js, pty-server.js).
149
137
  // This is a secondary gate to catch any bypass or future code path that skips validation.
@@ -9058,6 +9058,36 @@ class CWMApp {
9058
9058
  TERMINAL GRID VIEW
9059
9059
  ═══════════════════════════════════════════════════════════ */
9060
9060
 
9061
+ /**
9062
+ * Show a disconnected placeholder in a terminal pane slot.
9063
+ * Preserves session info in terminalPanes[] for layout saves.
9064
+ * Click reconnects via openTerminalInPane().
9065
+ */
9066
+ _showDisconnectedPlaceholder(slotIdx, sessionId, sessionName, spawnOpts) {
9067
+ // Store placeholder so saveCurrentGroupPanes preserves the mapping
9068
+ this.terminalPanes[slotIdx] = { sessionId, sessionName, spawnOpts: spawnOpts || {}, _disconnected: true };
9069
+ const paneEl = document.getElementById(`term-pane-${slotIdx}`);
9070
+ if (!paneEl) return;
9071
+ paneEl.hidden = false;
9072
+ paneEl.classList.remove('terminal-pane-empty');
9073
+ const titleEl = paneEl.querySelector('.terminal-pane-title');
9074
+ if (titleEl) titleEl.textContent = sessionName || sessionId;
9075
+ const closeBtn = paneEl.querySelector('.terminal-pane-close');
9076
+ if (closeBtn) closeBtn.hidden = false;
9077
+ const container = document.getElementById(`term-container-${slotIdx}`);
9078
+ if (container) {
9079
+ container.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:8px;color:var(--overlay0);font-size:13px;cursor:pointer;" class="reconnect-prompt">
9080
+ <span style="font-size:20px;">&#x1F50C;</span>
9081
+ <span>${this.escapeHtml(sessionName || sessionId)}</span>
9082
+ <span style="font-size:11px;opacity:0.7;">Click to connect</span>
9083
+ </div>`;
9084
+ container.querySelector('.reconnect-prompt').addEventListener('click', () => {
9085
+ this.openTerminalInPane(slotIdx, sessionId, sessionName, spawnOpts);
9086
+ });
9087
+ }
9088
+ this.updateTerminalGridLayout();
9089
+ }
9090
+
9061
9091
  openTerminalInPane(slotIdx, sessionId, sessionName, spawnOpts) {
9062
9092
  // Check localStorage for a previously saved name for this session
9063
9093
  const savedTitle = this.getProjectSessionTitle(sessionId);
@@ -12048,10 +12078,12 @@ class CWMApp {
12048
12078
  const uploadBtn = paneEl.querySelector('.terminal-pane-upload');
12049
12079
  if (uploadBtn) uploadBtn.hidden = false;
12050
12080
  }
12051
- // Reattach xterm DOM
12081
+ // Reattach xterm DOM, or re-render placeholder for disconnected panes
12052
12082
  if (cached.domFragments[i]) {
12053
12083
  const termContainer = document.getElementById(`term-container-${i}`);
12054
12084
  if (termContainer) termContainer.appendChild(cached.domFragments[i]);
12085
+ } else if (cached.panes[i]._disconnected) {
12086
+ this._showDisconnectedPlaceholder(i, cached.panes[i].sessionId, cached.panes[i].sessionName, cached.panes[i].spawnOpts);
12055
12087
  }
12056
12088
  }
12057
12089
  }
@@ -12071,12 +12103,15 @@ class CWMApp {
12071
12103
  }
12072
12104
  });
12073
12105
  } else {
12074
- // No cache, create fresh connections (first time opening this group)
12106
+ // No cache: show disconnected placeholders instead of spawning all PTYs.
12107
+ // Each PTY spawns a Claude process (~150MB), so eagerly connecting all
12108
+ // panes across all tab groups would consume gigabytes of system memory.
12109
+ // Users click a pane to connect on demand.
12075
12110
  const group = this._tabGroups.find(g => g.id === groupId);
12076
12111
  if (group && group.panes) {
12077
12112
  group.panes.forEach(p => {
12078
12113
  if (p.sessionId && !this.terminalPanes[p.slot]) {
12079
- this.openTerminalInPane(p.slot, p.sessionId, p.sessionName || 'Terminal', p.spawnOpts || {});
12114
+ this._showDisconnectedPlaceholder(p.slot, p.sessionId, p.sessionName || 'Terminal', p.spawnOpts || {});
12080
12115
  }
12081
12116
  });
12082
12117
  }