gm-plugkit 2.0.1518 → 2.0.1520

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/bootstrap.js CHANGED
@@ -665,6 +665,20 @@ function ensureWrapperFresh() {
665
665
  } catch (_) { return false; }
666
666
  }
667
667
 
668
+ function ensureGmPlugkitVersionFresh() {
669
+ try {
670
+ const ownPkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
671
+ if (!ownPkg || !ownPkg.version) return false;
672
+ const dst = path.join(gmToolsDir(), 'gm-plugkit.version');
673
+ let cur = null;
674
+ try { cur = fs.readFileSync(dst, 'utf-8').trim(); } catch (_) {}
675
+ if (cur === ownPkg.version) return false;
676
+ fs.mkdirSync(gmToolsDir(), { recursive: true });
677
+ fs.writeFileSync(dst, ownPkg.version);
678
+ return true;
679
+ } catch (_) { return false; }
680
+ }
681
+
668
682
  function ensureSkillMdFresh() {
669
683
  try {
670
684
  const candidates = [
@@ -820,8 +834,9 @@ async function ensureReady(opts) {
820
834
  if (isReady() && !versionDrift) {
821
835
  const wasmPath = getWasmPath();
822
836
  const wrapperUpdated = ensureWrapperFresh();
837
+ const versionMarkerUpdated = ensureGmPlugkitVersionFresh();
823
838
  ensureSkillMdFresh();
824
- return { ok: true, wasmPath, binaryPath: wasmPath, status: wrapperUpdated ? 'wrapper-refreshed' : 'already-ready', version: installed };
839
+ return { ok: true, wasmPath, binaryPath: wasmPath, status: (wrapperUpdated || versionMarkerUpdated) ? 'wrapper-refreshed' : 'already-ready', version: installed };
825
840
  }
826
841
  if (versionDrift) {
827
842
  try { killRunningDaemons(`version_drift:${installed}->${targetVersion}`); } catch (_) {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1518",
3
+ "version": "2.0.1520",
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": {
@@ -807,6 +807,49 @@ function cleanDeadProfileFragments(cwd) {
807
807
  }
808
808
  }
809
809
 
810
+ function parsePlaywriterSessionList(stdout) {
811
+ const rows = [];
812
+ if (!stdout) return rows;
813
+ const lines = stdout.split(/\r?\n/);
814
+ for (const line of lines) {
815
+ const m = line.match(/^\s*(\d+)\s+\S+\s+\S+\s+\S+\s+(\S+)/);
816
+ if (!m) continue;
817
+ const id = m[1];
818
+ let cwd = m[2];
819
+ if (cwd === '-') cwd = '';
820
+ rows.push({ id, cwd });
821
+ }
822
+ return rows;
823
+ }
824
+
825
+ function reapOrphanBrowserSessions(pw, cwd, claudeSessionId, reason) {
826
+ try {
827
+ const ports = readJsonFile(browserPortsFile(cwd), {});
828
+ const activeIds = new Set();
829
+ for (const ent of Object.values(ports)) {
830
+ if (ent && ent.pwSessionId) activeIds.add(String(ent.pwSessionId));
831
+ }
832
+ const r = runBrowserRunner(pw, ['session', 'list'], 15000, cwd, claudeSessionId);
833
+ if (!r || r.status !== 0) return { reaped: 0 };
834
+ const rows = parsePlaywriterSessionList(r.stdout || '');
835
+ const norm = (p) => String(p || '').replace(/[\\/]+$/, '').toLowerCase();
836
+ const wantCwd = norm(cwd);
837
+ let reaped = 0;
838
+ for (const { id, cwd: rowCwd } of rows) {
839
+ if (rowCwd && norm(rowCwd) !== wantCwd) continue;
840
+ if (activeIds.has(String(id))) continue;
841
+ const d = runBrowserRunner(pw, ['session', 'delete', id], 15000, cwd, claudeSessionId);
842
+ if (d && d.status === 0) {
843
+ reaped++;
844
+ try { logEvent('plugkit', 'browser.orphan-session-reaped', { session_id: id, reason: reason || 'boot', cwd }); } catch (_) {}
845
+ }
846
+ }
847
+ return { reaped };
848
+ } catch (_) {
849
+ return { reaped: 0 };
850
+ }
851
+ }
852
+
810
853
  function resolveWindowsExeLocal(cmd) {
811
854
  if (process.platform !== 'win32') return cmd;
812
855
  try {
@@ -828,7 +871,7 @@ function resolveWindowsExeLocal(cmd) {
828
871
 
829
872
  function isPortReachableSync(host, port, timeoutMs) {
830
873
  const r = spawnSync(process.execPath, ['-e', `
831
- const net = _netModule;
874
+ const net = require('net');
832
875
  const s = net.connect({ port: ${port}, host: ${JSON.stringify(host)} });
833
876
  let done = false;
834
877
  s.on('connect', () => { done = true; s.destroy(); process.exit(0); });
@@ -840,7 +883,7 @@ function isPortReachableSync(host, port, timeoutMs) {
840
883
 
841
884
  function findFreePortSync() {
842
885
  const r = spawnSync(process.execPath, ['-e', `
843
- const net = _netModule;
886
+ const net = require('net');
844
887
  const srv = net.createServer();
845
888
  srv.listen(0, '127.0.0.1', () => { const p = srv.address().port; srv.close(() => { process.stdout.write(String(p)); }); });
846
889
  srv.on('error', e => { process.stderr.write(e.message); process.exit(1); });
@@ -851,7 +894,7 @@ function findFreePortSync() {
851
894
 
852
895
  function isPortAliveSync(port) {
853
896
  const r = spawnSync(process.execPath, ['-e', `
854
- const net = _netModule;
897
+ const net = require('net');
855
898
  const s = net.connect({ port: ${port}, host: '127.0.0.1' });
856
899
  s.on('connect', () => { s.destroy(); process.exit(0); });
857
900
  s.on('error', () => process.exit(1));
@@ -949,7 +992,7 @@ function findInstalledChromiumBinary() {
949
992
 
950
993
  function fetchJsonSync(url, timeoutMs) {
951
994
  const r = spawnSync(process.execPath, ['-e', `
952
- const http = _httpModule;
995
+ const http = require('http');
953
996
  const req = http.get(${JSON.stringify(url)}, (res) => {
954
997
  let buf = '';
955
998
  res.on('data', d => buf += d);
@@ -1057,7 +1100,7 @@ function gracefulCloseBrowser(entry, reason) {
1057
1100
  const info = fetchJsonSync(`http://127.0.0.1:${port}/json/version`, 600);
1058
1101
  if (info && info.webSocketDebuggerUrl) {
1059
1102
  spawnSync(process.execPath, ['-e', `
1060
- const http = _httpModule;
1103
+ const http = require('http');
1061
1104
  const req = http.request({host:'127.0.0.1',port:${port},path:'/json/close/browser',method:'GET',timeout:1500},
1062
1105
  res => { res.resume(); res.on('end', () => process.exit(0)); });
1063
1106
  req.on('error', () => process.exit(1));
@@ -1137,6 +1180,7 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
1137
1180
  }
1138
1181
  }
1139
1182
  cleanDeadProfileFragments(cwd);
1183
+ reapOrphanBrowserSessions(pw, cwd, claudeSessionId, 'pre-spawn');
1140
1184
  const profileDir = acquireProfileDir(cwd, claudeSessionId);
1141
1185
  const aliveCdpForProfile = (() => {
1142
1186
  for (const key of Object.keys(ports)) {
@@ -2085,6 +2129,8 @@ async function runSpoolWatcher(instance, spoolDir) {
2085
2129
  fs.writeFileSync(path.join(gmDir, 'long-gap-retry-state'), '');
2086
2130
  } catch (_) {}
2087
2131
 
2132
+ try { reapOrphanBrowserSessions(findBrowserRunner(), process.cwd(), process.env.CLAUDE_SESSION_ID || 'claude-loop-iter', 'watcher-boot'); } catch (_) {}
2133
+
2088
2134
 
2089
2135
  const LOCK_PATH = path.join(spoolDir, '.watcher.lock');
2090
2136
  try {
@@ -2369,8 +2415,33 @@ async function runSpoolWatcher(instance, spoolDir) {
2369
2415
  }
2370
2416
  const latest = JSON.parse(body).version;
2371
2417
  const stalePath = path.join(spoolDir, '.gm-plugkit-stale.json');
2418
+ const respawnGuardPath = path.join(spoolDir, '.gm-plugkit-respawn-guard.json');
2372
2419
  if (!latest || latest === own) {
2373
2420
  if (fs.existsSync(stalePath)) { try { fs.unlinkSync(stalePath); } catch (_) {} }
2421
+ if (fs.existsSync(respawnGuardPath)) { try { fs.unlinkSync(respawnGuardPath); } catch (_) {} }
2422
+ return;
2423
+ }
2424
+ let respawnGuard = { attempts: 0, last_own: null, last_latest: null, first_ts: Date.now() };
2425
+ try {
2426
+ if (fs.existsSync(respawnGuardPath)) respawnGuard = JSON.parse(fs.readFileSync(respawnGuardPath, 'utf8'));
2427
+ } catch (_) {}
2428
+ const sameStaleAsBefore = respawnGuard.last_own === own && respawnGuard.last_latest === latest;
2429
+ const cameFromSelfRespawn = process.env.PLUGKIT_BOOT_REASON === 'self-respawn-from-self-stale';
2430
+ if (sameStaleAsBefore && respawnGuard.attempts >= 3) {
2431
+ try { fs.writeFileSync(stalePath, JSON.stringify({
2432
+ ts: new Date().toISOString(),
2433
+ reason: 'gm-plugkit-self-stale-respawn-exhausted',
2434
+ running_version: own,
2435
+ latest_version: latest,
2436
+ respawn_attempts: respawnGuard.attempts,
2437
+ instruction: `gm-plugkit ${own} cannot self-upgrade to ${latest}: ${respawnGuard.attempts} respawns all came up ${own} (bun/npx cache is serving the stale tarball). Respawn loop halted to keep this watcher alive and serving verbs. Fix manually: bun pm cache rm; npm cache clean --force; rm -rf ~/AppData/Local/npm-cache/_npx ~/.bun/install/cache; then bun x gm-plugkit@latest --kill-stale-watchers; bun x gm-plugkit@latest spool`,
2438
+ detected_by: 'watcher-periodic-probe',
2439
+ }, null, 2)); } catch (_) {}
2440
+ if (!_selfStaleLoggedOnce) {
2441
+ _selfStaleLoggedOnce = true;
2442
+ try { logEvent('plugkit', 'gm-plugkit.self-stale-respawn-exhausted', { running_version: own, latest_version: latest, attempts: respawnGuard.attempts }); } catch (_) {}
2443
+ console.error(`[plugkit-wasm] gm-plugkit self-stale respawn EXHAUSTED after ${respawnGuard.attempts} attempts (cache serving stale ${own} for latest ${latest}); halting respawn loop and staying alive to serve verbs`);
2444
+ }
2374
2445
  return;
2375
2446
  }
2376
2447
  const marker = {
@@ -2389,6 +2460,25 @@ async function runSpoolWatcher(instance, spoolDir) {
2389
2460
  try {
2390
2461
  const cp = _childProcess;
2391
2462
  const bunPath = process.env.GM_BUN_PATH || 'bun';
2463
+ const bustCache = sameStaleAsBefore || cameFromSelfRespawn;
2464
+ if (bustCache) {
2465
+ try { cp.execFileSync(bunPath, ['pm', 'cache', 'rm'], { stdio: 'ignore', timeout: 30000, windowsHide: true }); } catch (_) {}
2466
+ try {
2467
+ const home = process.env.USERPROFILE || process.env.HOME || '';
2468
+ for (const rel of ['AppData/Local/npm-cache/_npx', '.npm/_npx', '.bun/install/cache']) {
2469
+ try { fs.rmSync(path.join(home, rel), { recursive: true, force: true }); } catch (_) {}
2470
+ }
2471
+ } catch (_) {}
2472
+ try { logEvent('plugkit', 'gm-plugkit.self-stale-cache-busted', { running_version: own, latest_version: latest, attempt: (respawnGuard.attempts || 0) + 1 }); } catch (_) {}
2473
+ }
2474
+ try { fs.writeFileSync(respawnGuardPath, JSON.stringify({
2475
+ attempts: (sameStaleAsBefore ? (respawnGuard.attempts || 0) : 0) + 1,
2476
+ last_own: own,
2477
+ last_latest: latest,
2478
+ first_ts: respawnGuard.first_ts || Date.now(),
2479
+ last_ts: Date.now(),
2480
+ cache_busted: bustCache,
2481
+ }, null, 2)); } catch (_) {}
2392
2482
  const child = cp.spawn(bunPath, ['x', `gm-plugkit@${latest}`, 'spool'], {
2393
2483
  cwd: process.cwd(),
2394
2484
  detached: true,
@@ -2397,7 +2487,7 @@ async function runSpoolWatcher(instance, spoolDir) {
2397
2487
  env: { ...process.env, PLUGKIT_BOOT_REASON: 'self-respawn-from-self-stale' },
2398
2488
  });
2399
2489
  child.unref();
2400
- try { logEvent('plugkit', 'gm-plugkit.self-stale-respawn', { running_version: own, latest_version: latest }); } catch (_) {}
2490
+ try { logEvent('plugkit', 'gm-plugkit.self-stale-respawn', { running_version: own, latest_version: latest, cache_busted: bustCache, attempt: (respawnGuard.attempts || 0) + 1 }); } catch (_) {}
2401
2491
  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 (_) {}
2402
2492
  // Wait for the replacement's fresh heartbeat before exiting (mirror the
2403
2493
  // version-drift path) instead of a blind 2s exit: the gm-plugkit download can
@@ -2406,13 +2496,17 @@ async function runSpoolWatcher(instance, spoolDir) {
2406
2496
  const myPid = process.pid;
2407
2497
  const respawnDeadline = Date.now() + 90000;
2408
2498
  const exitSelfStale = () => { try { process.exit(0); } catch (_) {} };
2499
+ const ownVersionFile = path.join(GM_TOOLS_ROOT, 'gm-plugkit.version');
2409
2500
  const pollSelfStaleReplacement = () => {
2410
2501
  try {
2411
2502
  const st = JSON.parse(fs.readFileSync(STATUS_PATH_FOR_TEARDOWN, 'utf8'));
2412
2503
  const freshHeartbeat = st && st.ts && (Date.now() - st.ts) < 15000;
2413
2504
  const differentProc = st && st.pid && st.pid !== myPid;
2414
- if (freshHeartbeat && differentProc) {
2415
- try { logEvent('plugkit', 'gm-plugkit.self-stale-respawn-confirmed', { old_pid: myPid, new_pid: st.pid, new_version: st.version, latest_version: latest }); } catch (_) {}
2505
+ let replacementOnLatest = false;
2506
+ try { replacementOnLatest = fs.readFileSync(ownVersionFile, 'utf-8').trim() === latest; } catch (_) {}
2507
+ if (freshHeartbeat && differentProc && replacementOnLatest) {
2508
+ try { fs.unlinkSync(respawnGuardPath); } catch (_) {}
2509
+ try { logEvent('plugkit', 'gm-plugkit.self-stale-respawn-confirmed', { old_pid: myPid, new_pid: st.pid, new_version: st.version, latest_version: latest, replacement_gm_plugkit: latest }); } catch (_) {}
2416
2510
  return exitSelfStale();
2417
2511
  }
2418
2512
  } catch (_) {}
@@ -2674,6 +2768,7 @@ async function runSpoolWatcher(instance, spoolDir) {
2674
2768
  try { writeJsonFile(portsFile, ports); } catch (_) {}
2675
2769
  try { writeJsonFile(sessionsFile, sessions); } catch (_) {}
2676
2770
  }
2771
+ try { reapOrphanBrowserSessions(findBrowserRunner(), process.cwd(), process.env.CLAUDE_SESSION_ID || 'claude-loop-iter', 'idle-sweep'); } catch (_) {}
2677
2772
  } catch (e) {
2678
2773
  console.error(`[browser-idle] error: ${e.message}`);
2679
2774
  }