gm-skill 2.0.1634 → 2.0.1635

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.
@@ -46,6 +46,8 @@ dom=<css-selector>\n
46
46
 
47
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
48
 
49
+ **The session closes when YOU expect it to, not under you.** A session stays open across turns and think-gaps -- the idle window is generous (15 min of no use), measured from the END of your last dispatch, so a long read or a slow eval never shortens it. A dispatch in flight is never closed mid-run: the idle reaper and the orphan reaper both skip a session while its eval is executing, and a just-launched browser has a grace period before any reaper can touch it. An explicit `session close` is immediate. If the idle/orphan backstop did close a session and you dispatch to it again, it transparently re-launches and the envelope carries `session_relaunched: true` with a `relaunch_note` -- your in-page `window.*` state was reset, so re-establish it; you are told, never silently surprised.
50
+
49
51
  ## Envelope
50
52
 
51
53
  `{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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1634",
3
+ "version": "2.0.1635",
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": {
@@ -874,6 +874,39 @@ function reapOrphanBrowserSessions(pw, cwd, claudeSessionId, reason) {
874
874
  }
875
875
 
876
876
  const __openedSessionIds = new Set();
877
+ const __idleClosedSessions = new Set();
878
+ const __inflightDispatch = new Map();
879
+ const __launchingPids = new Map();
880
+ const INFLIGHT_MAX_MS = 130000;
881
+ const LAUNCH_GRACE_MS = 30000;
882
+ function markInflight(sessionId, pid) {
883
+ __inflightDispatch.set(sessionId, { pid: pid || null, ts: Date.now() });
884
+ }
885
+ function clearInflight(sessionId) {
886
+ __inflightDispatch.delete(sessionId);
887
+ }
888
+ function inflightPids() {
889
+ const now = Date.now();
890
+ const pids = new Set();
891
+ const sids = new Set();
892
+ for (const [sid, v] of __inflightDispatch) {
893
+ if (now - v.ts > INFLIGHT_MAX_MS) { __inflightDispatch.delete(sid); continue; }
894
+ sids.add(sid);
895
+ if (Number.isFinite(v.pid)) pids.add(v.pid);
896
+ }
897
+ return { pids, sids };
898
+ }
899
+ function markLaunching(pid) { if (Number.isFinite(pid)) __launchingPids.set(pid, Date.now()); }
900
+ function clearLaunching(pid) { __launchingPids.delete(pid); }
901
+ function launchingPidsFresh() {
902
+ const now = Date.now();
903
+ const pids = new Set();
904
+ for (const [pid, ts] of __launchingPids) {
905
+ if (now - ts > LAUNCH_GRACE_MS) { __launchingPids.delete(pid); continue; }
906
+ pids.add(pid);
907
+ }
908
+ return pids;
909
+ }
877
910
  function enumerateManagedChromiums(profileRootMarker) {
878
911
  const marker = String(profileRootMarker || '').toLowerCase().replace(/\\/g, '/');
879
912
  const out = [];
@@ -918,9 +951,11 @@ function reapOrphanChromiums(cwd, reason) {
918
951
  for (const ent of Object.values(ports)) {
919
952
  if (ent && Number.isFinite(ent.pid) && isProcessAliveSync(ent.pid)) livePids.add(ent.pid);
920
953
  }
954
+ const { pids: protectedInflight } = inflightPids();
955
+ const launching = launchingPidsFresh();
921
956
  let reaped = 0;
922
957
  for (const { pid } of procs) {
923
- if (livePids.has(pid)) continue;
958
+ if (livePids.has(pid) || protectedInflight.has(pid) || launching.has(pid)) continue;
924
959
  try { killPidQuiet(pid); reaped++; logEvent('plugkit', 'browser.os-orphan-reaped', { pid, reason: reason || 'sweep' }); } catch (_) {}
925
960
  }
926
961
  return { reaped };
@@ -1348,6 +1383,7 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
1348
1383
  logEvent('plugkit', 'browser.start', { profileDir });
1349
1384
  ({ pid: browserPid, port, wsEndpoint } = startManagedBrowser(pw, profileDir));
1350
1385
  }
1386
+ markLaunching(browserPid);
1351
1387
  const r = runBrowserRunner(pw, ['session', 'new', '--direct', wsEndpoint], 30000, cwd, claudeSessionId);
1352
1388
  if (!r || r.status !== 0) {
1353
1389
  const errTxt = scrubBrowserRunnerText((r && (r.stderr || r.stdout)) || 'unknown');
@@ -1363,6 +1399,7 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
1363
1399
  sessions[claudeSessionId] = [pwSessionId];
1364
1400
  writeJsonFile(portsFile, ports);
1365
1401
  writeJsonFile(sessionsFile, sessions);
1402
+ clearLaunching(browserPid);
1366
1403
  if (!__openedSessionIds.has(claudeSessionId) && __openedSessionIds.size >= 1) {
1367
1404
  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
1405
  }
@@ -2281,8 +2318,13 @@ function makeHostFunctions(instanceRef) {
2281
2318
  });
2282
2319
  }
2283
2320
 
2321
+ const wasIdleClosed = __idleClosedSessions.has(sessionId);
2284
2322
  const pwSessionId = getOrCreateBrowserSession(cwd, sessionId, pw);
2323
+ const curPid = (() => { try { const e = readJsonFile(browserPortsFile(cwd), {})[sessionId]; return e && e.pid; } catch (_) { return null; } })();
2324
+ const wasRelaunched = wasIdleClosed;
2325
+ __idleClosedSessions.delete(sessionId);
2285
2326
  stampBrowserLastUse(cwd, sessionId);
2327
+ markInflight(sessionId, curPid);
2286
2328
  let evalBody = body;
2287
2329
  let timeoutMs = 120000;
2288
2330
  const timeoutMatch = body.match(/^timeout=(\d+)\s*\n([\s\S]*)$/);
@@ -2409,7 +2451,13 @@ function makeHostFunctions(instanceRef) {
2409
2451
  evalBody = `const __RET=await (async()=>{${evalBody}})();\n` + emitResult + `return __RET;`;
2410
2452
  }
2411
2453
  const outerTimeoutMs = Math.min(timeoutMs + 6000, 126000);
2412
- const r = runBrowserRunner(pw, ['-s', pwSessionId, '--timeout', String(timeoutMs), '-e', evalBody], outerTimeoutMs, cwd, sessionId);
2454
+ let r;
2455
+ try {
2456
+ r = runBrowserRunner(pw, ['-s', pwSessionId, '--timeout', String(timeoutMs), '-e', evalBody], outerTimeoutMs, cwd, sessionId);
2457
+ } finally {
2458
+ clearInflight(sessionId);
2459
+ stampBrowserLastUse(cwd, sessionId);
2460
+ }
2413
2461
  const ok = r.status === 0;
2414
2462
  if (!ok && r.status === null) {
2415
2463
  logEvent('plugkit', 'browser.runner-timeout', { session_id: pwSessionId, timeout_ms: timeoutMs, body_bytes: evalBody.length });
@@ -2436,6 +2484,10 @@ function makeHostFunctions(instanceRef) {
2436
2484
  timeout_ms_used: timeoutMs,
2437
2485
  };
2438
2486
  if (resultParsed) envelope.result = parsedResult;
2487
+ if (wasRelaunched) {
2488
+ envelope.session_relaunched = true;
2489
+ envelope.relaunch_note = 'This session was closed (idle/orphan reaper) and re-launched fresh -- any window.* globals or in-page state from earlier dispatches are gone. Re-establish them before relying on them.';
2490
+ }
2439
2491
  envelope.navigation_requested = !!startUrl;
2440
2492
  if (__openedSessionIds.size > 1) {
2441
2493
  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.`;
@@ -2824,6 +2876,7 @@ async function runSpoolWatcher(instance, spoolDir) {
2824
2876
  }
2825
2877
  try { fs.unlinkSync(portsFile); } catch (_) {}
2826
2878
  try { fs.unlinkSync(sessionsFile); } catch (_) {}
2879
+ try { __inflightDispatch.clear(); __launchingPids.clear(); reapOrphanChromiums(process.cwd(), `teardown:${reason}`); } catch (_) {}
2827
2880
  } catch (_) {}
2828
2881
 
2829
2882
  try {
@@ -3189,7 +3242,7 @@ async function runSpoolWatcher(instance, spoolDir) {
3189
3242
  }
3190
3243
  }, 60_000);
3191
3244
 
3192
- const BROWSER_IDLE_LIMIT_MS = parseInt(process.env.PLUGKIT_BROWSER_IDLE_LIMIT_MS, 10) || 3 * 60 * 1000;
3245
+ const BROWSER_IDLE_LIMIT_MS = parseInt(process.env.PLUGKIT_BROWSER_IDLE_LIMIT_MS, 10) || 15 * 60 * 1000;
3193
3246
  setInterval(() => {
3194
3247
  try {
3195
3248
  const portsFile = browserPortsFile(process.cwd());
@@ -3197,13 +3250,15 @@ async function runSpoolWatcher(instance, spoolDir) {
3197
3250
  const ports = readJsonFile(portsFile, {});
3198
3251
  const sessions = readJsonFile(sessionsFile, {});
3199
3252
  const now = Date.now();
3200
- const idle = selectIdleBrowserSessions(ports, now, BROWSER_IDLE_LIMIT_MS);
3253
+ const { sids: inflightSids } = inflightPids();
3254
+ const idle = selectIdleBrowserSessions(ports, now, BROWSER_IDLE_LIMIT_MS).filter((x) => !inflightSids.has(x.sid));
3201
3255
  const idleSids = new Set(idle.map((x) => x.sid));
3202
3256
  let mutated = false;
3203
3257
  for (const { sid, entry, idleMs } of idle) {
3204
3258
  if (Number.isFinite(entry.pid) && isProcessAliveSync(entry.pid)) {
3205
3259
  try { gracefulCloseBrowser(entry, 'browser-idle'); } catch (_) {}
3206
3260
  }
3261
+ try { __idleClosedSessions.add(sid); } catch (_) {}
3207
3262
  delete ports[sid];
3208
3263
  delete sessions[sid];
3209
3264
  mutated = true;
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1634",
3
+ "version": "2.0.1635",
4
4
  "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-skill",
3
- "version": "2.0.1634",
3
+ "version": "2.0.1635",
4
4
  "description": "Canonical universal harness — AI-native software engineering via skill-driven orchestration; bootstraps plugkit for task execution and session isolation. Install in any AI coding agent host.",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",