gm-qwen 2.0.883 → 2.0.884
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 +71 -4
- package/bin/plugkit.js +28 -16
- package/gm.json +1 -1
- package/package.json +1 -1
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/gm.json
CHANGED