gm-plugkit 2.0.1633 → 2.0.1634

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.
@@ -44,9 +44,11 @@ dom=<css-selector>\n
44
44
 
45
45
  **`dom=<css-selector>\n` is the zero-boilerplate element probe.** Returns `{selector, match_count, elements:[{tag, text, attrs, visible, rect}]}` for up to 20 matches -- the fastest answer to "is this element there and what does it say." An invalid selector returns `result.error` (no crash). Composes with `url=`.
46
46
 
47
+ **One session per run -- reuse it, then close it.** A browser session is keyed by its spool `sessionId`; every dispatch carrying the SAME sessionId reuses the SAME chromium. A DIFFERENT sessionId opens its OWN chromium -- so a run that invents `probe`/`w2`/`w3`/... names leaks one browser per name. Pick one sessionId, use it for every dispatch, and end with `session close` so nothing is left open; the eval envelope carries a `multi_session_warning` the moment a second distinct session opens. The idle reaper (closes sessions unused past the idle window) and the OS-orphan reaper (kills managed chromiums no live session owns, sparing in-use ones and your own Chrome) are backstops for crashes, not a license to leave sessions open -- close yours.
48
+
47
49
  ## Envelope
48
50
 
49
- `{ok, stdout, stderr, exit_code, session_id?, navigation_requested, landed_on_blank?, hint?}`. `stdout` = stringified eval result; `stderr` = page errors + launch diagnostics; `exit_code` non-zero = the dispatch did not land -- read `stderr` and re-dispatch, never blind. `navigation_requested` reflects whether the dispatch carried a `url=`/bare-URL navigation; `landed_on_blank: true` with a `hint` means the expression ran against `about:blank` -- prefix `url=<target>` and re-dispatch.
51
+ `{ok, stdout, stderr, exit_code, session_id?, navigation_requested, landed_on_blank?, hint?, multi_session_warning?}`. `stdout` = stringified eval result; `stderr` = page errors + launch diagnostics; `exit_code` non-zero = the dispatch did not land -- read `stderr` and re-dispatch, never blind. `navigation_requested` reflects whether the dispatch carried a `url=`/bare-URL navigation; `landed_on_blank: true` with a `hint` means the expression ran against `about:blank` -- prefix `url=<target>` and re-dispatch.
50
52
 
51
53
  ## Headed by default
52
54
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1633",
3
+ "version": "2.0.1634",
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": {
@@ -873,6 +873,62 @@ function reapOrphanBrowserSessions(pw, cwd, claudeSessionId, reason) {
873
873
  }
874
874
  }
875
875
 
876
+ const __openedSessionIds = new Set();
877
+ function enumerateManagedChromiums(profileRootMarker) {
878
+ const marker = String(profileRootMarker || '').toLowerCase().replace(/\\/g, '/');
879
+ const out = [];
880
+ try {
881
+ if (process.platform === 'win32') {
882
+ const ps = `Get-CimInstance Win32_Process -Filter "Name='chrome.exe'" | Where-Object { $_.CommandLine -like '*--remote-debugging-port*' -and $_.CommandLine -like '*browser-profile*' -and $_.CommandLine -notlike '*--type=*' } | ForEach-Object { $_.ProcessId.ToString() + '|' + $_.CommandLine }`;
883
+ const r = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', ps], { encoding: 'utf-8', windowsHide: true, timeout: 10000 });
884
+ if (r.status === 0 && r.stdout) {
885
+ for (const line of r.stdout.split(/\r?\n/).filter(Boolean)) {
886
+ const bar = line.indexOf('|');
887
+ if (bar < 0) continue;
888
+ const pid = parseInt(line.slice(0, bar), 10);
889
+ const cmd = line.slice(bar + 1);
890
+ if (/--type=/.test(cmd)) continue;
891
+ if (Number.isFinite(pid) && cmd.toLowerCase().replace(/\\/g, '/').includes(marker)) out.push({ pid, cmd });
892
+ }
893
+ }
894
+ } else {
895
+ const r = spawnSync('ps', ['-eo', 'pid,command'], { encoding: 'utf-8', timeout: 10000 });
896
+ if (r.status === 0 && r.stdout) {
897
+ for (const line of r.stdout.split('\n').slice(1)) {
898
+ if (!/--remote-debugging-port/.test(line) || !/browser-profile/.test(line)) continue;
899
+ if (/--type=/.test(line)) continue;
900
+ const m = line.match(/^\s*(\d+)\s+(.+)$/);
901
+ if (!m) continue;
902
+ const pid = parseInt(m[1], 10);
903
+ if (Number.isFinite(pid) && m[2].toLowerCase().replace(/\\/g, '/').includes(marker)) out.push({ pid, cmd: m[2] });
904
+ }
905
+ }
906
+ }
907
+ } catch (_) {}
908
+ return out;
909
+ }
910
+ function reapOrphanChromiums(cwd, reason) {
911
+ try {
912
+ const root = browserRootDir(cwd);
913
+ const marker = path.join(root, '.gm', 'browser-profile').toLowerCase().replace(/\\/g, '/');
914
+ const procs = enumerateManagedChromiums(marker);
915
+ if (procs.length === 0) return { reaped: 0 };
916
+ const ports = readJsonFile(browserPortsFile(cwd), {});
917
+ const livePids = new Set();
918
+ for (const ent of Object.values(ports)) {
919
+ if (ent && Number.isFinite(ent.pid) && isProcessAliveSync(ent.pid)) livePids.add(ent.pid);
920
+ }
921
+ let reaped = 0;
922
+ for (const { pid } of procs) {
923
+ if (livePids.has(pid)) continue;
924
+ try { killPidQuiet(pid); reaped++; logEvent('plugkit', 'browser.os-orphan-reaped', { pid, reason: reason || 'sweep' }); } catch (_) {}
925
+ }
926
+ return { reaped };
927
+ } catch (_) {
928
+ return { reaped: 0 };
929
+ }
930
+ }
931
+
876
932
  function resolveWindowsExeLocal(cmd) {
877
933
  if (process.platform !== 'win32') return cmd;
878
934
  try {
@@ -1307,6 +1363,10 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
1307
1363
  sessions[claudeSessionId] = [pwSessionId];
1308
1364
  writeJsonFile(portsFile, ports);
1309
1365
  writeJsonFile(sessionsFile, sessions);
1366
+ if (!__openedSessionIds.has(claudeSessionId) && __openedSessionIds.size >= 1) {
1367
+ logEvent('hook', 'deviation.browser-multi-session', { sid: claudeSessionId, already_open: Array.from(__openedSessionIds), reason: 'a 2nd distinct browser sessionId launched its own chromium this run -- reuse one session per run and close it when done' });
1368
+ }
1369
+ __openedSessionIds.add(claudeSessionId);
1310
1370
  logEvent('plugkit', 'browser.attached', { pwSessionId, pid: browserPid, port });
1311
1371
  return pwSessionId;
1312
1372
  } finally { releaseSpawnLock(); }
@@ -2187,6 +2247,7 @@ function makeHostFunctions(instanceRef) {
2187
2247
  stderr: '',
2188
2248
  exit_code: 0,
2189
2249
  session_id: pwSessionId,
2250
+ hint: 'Reuse this same session for every browser dispatch this run (the spool sessionId selects it); a different sessionId opens its OWN chromium. Close it with `session close` when done -- the idle/orphan reaper is only a backstop.',
2190
2251
  });
2191
2252
  }
2192
2253
 
@@ -2376,6 +2437,9 @@ function makeHostFunctions(instanceRef) {
2376
2437
  };
2377
2438
  if (resultParsed) envelope.result = parsedResult;
2378
2439
  envelope.navigation_requested = !!startUrl;
2440
+ if (__openedSessionIds.size > 1) {
2441
+ envelope.multi_session_warning = `${__openedSessionIds.size} distinct browser sessions opened this run, each its own chromium -- reuse ONE sessionId per run and 'session close' it when done to avoid leaking browsers.`;
2442
+ }
2379
2443
  if (landedOnBlank) {
2380
2444
  envelope.landed_on_blank = true;
2381
2445
  envelope.hint = "page is about:blank: this dispatch did not navigate, so the expression evaluated against an empty page. Prefix the body with 'url=<target>' (or send a bare 'https://...' URL) to open the page you want before evaluating.";
@@ -2488,6 +2552,7 @@ async function runSpoolWatcher(instance, spoolDir) {
2488
2552
  } catch (_) {}
2489
2553
 
2490
2554
  try { reapOrphanBrowserSessions(findBrowserRunner(), process.cwd(), process.env.CLAUDE_SESSION_ID || 'claude-loop-iter', 'watcher-boot'); } catch (_) {}
2555
+ try { reapOrphanChromiums(process.cwd(), 'watcher-boot'); } catch (_) {}
2491
2556
 
2492
2557
 
2493
2558
  const LOCK_PATH = path.join(spoolDir, '.watcher.lock');
@@ -3124,7 +3189,7 @@ async function runSpoolWatcher(instance, spoolDir) {
3124
3189
  }
3125
3190
  }, 60_000);
3126
3191
 
3127
- const BROWSER_IDLE_LIMIT_MS = parseInt(process.env.PLUGKIT_BROWSER_IDLE_LIMIT_MS, 10) || 10 * 60 * 1000;
3192
+ const BROWSER_IDLE_LIMIT_MS = parseInt(process.env.PLUGKIT_BROWSER_IDLE_LIMIT_MS, 10) || 3 * 60 * 1000;
3128
3193
  setInterval(() => {
3129
3194
  try {
3130
3195
  const portsFile = browserPortsFile(process.cwd());
@@ -3168,6 +3233,7 @@ async function runSpoolWatcher(instance, spoolDir) {
3168
3233
  try { writeJsonFile(sessionsFile, sessions); } catch (_) {}
3169
3234
  }
3170
3235
  try { reapOrphanBrowserSessions(findBrowserRunner(), process.cwd(), process.env.CLAUDE_SESSION_ID || 'claude-loop-iter', 'idle-sweep'); } catch (_) {}
3236
+ try { reapOrphanChromiums(process.cwd(), 'idle-sweep'); } catch (_) {}
3171
3237
  } catch (e) {
3172
3238
  console.error(`[browser-idle] error: ${e.message}`);
3173
3239
  }