gm-plugkit 2.0.1517 → 2.0.1519

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.1517",
3
+ "version": "2.0.1519",
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": {
@@ -828,7 +828,7 @@ function resolveWindowsExeLocal(cmd) {
828
828
 
829
829
  function isPortReachableSync(host, port, timeoutMs) {
830
830
  const r = spawnSync(process.execPath, ['-e', `
831
- const net = _netModule;
831
+ const net = require('net');
832
832
  const s = net.connect({ port: ${port}, host: ${JSON.stringify(host)} });
833
833
  let done = false;
834
834
  s.on('connect', () => { done = true; s.destroy(); process.exit(0); });
@@ -840,7 +840,7 @@ function isPortReachableSync(host, port, timeoutMs) {
840
840
 
841
841
  function findFreePortSync() {
842
842
  const r = spawnSync(process.execPath, ['-e', `
843
- const net = _netModule;
843
+ const net = require('net');
844
844
  const srv = net.createServer();
845
845
  srv.listen(0, '127.0.0.1', () => { const p = srv.address().port; srv.close(() => { process.stdout.write(String(p)); }); });
846
846
  srv.on('error', e => { process.stderr.write(e.message); process.exit(1); });
@@ -851,7 +851,7 @@ function findFreePortSync() {
851
851
 
852
852
  function isPortAliveSync(port) {
853
853
  const r = spawnSync(process.execPath, ['-e', `
854
- const net = _netModule;
854
+ const net = require('net');
855
855
  const s = net.connect({ port: ${port}, host: '127.0.0.1' });
856
856
  s.on('connect', () => { s.destroy(); process.exit(0); });
857
857
  s.on('error', () => process.exit(1));
@@ -949,7 +949,7 @@ function findInstalledChromiumBinary() {
949
949
 
950
950
  function fetchJsonSync(url, timeoutMs) {
951
951
  const r = spawnSync(process.execPath, ['-e', `
952
- const http = _httpModule;
952
+ const http = require('http');
953
953
  const req = http.get(${JSON.stringify(url)}, (res) => {
954
954
  let buf = '';
955
955
  res.on('data', d => buf += d);
@@ -1057,7 +1057,7 @@ function gracefulCloseBrowser(entry, reason) {
1057
1057
  const info = fetchJsonSync(`http://127.0.0.1:${port}/json/version`, 600);
1058
1058
  if (info && info.webSocketDebuggerUrl) {
1059
1059
  spawnSync(process.execPath, ['-e', `
1060
- const http = _httpModule;
1060
+ const http = require('http');
1061
1061
  const req = http.request({host:'127.0.0.1',port:${port},path:'/json/close/browser',method:'GET',timeout:1500},
1062
1062
  res => { res.resume(); res.on('end', () => process.exit(0)); });
1063
1063
  req.on('error', () => process.exit(1));
@@ -2369,8 +2369,33 @@ async function runSpoolWatcher(instance, spoolDir) {
2369
2369
  }
2370
2370
  const latest = JSON.parse(body).version;
2371
2371
  const stalePath = path.join(spoolDir, '.gm-plugkit-stale.json');
2372
+ const respawnGuardPath = path.join(spoolDir, '.gm-plugkit-respawn-guard.json');
2372
2373
  if (!latest || latest === own) {
2373
2374
  if (fs.existsSync(stalePath)) { try { fs.unlinkSync(stalePath); } catch (_) {} }
2375
+ if (fs.existsSync(respawnGuardPath)) { try { fs.unlinkSync(respawnGuardPath); } catch (_) {} }
2376
+ return;
2377
+ }
2378
+ let respawnGuard = { attempts: 0, last_own: null, last_latest: null, first_ts: Date.now() };
2379
+ try {
2380
+ if (fs.existsSync(respawnGuardPath)) respawnGuard = JSON.parse(fs.readFileSync(respawnGuardPath, 'utf8'));
2381
+ } catch (_) {}
2382
+ const sameStaleAsBefore = respawnGuard.last_own === own && respawnGuard.last_latest === latest;
2383
+ const cameFromSelfRespawn = process.env.PLUGKIT_BOOT_REASON === 'self-respawn-from-self-stale';
2384
+ if (sameStaleAsBefore && respawnGuard.attempts >= 3) {
2385
+ try { fs.writeFileSync(stalePath, JSON.stringify({
2386
+ ts: new Date().toISOString(),
2387
+ reason: 'gm-plugkit-self-stale-respawn-exhausted',
2388
+ running_version: own,
2389
+ latest_version: latest,
2390
+ respawn_attempts: respawnGuard.attempts,
2391
+ 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`,
2392
+ detected_by: 'watcher-periodic-probe',
2393
+ }, null, 2)); } catch (_) {}
2394
+ if (!_selfStaleLoggedOnce) {
2395
+ _selfStaleLoggedOnce = true;
2396
+ try { logEvent('plugkit', 'gm-plugkit.self-stale-respawn-exhausted', { running_version: own, latest_version: latest, attempts: respawnGuard.attempts }); } catch (_) {}
2397
+ 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`);
2398
+ }
2374
2399
  return;
2375
2400
  }
2376
2401
  const marker = {
@@ -2389,6 +2414,25 @@ async function runSpoolWatcher(instance, spoolDir) {
2389
2414
  try {
2390
2415
  const cp = _childProcess;
2391
2416
  const bunPath = process.env.GM_BUN_PATH || 'bun';
2417
+ const bustCache = sameStaleAsBefore || cameFromSelfRespawn;
2418
+ if (bustCache) {
2419
+ try { cp.execFileSync(bunPath, ['pm', 'cache', 'rm'], { stdio: 'ignore', timeout: 30000, windowsHide: true }); } catch (_) {}
2420
+ try {
2421
+ const home = process.env.USERPROFILE || process.env.HOME || '';
2422
+ for (const rel of ['AppData/Local/npm-cache/_npx', '.npm/_npx', '.bun/install/cache']) {
2423
+ try { fs.rmSync(path.join(home, rel), { recursive: true, force: true }); } catch (_) {}
2424
+ }
2425
+ } catch (_) {}
2426
+ try { logEvent('plugkit', 'gm-plugkit.self-stale-cache-busted', { running_version: own, latest_version: latest, attempt: (respawnGuard.attempts || 0) + 1 }); } catch (_) {}
2427
+ }
2428
+ try { fs.writeFileSync(respawnGuardPath, JSON.stringify({
2429
+ attempts: (sameStaleAsBefore ? (respawnGuard.attempts || 0) : 0) + 1,
2430
+ last_own: own,
2431
+ last_latest: latest,
2432
+ first_ts: respawnGuard.first_ts || Date.now(),
2433
+ last_ts: Date.now(),
2434
+ cache_busted: bustCache,
2435
+ }, null, 2)); } catch (_) {}
2392
2436
  const child = cp.spawn(bunPath, ['x', `gm-plugkit@${latest}`, 'spool'], {
2393
2437
  cwd: process.cwd(),
2394
2438
  detached: true,
@@ -2397,7 +2441,7 @@ async function runSpoolWatcher(instance, spoolDir) {
2397
2441
  env: { ...process.env, PLUGKIT_BOOT_REASON: 'self-respawn-from-self-stale' },
2398
2442
  });
2399
2443
  child.unref();
2400
- try { logEvent('plugkit', 'gm-plugkit.self-stale-respawn', { running_version: own, latest_version: latest }); } catch (_) {}
2444
+ try { logEvent('plugkit', 'gm-plugkit.self-stale-respawn', { running_version: own, latest_version: latest, cache_busted: bustCache, attempt: (respawnGuard.attempts || 0) + 1 }); } catch (_) {}
2401
2445
  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
2446
  // Wait for the replacement's fresh heartbeat before exiting (mirror the
2403
2447
  // version-drift path) instead of a blind 2s exit: the gm-plugkit download can
@@ -2406,13 +2450,17 @@ async function runSpoolWatcher(instance, spoolDir) {
2406
2450
  const myPid = process.pid;
2407
2451
  const respawnDeadline = Date.now() + 90000;
2408
2452
  const exitSelfStale = () => { try { process.exit(0); } catch (_) {} };
2453
+ const ownVersionFile = path.join(GM_TOOLS_ROOT, 'gm-plugkit.version');
2409
2454
  const pollSelfStaleReplacement = () => {
2410
2455
  try {
2411
2456
  const st = JSON.parse(fs.readFileSync(STATUS_PATH_FOR_TEARDOWN, 'utf8'));
2412
2457
  const freshHeartbeat = st && st.ts && (Date.now() - st.ts) < 15000;
2413
2458
  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 (_) {}
2459
+ let replacementOnLatest = false;
2460
+ try { replacementOnLatest = fs.readFileSync(ownVersionFile, 'utf-8').trim() === latest; } catch (_) {}
2461
+ if (freshHeartbeat && differentProc && replacementOnLatest) {
2462
+ try { fs.unlinkSync(respawnGuardPath); } catch (_) {}
2463
+ 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
2464
  return exitSelfStale();
2417
2465
  }
2418
2466
  } catch (_) {}
package/supervisor.js CHANGED
@@ -72,20 +72,31 @@ function pidAlive(pid) {
72
72
  // and duplicates spawn duplicate watchers that lock-fight in an endless spawn-reject churn. Write the
73
73
  // pid file on startup and refuse to start if a live peer already holds it.
74
74
  function acquireSingleInstance() {
75
- try {
76
- if (fs.existsSync(SUPERVISOR_PID_PATH)) {
77
- const other = parseInt(fs.readFileSync(SUPERVISOR_PID_PATH, 'utf-8').trim(), 10);
78
- if (Number.isFinite(other) && other !== process.pid && pidAlive(other)) {
79
- logEvent('supervisor.refused-duplicate', { existing_pid: other, severity: 'warn' });
80
- return false;
75
+ // Atomic via O_EXCL ('wx'): exclusive-create fails if the file exists, so when N supervisors
76
+ // race to start in the same instant exactly one wins. A plain existsSync->write is TOCTOU and
77
+ // lets a concurrent burst all pass, which is the duplicate-supervisor churn this guards against.
78
+ for (let attempt = 0; attempt < 2; attempt++) {
79
+ try {
80
+ const fd = fs.openSync(SUPERVISOR_PID_PATH, 'wx');
81
+ try { fs.writeSync(fd, String(process.pid)); } finally { fs.closeSync(fd); }
82
+ return true;
83
+ } catch (e) {
84
+ if (e && e.code === 'EEXIST') {
85
+ let other = NaN;
86
+ try { other = parseInt(fs.readFileSync(SUPERVISOR_PID_PATH, 'utf-8').trim(), 10); } catch (_) {}
87
+ if (Number.isFinite(other) && other !== process.pid && pidAlive(other)) {
88
+ logEvent('supervisor.refused-duplicate', { existing_pid: other, severity: 'warn' });
89
+ return false;
90
+ }
91
+ // Holder is dead/stale: remove and retry the exclusive create once.
92
+ try { fs.unlinkSync(SUPERVISOR_PID_PATH); } catch (_) {}
93
+ continue;
81
94
  }
95
+ logEvent('supervisor.pid-write-failed', { error: e && e.message, severity: 'warn' });
96
+ return true;
82
97
  }
83
- fs.writeFileSync(SUPERVISOR_PID_PATH, String(process.pid));
84
- return true;
85
- } catch (e) {
86
- logEvent('supervisor.pid-write-failed', { error: e.message, severity: 'warn' });
87
- return true;
88
98
  }
99
+ return true;
89
100
  }
90
101
 
91
102
  function releaseSingleInstance() {