gm-kilo 2.0.883 → 2.0.885

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/bin/bootstrap.js CHANGED
@@ -9,11 +9,16 @@ const crypto = require('crypto');
9
9
  const { URL } = require('url');
10
10
 
11
11
  const RELEASE_REPO = 'AnEntrypoint/plugkit-bin';
12
- const ATTEMPT_TIMEOUT_MS = 60 * 1000;
13
- const STALL_TIMEOUT_MS = 20 * 1000;
12
+ const ATTEMPT_TIMEOUT_MS = 5 * 60 * 1000;
13
+ const STALL_TIMEOUT_MS = 60 * 1000;
14
14
  const MAX_ATTEMPTS = 5;
15
15
  const BACKOFF_MS = [2000, 5000, 15000, 30000];
16
- const LOCK_STALE_MS = 5 * 60 * 1000;
16
+ // Worst case: a slow link downloading 140MB at 1MB/s = ~140s. Allow 30 minutes
17
+ // before another bootstrap process treats this lock as abandoned. Below this,
18
+ // concurrent bootstrap calls would wipe an in-progress download mid-stream
19
+ // (see the v0.1.294 incident where a race between two wrappers blew away the
20
+ // .partial during a 10-minute fetch).
21
+ const LOCK_STALE_MS = 30 * 60 * 1000;
17
22
 
18
23
  function log(msg) {
19
24
  try { process.stderr.write(`[plugkit-bootstrap] ${msg}\n`); } catch (_) {}
@@ -170,6 +175,10 @@ function fetchToFile(url, destPath, expectedTotal) {
170
175
  return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
171
176
  }
172
177
  const append = res.statusCode === 206 && existing > 0;
178
+ // Ensure parent dir exists — a concurrent prune may have removed it
179
+ // between lock-acquire and now. Recreating is cheap and avoids a
180
+ // confusing ENOENT later.
181
+ try { ensureDir(path.dirname(destPath)); } catch (_) {}
173
182
  const out = fs.createWriteStream(destPath, { flags: append ? 'a' : 'w' });
174
183
  let bytes = append ? existing : 0;
175
184
  let lastStderr = Date.now();
@@ -402,7 +411,65 @@ function resolveCachedBinary(opts) {
402
411
  return null;
403
412
  }
404
413
 
405
- module.exports = { bootstrap, resolveCachedBinary, resolveCachedRtk, platformKey, binaryName, rtkBinaryName, cacheRoot, obsEvent };
414
+ // ---------------------------------------------------------------------------
415
+ // Daemon kill on version change.
416
+ //
417
+ // The plugin tarball pins `plugkit.version`. When that pin advances and we
418
+ // install a newer cached binary, any long-running daemon (the runner) holds
419
+ // stale code and serves stale RPCs until killed. We track which version the
420
+ // daemon was last started under via `.daemon-version`; on every wrapper
421
+ // invocation, if the wrapper-pinned version differs, we kill the daemon so
422
+ // the next exec spawns it fresh under the new binary.
423
+ // ---------------------------------------------------------------------------
424
+
425
+ function daemonVersionSentinel() {
426
+ const root = (() => {
427
+ try { const r = cacheRoot(); ensureDir(r); return r; }
428
+ catch (_) { const r = fallbackCacheRoot(); ensureDir(r); return r; }
429
+ })();
430
+ return path.join(root, '.daemon-version');
431
+ }
432
+
433
+ function readDaemonVersion() {
434
+ try { return fs.readFileSync(daemonVersionSentinel(), 'utf8').trim(); }
435
+ catch (_) { return null; }
436
+ }
437
+
438
+ function writeDaemonVersion(v) {
439
+ try { fs.writeFileSync(daemonVersionSentinel(), String(v)); } catch (_) {}
440
+ }
441
+
442
+ function killRunningDaemons(reason) {
443
+ const tmp = os.tmpdir();
444
+ let killed = 0;
445
+ for (const pidFile of ['glootie-runner.pid', 'plugkit-runner.pid']) {
446
+ const pidPath = path.join(tmp, pidFile);
447
+ if (!fs.existsSync(pidPath)) continue;
448
+ try {
449
+ const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
450
+ if (Number.isFinite(pid) && pid !== process.pid && pidAlive(pid)) {
451
+ try { process.kill(pid, 'SIGTERM'); killed++; }
452
+ catch (_) { try { process.kill(pid); killed++; } catch (_) {} }
453
+ obsEvent('bootstrap', 'daemon.killed', { pid, pidFile, reason });
454
+ }
455
+ try { fs.unlinkSync(pidPath); } catch (_) {}
456
+ } catch (_) {}
457
+ }
458
+ return killed;
459
+ }
460
+
461
+ // Compare wrapper-pinned version against last-recorded daemon version. If
462
+ // they differ, kill the daemon so it respawns under the new binary.
463
+ function killStaleDaemonIfVersionChanged(wrapperDir) {
464
+ let currentVersion;
465
+ try { currentVersion = readVersionFile(wrapperDir); } catch (_) { return; }
466
+ const recorded = readDaemonVersion();
467
+ if (recorded === currentVersion) return;
468
+ if (recorded) killRunningDaemons(`version_change:${recorded}->${currentVersion}`);
469
+ writeDaemonVersion(currentVersion);
470
+ }
471
+
472
+ module.exports = { bootstrap, resolveCachedBinary, resolveCachedRtk, platformKey, binaryName, rtkBinaryName, cacheRoot, obsEvent, killRunningDaemons, killStaleDaemonIfVersionChanged };
406
473
 
407
474
  if (require.main === module) {
408
475
  bootstrap({ silent: false })
package/bin/plugkit.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const { spawn, spawnSync } = require('child_process');
4
4
  const path = require('path');
5
5
  const fs = require('fs');
6
- const { bootstrap, resolveCachedBinary, resolveCachedRtk, obsEvent } = require('./bootstrap');
6
+ const { bootstrap, resolveCachedBinary, resolveCachedRtk, obsEvent, killStaleDaemonIfVersionChanged } = require('./bootstrap');
7
7
 
8
8
  const dir = __dirname;
9
9
 
@@ -26,21 +26,31 @@ async function main() {
26
26
  const isHook = args[0] === 'hook';
27
27
  const startedAt = Date.now();
28
28
  obsEvent('plugkit_wrapper', 'invoke', { argv: args.slice(0, 4), is_hook: isHook });
29
+ // If the plugin tarball updated `plugkit.version` since the runner daemon
30
+ // was last started, kill the daemon so the next `runner start` picks up
31
+ // the freshly-installed binary instead of serving stale RPCs.
32
+ try { killStaleDaemonIfVersionChanged(dir); } catch (_) {}
29
33
  let bin;
30
34
  try {
31
- if (isHook) {
35
+ const hookSubcmd = isHook ? (args[1] || '') : '';
36
+ // session-start ALWAYS bootstraps: this is the once-per-session moment
37
+ // where we guarantee the cached binary matches the wrapper-pinned version.
38
+ // If the bootstrap fails (offline) we fall through to whatever the cache
39
+ // currently has — the hook itself isn't blocking, just refreshing.
40
+ if (isHook && hookSubcmd === 'session-start') {
41
+ obsEvent('plugkit_wrapper', 'hook_bootstrap_session_start', { argv: args.slice(0, 4) });
42
+ try {
43
+ bin = await bootstrap({ wrapperDir: dir, silent: true });
44
+ } catch (e) {
45
+ process.stderr.write(`[plugkit] session-start bootstrap failed: ${e.message}\n`);
46
+ bin = resolveCachedBinary({ wrapperDir: dir }) || legacyFallback();
47
+ }
48
+ // session-start hook itself runs in the freshly-bootstrapped binary
49
+ // below — fall through to the spawn path so the actual handler runs.
50
+ if (!bin) process.exit(0);
51
+ } else if (isHook) {
32
52
  bin = resolveCachedBinary({ wrapperDir: dir }) || legacyFallback();
33
53
  if (!bin) {
34
- const hookSubcmd = args[1] || '';
35
- if (hookSubcmd === 'session-start') {
36
- obsEvent('plugkit_wrapper', 'hook_bootstrap_session_start', { argv: args.slice(0, 4) });
37
- try {
38
- await bootstrap({ wrapperDir: dir });
39
- } catch (e) {
40
- process.stderr.write(`[plugkit] session-start bootstrap failed: ${e.message}\n`);
41
- }
42
- process.exit(0);
43
- }
44
54
  process.stderr.write(`[plugkit] hook ${hookSubcmd} skipped: binary not yet installed. Bootstrap will run on session-start.\n`);
45
55
  obsEvent('plugkit_wrapper', 'hook_skip_uncached', { argv: args.slice(0, 4), dur_ms: Date.now() - startedAt });
46
56
  process.exit(0);
@@ -76,16 +86,18 @@ async function main() {
76
86
  }
77
87
  }
78
88
 
89
+ // legacyFallback only returns a binary that lives next to the wrapper. We
90
+ // never reach across to ~/.claude/gm-tools/plugkit.exe or other ambient
91
+ // install dirs — those have proven to mask bootstrap failures by serving a
92
+ // stale version whose hooks silently mismatch the active wrapper code (see
93
+ // the v0.1.292-vs-v0.1.294 incident).
79
94
  function legacyFallback() {
80
95
  const os = require('os');
81
96
  const p = os.platform();
82
97
  const a = os.arch();
83
98
  let candidates = [];
84
99
  if (p === 'win32') {
85
- candidates = [
86
- path.join(dir, a === 'arm64' ? 'plugkit-win32-arm64.exe' : 'plugkit-win32-x64.exe'),
87
- path.join(dir, 'plugkit.exe'),
88
- ];
100
+ candidates = [path.join(dir, a === 'arm64' ? 'plugkit-win32-arm64.exe' : 'plugkit-win32-x64.exe')];
89
101
  } else if (p === 'darwin') {
90
102
  candidates = [path.join(dir, a === 'arm64' ? 'plugkit-darwin-arm64' : 'plugkit-darwin-x64')];
91
103
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-kilo",
3
- "version": "2.0.883",
3
+ "version": "2.0.885",
4
4
  "description": "State machine agent with hooks, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",