gm-plugkit 2.0.1634 → 2.0.1636

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/SKILL.md CHANGED
@@ -34,7 +34,7 @@ Every turn: dispatch `instruction`, read it, follow the imperative, dispatch the
34
34
  cat .gm/exec-spool/.status.json 2>/dev/null; echo ---; cat .gm/exec-spool/.turn-summary.json 2>/dev/null; echo ---; date +%s%3N
35
35
  ```
36
36
 
37
- `.turn-summary.json` carries `phase`, `last_skill`, `prd_pending`, `last_instruction_ts`, `last_instruction_age_ms`, `long_gap_threshold_ms`, `browser_sessions_alive`, `update_available`, `deviations_30m`, `watcher_uptime_ms`. Age over threshold: your next non-orienting verb is gated, dispatch `instruction` first. `update_available` non-null: eager-upgrade with `bun x gm-plugkit@latest --kill-stale-watchers; bun x gm-plugkit@latest spool` (the spool call blocks until serving), then read `.status.json` to confirm `version`. `deviations_30m` non-zero indicates active drift to investigate before continuing.
37
+ `.turn-summary.json` carries `phase`, `last_skill`, `prd_pending`, `last_instruction_ts`, `last_instruction_age_ms`, `long_gap_threshold_ms`, `browser_sessions_alive`, `update_available`, `deviations_30m`, `watcher_uptime_ms`. Age over threshold: your next non-orienting verb is gated, dispatch `instruction` first. `update_available` non-null: the watcher auto-updates itself when idle (cache-busted self-respawn to latest), so it usually clears on its own within a few minutes -- keep working. To land it immediately, just re-run the idempotent `bun x gm-plugkit@latest spool` (blocks until serving); only add `--kill-stale-watchers` first if it stays stuck across several turns. `PLUGKIT_NO_AUTO_UPDATE=1` pins the version. `deviations_30m` non-zero indicates active drift to investigate before continuing.
38
38
 
39
39
  Compare `.status.json` `ts` to the printed epoch: gap > 15000 = dead, boot it. Exception: a future `busy_until` means a long verb (browser/chromium spawn blocks the heartbeat ~15-18s) -- wait, do not boot a second watcher.
40
40
 
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1634",
3
+ "version": "2.0.1636",
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": {
@@ -308,7 +308,7 @@ function injectUpdateWarning(parsed) {
308
308
  if (!upd || !upd.installed || !upd.latest || upd.installed === upd.latest) return;
309
309
  const target = (parsed.data && typeof parsed.data === 'object') ? parsed.data : parsed;
310
310
  target.update_available = { installed: upd.installed, latest: upd.latest, update_url: upd.update_url || null };
311
- target.update_warning = `STALE RUNTIME: running plugkit ${upd.installed} but ${upd.latest} is published and not yet running. Restart onto the new version now: bun x gm-plugkit@latest --kill-stale-watchers; bun x gm-plugkit@latest spool. This warning repeats every turn until the running version catches up.`;
311
+ target.update_warning = `STALE RUNTIME: running plugkit ${upd.installed} but ${upd.latest} is published. The watcher auto-updates when idle (cache-busted self-respawn to latest), so this usually clears on its own within a few minutes; just keep working. If it persists, re-run the idempotent boot to land latest now: bun x gm-plugkit@latest spool (add --kill-stale-watchers first only if it stays stuck). Set PLUGKIT_NO_AUTO_UPDATE=1 to pin. This warning repeats until the running version catches up.`;
312
312
  }
313
313
 
314
314
  function mergeAutoRecallIntoInstructionResponse(resultStr, autoRecall) {
@@ -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 {
@@ -2851,6 +2904,10 @@ async function runSpoolWatcher(instance, spoolDir) {
2851
2904
  let _selfStaleProbeErrorLogged = false;
2852
2905
  function probeGmPlugkitSelfStale() {
2853
2906
  try {
2907
+ if (process.env.PLUGKIT_NO_AUTO_UPDATE === '1') return;
2908
+ const { sids: _ifSids } = (typeof inflightPids === 'function') ? inflightPids() : { sids: new Set() };
2909
+ if (_ifSids.size > 0) return;
2910
+ if ((Date.now() - lastActivityMs) < 30000) return;
2854
2911
  const ownPkgVersionFile = path.join(GM_TOOLS_ROOT, 'gm-plugkit.version');
2855
2912
  const ownPkgJsonFile = path.join(__dirname, 'package.json');
2856
2913
  let own = null;
@@ -2922,7 +2979,7 @@ async function runSpoolWatcher(instance, spoolDir) {
2922
2979
  try {
2923
2980
  const cp = _childProcess;
2924
2981
  const bunPath = process.env.GM_BUN_PATH || 'bun';
2925
- const bustCache = sameStaleAsBefore || cameFromSelfRespawn;
2982
+ const bustCache = true;
2926
2983
  if (bustCache) {
2927
2984
  try { cp.execFileSync(bunPath, ['pm', 'cache', 'rm'], { stdio: 'ignore', timeout: 30000, windowsHide: true }); } catch (_) {}
2928
2985
  try {
@@ -2949,7 +3006,7 @@ async function runSpoolWatcher(instance, spoolDir) {
2949
3006
  env: { ...process.env, PLUGKIT_BOOT_REASON: 'self-respawn-from-self-stale' },
2950
3007
  });
2951
3008
  child.unref();
2952
- try { logEvent('plugkit', 'gm-plugkit.self-stale-respawn', { running_version: own, latest_version: latest, cache_busted: bustCache, attempt: (respawnGuard.attempts || 0) + 1 }); } catch (_) {}
3009
+ try { logEvent('plugkit', 'update.auto-applying', { running_version: own, latest_version: latest, cache_busted: bustCache, attempt: (respawnGuard.attempts || 0) + 1, note: 'auto-update: cache-busted self-respawn to latest' }); } catch (_) {}
2953
3010
  try { fs.writeFileSync(path.join(spoolDir, '.shutdown-reason.json'), JSON.stringify({ reason: 'gm-plugkit-self-stale', ts: Date.now(), pid: process.pid, running_version: own, latest_version: latest })); } catch (_) {}
2954
3011
  const myPid = process.pid;
2955
3012
  const respawnDeadline = Date.now() + 90000;
@@ -3189,7 +3246,7 @@ async function runSpoolWatcher(instance, spoolDir) {
3189
3246
  }
3190
3247
  }, 60_000);
3191
3248
 
3192
- const BROWSER_IDLE_LIMIT_MS = parseInt(process.env.PLUGKIT_BROWSER_IDLE_LIMIT_MS, 10) || 3 * 60 * 1000;
3249
+ const BROWSER_IDLE_LIMIT_MS = parseInt(process.env.PLUGKIT_BROWSER_IDLE_LIMIT_MS, 10) || 15 * 60 * 1000;
3193
3250
  setInterval(() => {
3194
3251
  try {
3195
3252
  const portsFile = browserPortsFile(process.cwd());
@@ -3197,13 +3254,15 @@ async function runSpoolWatcher(instance, spoolDir) {
3197
3254
  const ports = readJsonFile(portsFile, {});
3198
3255
  const sessions = readJsonFile(sessionsFile, {});
3199
3256
  const now = Date.now();
3200
- const idle = selectIdleBrowserSessions(ports, now, BROWSER_IDLE_LIMIT_MS);
3257
+ const { sids: inflightSids } = inflightPids();
3258
+ const idle = selectIdleBrowserSessions(ports, now, BROWSER_IDLE_LIMIT_MS).filter((x) => !inflightSids.has(x.sid));
3201
3259
  const idleSids = new Set(idle.map((x) => x.sid));
3202
3260
  let mutated = false;
3203
3261
  for (const { sid, entry, idleMs } of idle) {
3204
3262
  if (Number.isFinite(entry.pid) && isProcessAliveSync(entry.pid)) {
3205
3263
  try { gracefulCloseBrowser(entry, 'browser-idle'); } catch (_) {}
3206
3264
  }
3265
+ try { __idleClosedSessions.add(sid); } catch (_) {}
3207
3266
  delete ports[sid];
3208
3267
  delete sessions[sid];
3209
3268
  mutated = true;