myrlin-workbook 0.9.17 → 0.9.19

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
- 21156
1
+ 49156
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myrlin-workbook",
3
- "version": "0.9.17",
3
+ "version": "0.9.19",
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
@@ -191,10 +191,22 @@ if (!process.env.CWM_NO_OPEN) {
191
191
 
192
192
  // ─── Memory Watchdog ──────────────────────────────────────
193
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
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
201
+ const _rssLogger = setInterval(() => {
202
+ const mem = process.memoryUsage();
203
+ const ptyManager = getPtyManager();
204
+ const ptySessions = ptyManager ? ptyManager.listSessions().length : 0;
205
+ try {
206
+ console.log(`[RSS] ${Math.round(mem.rss / 1024 / 1024)}MB heap=${Math.round(mem.heapUsed / 1024 / 1024)}/${Math.round(mem.heapTotal / 1024 / 1024)}MB pty=${ptySessions}`);
207
+ } catch (_) {}
208
+ }, 60000);
209
+ _rssLogger.unref();
198
210
 
199
211
  const _memoryWatchdog = setInterval(() => {
200
212
  const rssMB = Math.round(process.memoryUsage().rss / 1024 / 1024);
@@ -220,8 +232,8 @@ const _memoryWatchdog = setInterval(() => {
220
232
  }
221
233
 
222
234
  // 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));
235
+ if (isCritical && killed === 0 && sessions.length > 1) {
236
+ const oldest = [...sessions].sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0));
225
237
  ptyManager.killSession(oldest[0].sessionId);
226
238
  killed++;
227
239
  }
package/src/supervisor.js CHANGED
@@ -33,25 +33,57 @@ if (process.argv.includes('--daemon')) {
33
33
  const logDir = path.join(__dirname, '..', 'logs');
34
34
  if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
35
35
  const logFile = path.join(logDir, 'server.log');
36
- const out = fs.openSync(logFile, 'a');
37
- const err = fs.openSync(logFile, 'a');
36
+ const pidFile = path.join(logDir, 'server.pid');
38
37
 
39
38
  // Strip --daemon from args so the child runs in foreground (supervised) mode
40
39
  const childArgs = process.argv.slice(2).filter(a => a !== '--daemon');
41
-
42
- const child = spawn(process.execPath, [__filename, ...childArgs], {
43
- stdio: ['ignore', out, err],
44
- detached: true,
45
- env: { ...process.env },
46
- });
47
-
48
- // Write PID file so other tools can find/stop the server
49
- const pidFile = path.join(logDir, 'server.pid');
50
- fs.writeFileSync(pidFile, String(child.pid), 'utf8');
51
-
52
- child.unref();
53
- console.log(`[supervisor] Daemonized server (PID ${child.pid}), logs at ${logFile}`);
54
- process.exit(0);
40
+ const nodeExe = process.execPath;
41
+ const scriptArgs = [__filename, ...childArgs].map(a => `"${a}"`).join(' ');
42
+
43
+ if (process.platform === 'win32') {
44
+ // On Windows, Node's detached:true still inherits the console session's
45
+ // Job Object. When the parent shell (Git Bash, cmd, Claude Code) exits,
46
+ // Windows kills the entire job group. Use cmd.exe /c start to create a
47
+ // process in a completely new console session, then redirect its output.
48
+ const { execSync } = require('child_process');
49
+ const cmd = `cmd.exe /c start /b "" "${nodeExe}" --max-old-space-size=1024 ${scriptArgs} >> "${logFile}" 2>&1`;
50
+ execSync(cmd, { stdio: 'ignore', windowsHide: true });
51
+
52
+ // The PID isn't directly available from start /b. Write a marker so we
53
+ // can find it via tasklist. Wait briefly for the process to appear.
54
+ setTimeout(() => {
55
+ try {
56
+ const { execSync: es } = require('child_process');
57
+ const psCmd = `powershell.exe -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*supervisor.js*' -and $_.CommandLine -notlike '*--daemon*' } | Select-Object -ExpandProperty ProcessId"`;
58
+ const out = es(psCmd, { encoding: 'utf8', timeout: 10000 });
59
+ const pids = out.trim().split('\n').map(l => l.trim()).filter(Boolean);
60
+ if (pids.length > 0) {
61
+ const pid = pids[pids.length - 1];
62
+ fs.writeFileSync(pidFile, pid, 'utf8');
63
+ console.log(`[supervisor] Daemonized server (PID ${pid}), logs at ${logFile}`);
64
+ } else {
65
+ console.log(`[supervisor] Daemonized server, logs at ${logFile}`);
66
+ }
67
+ } catch (_) {
68
+ console.log(`[supervisor] Daemonized server, logs at ${logFile}`);
69
+ }
70
+ process.exit(0);
71
+ }, 2000);
72
+ } else {
73
+ // Unix: standard detach with file descriptors
74
+ const out = fs.openSync(logFile, 'a');
75
+ const err = fs.openSync(logFile, 'a');
76
+ const child = spawn(nodeExe, ['--max-old-space-size=1024', __filename, ...childArgs], {
77
+ stdio: ['ignore', out, err],
78
+ detached: true,
79
+ env: { ...process.env },
80
+ });
81
+ fs.writeFileSync(pidFile, String(child.pid), 'utf8');
82
+ child.unref();
83
+ console.log(`[supervisor] Daemonized server (PID ${child.pid}), logs at ${logFile}`);
84
+ process.exit(0);
85
+ }
86
+ return; // Guard: don't fall through to supervisor logic while waiting
55
87
  }
56
88
 
57
89
  // ─── EPIPE Protection ────────────────────────────────────
@@ -111,7 +111,7 @@ class PtySession {
111
111
  // Maximum number of live PTY sessions. Each one is a ConPTY handle + a Claude
112
112
  // process (100-200MB each). Beyond this limit, new spawns are rejected to
113
113
  // prevent the OS from OOM-killing the server process tree.
114
- const MAX_PTY_SESSIONS = 10;
114
+ const MAX_PTY_SESSIONS = 5;
115
115
 
116
116
  class PtySessionManager {
117
117
  constructor() {