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 +1 -1
- package/instructions/browser.md +2 -0
- package/package.json +1 -1
- package/plugkit-wasm-wrapper.js +66 -7
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:
|
|
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
|
|
package/instructions/browser.md
CHANGED
|
@@ -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.
|
|
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": {
|
package/plugkit-wasm-wrapper.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 =
|
|
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', '
|
|
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) ||
|
|
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
|
|
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;
|