gm-plugkit 2.0.1503 → 2.0.1505

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1503",
3
+ "version": "2.0.1505",
4
4
  "description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform binary, verifies SHA256, and starts the spool watcher daemon. Includes plugkit-wasm-wrapper for WASM-based spool watching.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -563,6 +563,31 @@ function browserStateDir(cwd) {
563
563
  function browserPortsFile(cwd) { return path.join(browserStateDir(cwd), 'browser-ports.json'); }
564
564
  function browserSessionsFile(cwd) { return path.join(browserStateDir(cwd), 'browser-sessions.json'); }
565
565
 
566
+ function selectIdleBrowserSessions(ports, now, limitMs) {
567
+ const idle = [];
568
+ if (!ports || typeof ports !== 'object') return idle;
569
+ for (const [sid, entry] of Object.entries(ports)) {
570
+ if (!entry || typeof entry !== 'object') continue;
571
+ const lastUse = Number.isFinite(entry.lastUse) ? entry.lastUse : 0;
572
+ const idleMs = now - lastUse;
573
+ if (idleMs >= limitMs) idle.push({ sid, entry, idleMs });
574
+ }
575
+ return idle;
576
+ }
577
+
578
+ function stampBrowserLastUse(cwd, claudeSessionId) {
579
+ try {
580
+ const portsFile = browserPortsFile(cwd);
581
+ const ports = readJsonFile(portsFile, {});
582
+ const entry = ports[claudeSessionId];
583
+ if (entry && typeof entry === 'object') {
584
+ entry.lastUse = Date.now();
585
+ ports[claudeSessionId] = entry;
586
+ writeJsonFile(portsFile, ports);
587
+ }
588
+ } catch (_) {}
589
+ }
590
+
566
591
  function atomicWriteJson(filePath, obj) {
567
592
  const tmp = filePath + '.tmp.' + process.pid + '.' + Date.now() + '.' + Math.random().toString(36).slice(2, 8);
568
593
  fs.writeFileSync(tmp, JSON.stringify(obj, null, 2));
@@ -1020,6 +1045,7 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
1020
1045
  const sid = parseSessionId(r.stdout || '');
1021
1046
  if (sid) {
1022
1047
  existing.pwSessionId = sid;
1048
+ existing.lastUse = Date.now();
1023
1049
  ports[claudeSessionId] = existing;
1024
1050
  sessions[claudeSessionId] = [sid];
1025
1051
  writeJsonFile(portsFile, ports);
@@ -1092,7 +1118,7 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
1092
1118
  logEvent('plugkit', 'browser.launch-failed', { reason: 'session-id-unparseable', stdout: r.stdout });
1093
1119
  throw new Error(`could not parse managed browser session id from: ${scrubBrowserRunnerText(r.stdout || '')}`);
1094
1120
  }
1095
- ports[claudeSessionId] = { profileDir, pid: browserPid, port, wsEndpoint, pwSessionId };
1121
+ ports[claudeSessionId] = { profileDir, pid: browserPid, port, wsEndpoint, pwSessionId, lastUse: Date.now() };
1096
1122
  sessions[claudeSessionId] = [pwSessionId];
1097
1123
  writeJsonFile(portsFile, ports);
1098
1124
  writeJsonFile(sessionsFile, sessions);
@@ -1833,7 +1859,7 @@ function makeHostFunctions(instanceRef) {
1833
1859
 
1834
1860
  if (trimmed === 'session new' || trimmed === '') {
1835
1861
  const pwSessionId = getOrCreateBrowserSession(cwd, sessionId, pw);
1836
- try { lastBrowserActivityMs = Date.now(); } catch (_) {}
1862
+ stampBrowserLastUse(cwd, sessionId);
1837
1863
  return writeWasmJson(instanceRef.value, {
1838
1864
  ok: true,
1839
1865
  stdout: `Session ${pwSessionId} attached to locally-profiled chromium at ${path.join(cwd, '.gm', 'browser-profile')}`,
@@ -1845,12 +1871,26 @@ function makeHostFunctions(instanceRef) {
1845
1871
 
1846
1872
  if (trimmed.startsWith('session ')) {
1847
1873
  const parts = trimmed.slice(8).trim().split(/\s+/);
1848
- // playwriter's actual session subcommands are list|new|delete|reset.
1849
- // Map the legacy close/kill aliases (recognized here historically) to delete,
1850
- // and pass list/new/delete/reset through verbatim. Anything else is rejected
1851
- // by playwriter with its own usage text, which is the correct surface.
1852
1874
  if (parts[0] === 'close' || parts[0] === 'kill') parts[0] = 'delete';
1853
1875
  const r = runBrowserRunner(pw, ['session', ...parts], 30000, cwd, sessionId);
1876
+ if (r.status === 0 && (parts[0] === 'delete' || parts[0] === 'reset')) {
1877
+ try {
1878
+ const portsFile = browserPortsFile(cwd);
1879
+ const sessionsFile = browserSessionsFile(cwd);
1880
+ const ports = readJsonFile(portsFile, {});
1881
+ const sessions = readJsonFile(sessionsFile, {});
1882
+ const entry = ports[sessionId];
1883
+ if (entry && typeof entry === 'object') {
1884
+ if (Number.isFinite(entry.pid) && isProcessAliveSync(entry.pid)) {
1885
+ gracefulCloseBrowser(entry, `session-${parts[0]}`);
1886
+ }
1887
+ delete ports[sessionId];
1888
+ delete sessions[sessionId];
1889
+ writeJsonFile(portsFile, ports);
1890
+ writeJsonFile(sessionsFile, sessions);
1891
+ }
1892
+ } catch (_) {}
1893
+ }
1854
1894
  return writeWasmJson(instanceRef.value, {
1855
1895
  ok: r.status === 0,
1856
1896
  stdout: scrubBrowserRunnerText(r.stdout || ''),
@@ -1860,7 +1900,7 @@ function makeHostFunctions(instanceRef) {
1860
1900
  }
1861
1901
 
1862
1902
  const pwSessionId = getOrCreateBrowserSession(cwd, sessionId, pw);
1863
- try { lastBrowserActivityMs = Date.now(); } catch (_) {}
1903
+ stampBrowserLastUse(cwd, sessionId);
1864
1904
  let evalBody = body;
1865
1905
  let timeoutMs = 14000;
1866
1906
  const timeoutMatch = body.match(/^timeout=(\d+)\s*\n([\s\S]*)$/);
@@ -2538,27 +2578,49 @@ async function runSpoolWatcher(instance, spoolDir) {
2538
2578
  }
2539
2579
  }, 60_000);
2540
2580
 
2541
- const BROWSER_IDLE_LIMIT_MS = parseInt(process.env.PLUGKIT_BROWSER_IDLE_LIMIT_MS, 10) || 60 * 60 * 1000;
2542
- let lastBrowserActivityMs = Date.now();
2581
+ const BROWSER_IDLE_LIMIT_MS = parseInt(process.env.PLUGKIT_BROWSER_IDLE_LIMIT_MS, 10) || 10 * 60 * 1000;
2543
2582
  setInterval(() => {
2544
2583
  try {
2545
- const browserIdleMs = Date.now() - lastBrowserActivityMs;
2546
- if (browserIdleMs < BROWSER_IDLE_LIMIT_MS) return;
2547
2584
  const portsFile = browserPortsFile(process.cwd());
2548
2585
  const sessionsFile = browserSessionsFile(process.cwd());
2549
2586
  const ports = readJsonFile(portsFile, {});
2550
- let closed = 0;
2587
+ const sessions = readJsonFile(sessionsFile, {});
2588
+ const now = Date.now();
2589
+ const idle = selectIdleBrowserSessions(ports, now, BROWSER_IDLE_LIMIT_MS);
2590
+ const idleSids = new Set(idle.map((x) => x.sid));
2591
+ let mutated = false;
2592
+ for (const { sid, entry, idleMs } of idle) {
2593
+ if (Number.isFinite(entry.pid) && isProcessAliveSync(entry.pid)) {
2594
+ try { gracefulCloseBrowser(entry, 'browser-idle'); } catch (_) {}
2595
+ }
2596
+ delete ports[sid];
2597
+ delete sessions[sid];
2598
+ mutated = true;
2599
+ logEvent('plugkit', 'browser.idle-closed', { sid, pid: entry.pid || null, idle_ms: idleMs });
2600
+ }
2551
2601
  for (const [sid, entry] of Object.entries(ports)) {
2552
- if (entry && Number.isFinite(entry.pid) && isProcessAliveSync(entry.pid)) {
2553
- try { gracefulCloseBrowser(entry, 'browser-idle'); closed++; } catch (_) {}
2602
+ if (idleSids.has(sid) || !entry || typeof entry !== 'object') continue;
2603
+ const pidAlive = Number.isFinite(entry.pid) && isProcessAliveSync(entry.pid);
2604
+ if (!pidAlive) {
2605
+ delete ports[sid];
2606
+ delete sessions[sid];
2607
+ mutated = true;
2608
+ logEvent('plugkit', 'browser.stale-reclaimed', { sid, pid: entry.pid || null, reason: 'pid-dead' });
2609
+ continue;
2610
+ }
2611
+ const cdpOk = !!fetchJsonSync(`http://127.0.0.1:${entry.port}/json/version`, 1000);
2612
+ if (!cdpOk) {
2613
+ try { gracefulCloseBrowser(entry, 'orphan-cdp-dead'); } catch (_) {}
2614
+ delete ports[sid];
2615
+ delete sessions[sid];
2616
+ mutated = true;
2617
+ logEvent('plugkit', 'browser.stale-reclaimed', { sid, pid: entry.pid || null, reason: 'cdp-dead' });
2554
2618
  }
2555
2619
  }
2556
- if (closed > 0) {
2557
- try { fs.unlinkSync(portsFile); } catch (_) {}
2558
- try { fs.unlinkSync(sessionsFile); } catch (_) {}
2559
- logEvent('plugkit', 'browser.idle-closed', { count: closed, idle_ms: browserIdleMs });
2620
+ if (mutated) {
2621
+ try { writeJsonFile(portsFile, ports); } catch (_) {}
2622
+ try { writeJsonFile(sessionsFile, sessions); } catch (_) {}
2560
2623
  }
2561
- lastBrowserActivityMs = Date.now();
2562
2624
  } catch (e) {
2563
2625
  console.error(`[browser-idle] error: ${e.message}`);
2564
2626
  }