gm-plugkit 2.0.1633 → 2.0.1635
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/instructions/browser.md +5 -1
- package/package.json +1 -1
- package/plugkit-wasm-wrapper.js +124 -3
package/instructions/browser.md
CHANGED
|
@@ -44,9 +44,13 @@ dom=<css-selector>\n
|
|
|
44
44
|
|
|
45
45
|
**`dom=<css-selector>\n` is the zero-boilerplate element probe.** Returns `{selector, match_count, elements:[{tag, text, attrs, visible, rect}]}` for up to 20 matches -- the fastest answer to "is this element there and what does it say." An invalid selector returns `result.error` (no crash). Composes with `url=`.
|
|
46
46
|
|
|
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
|
+
|
|
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
|
+
|
|
47
51
|
## Envelope
|
|
48
52
|
|
|
49
|
-
`{ok, stdout, stderr, exit_code, session_id?, navigation_requested, landed_on_blank?, hint?}`. `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.
|
|
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.
|
|
50
54
|
|
|
51
55
|
## Headed by default
|
|
52
56
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-plugkit",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1635",
|
|
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
|
@@ -873,6 +873,97 @@ function reapOrphanBrowserSessions(pw, cwd, claudeSessionId, reason) {
|
|
|
873
873
|
}
|
|
874
874
|
}
|
|
875
875
|
|
|
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
|
+
}
|
|
910
|
+
function enumerateManagedChromiums(profileRootMarker) {
|
|
911
|
+
const marker = String(profileRootMarker || '').toLowerCase().replace(/\\/g, '/');
|
|
912
|
+
const out = [];
|
|
913
|
+
try {
|
|
914
|
+
if (process.platform === 'win32') {
|
|
915
|
+
const ps = `Get-CimInstance Win32_Process -Filter "Name='chrome.exe'" | Where-Object { $_.CommandLine -like '*--remote-debugging-port*' -and $_.CommandLine -like '*browser-profile*' -and $_.CommandLine -notlike '*--type=*' } | ForEach-Object { $_.ProcessId.ToString() + '|' + $_.CommandLine }`;
|
|
916
|
+
const r = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', ps], { encoding: 'utf-8', windowsHide: true, timeout: 10000 });
|
|
917
|
+
if (r.status === 0 && r.stdout) {
|
|
918
|
+
for (const line of r.stdout.split(/\r?\n/).filter(Boolean)) {
|
|
919
|
+
const bar = line.indexOf('|');
|
|
920
|
+
if (bar < 0) continue;
|
|
921
|
+
const pid = parseInt(line.slice(0, bar), 10);
|
|
922
|
+
const cmd = line.slice(bar + 1);
|
|
923
|
+
if (/--type=/.test(cmd)) continue;
|
|
924
|
+
if (Number.isFinite(pid) && cmd.toLowerCase().replace(/\\/g, '/').includes(marker)) out.push({ pid, cmd });
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
} else {
|
|
928
|
+
const r = spawnSync('ps', ['-eo', 'pid,command'], { encoding: 'utf-8', timeout: 10000 });
|
|
929
|
+
if (r.status === 0 && r.stdout) {
|
|
930
|
+
for (const line of r.stdout.split('\n').slice(1)) {
|
|
931
|
+
if (!/--remote-debugging-port/.test(line) || !/browser-profile/.test(line)) continue;
|
|
932
|
+
if (/--type=/.test(line)) continue;
|
|
933
|
+
const m = line.match(/^\s*(\d+)\s+(.+)$/);
|
|
934
|
+
if (!m) continue;
|
|
935
|
+
const pid = parseInt(m[1], 10);
|
|
936
|
+
if (Number.isFinite(pid) && m[2].toLowerCase().replace(/\\/g, '/').includes(marker)) out.push({ pid, cmd: m[2] });
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
} catch (_) {}
|
|
941
|
+
return out;
|
|
942
|
+
}
|
|
943
|
+
function reapOrphanChromiums(cwd, reason) {
|
|
944
|
+
try {
|
|
945
|
+
const root = browserRootDir(cwd);
|
|
946
|
+
const marker = path.join(root, '.gm', 'browser-profile').toLowerCase().replace(/\\/g, '/');
|
|
947
|
+
const procs = enumerateManagedChromiums(marker);
|
|
948
|
+
if (procs.length === 0) return { reaped: 0 };
|
|
949
|
+
const ports = readJsonFile(browserPortsFile(cwd), {});
|
|
950
|
+
const livePids = new Set();
|
|
951
|
+
for (const ent of Object.values(ports)) {
|
|
952
|
+
if (ent && Number.isFinite(ent.pid) && isProcessAliveSync(ent.pid)) livePids.add(ent.pid);
|
|
953
|
+
}
|
|
954
|
+
const { pids: protectedInflight } = inflightPids();
|
|
955
|
+
const launching = launchingPidsFresh();
|
|
956
|
+
let reaped = 0;
|
|
957
|
+
for (const { pid } of procs) {
|
|
958
|
+
if (livePids.has(pid) || protectedInflight.has(pid) || launching.has(pid)) continue;
|
|
959
|
+
try { killPidQuiet(pid); reaped++; logEvent('plugkit', 'browser.os-orphan-reaped', { pid, reason: reason || 'sweep' }); } catch (_) {}
|
|
960
|
+
}
|
|
961
|
+
return { reaped };
|
|
962
|
+
} catch (_) {
|
|
963
|
+
return { reaped: 0 };
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
876
967
|
function resolveWindowsExeLocal(cmd) {
|
|
877
968
|
if (process.platform !== 'win32') return cmd;
|
|
878
969
|
try {
|
|
@@ -1292,6 +1383,7 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
|
|
|
1292
1383
|
logEvent('plugkit', 'browser.start', { profileDir });
|
|
1293
1384
|
({ pid: browserPid, port, wsEndpoint } = startManagedBrowser(pw, profileDir));
|
|
1294
1385
|
}
|
|
1386
|
+
markLaunching(browserPid);
|
|
1295
1387
|
const r = runBrowserRunner(pw, ['session', 'new', '--direct', wsEndpoint], 30000, cwd, claudeSessionId);
|
|
1296
1388
|
if (!r || r.status !== 0) {
|
|
1297
1389
|
const errTxt = scrubBrowserRunnerText((r && (r.stderr || r.stdout)) || 'unknown');
|
|
@@ -1307,6 +1399,11 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
|
|
|
1307
1399
|
sessions[claudeSessionId] = [pwSessionId];
|
|
1308
1400
|
writeJsonFile(portsFile, ports);
|
|
1309
1401
|
writeJsonFile(sessionsFile, sessions);
|
|
1402
|
+
clearLaunching(browserPid);
|
|
1403
|
+
if (!__openedSessionIds.has(claudeSessionId) && __openedSessionIds.size >= 1) {
|
|
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' });
|
|
1405
|
+
}
|
|
1406
|
+
__openedSessionIds.add(claudeSessionId);
|
|
1310
1407
|
logEvent('plugkit', 'browser.attached', { pwSessionId, pid: browserPid, port });
|
|
1311
1408
|
return pwSessionId;
|
|
1312
1409
|
} finally { releaseSpawnLock(); }
|
|
@@ -2187,6 +2284,7 @@ function makeHostFunctions(instanceRef) {
|
|
|
2187
2284
|
stderr: '',
|
|
2188
2285
|
exit_code: 0,
|
|
2189
2286
|
session_id: pwSessionId,
|
|
2287
|
+
hint: 'Reuse this same session for every browser dispatch this run (the spool sessionId selects it); a different sessionId opens its OWN chromium. Close it with `session close` when done -- the idle/orphan reaper is only a backstop.',
|
|
2190
2288
|
});
|
|
2191
2289
|
}
|
|
2192
2290
|
|
|
@@ -2220,8 +2318,13 @@ function makeHostFunctions(instanceRef) {
|
|
|
2220
2318
|
});
|
|
2221
2319
|
}
|
|
2222
2320
|
|
|
2321
|
+
const wasIdleClosed = __idleClosedSessions.has(sessionId);
|
|
2223
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);
|
|
2224
2326
|
stampBrowserLastUse(cwd, sessionId);
|
|
2327
|
+
markInflight(sessionId, curPid);
|
|
2225
2328
|
let evalBody = body;
|
|
2226
2329
|
let timeoutMs = 120000;
|
|
2227
2330
|
const timeoutMatch = body.match(/^timeout=(\d+)\s*\n([\s\S]*)$/);
|
|
@@ -2348,7 +2451,13 @@ function makeHostFunctions(instanceRef) {
|
|
|
2348
2451
|
evalBody = `const __RET=await (async()=>{${evalBody}})();\n` + emitResult + `return __RET;`;
|
|
2349
2452
|
}
|
|
2350
2453
|
const outerTimeoutMs = Math.min(timeoutMs + 6000, 126000);
|
|
2351
|
-
|
|
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
|
+
}
|
|
2352
2461
|
const ok = r.status === 0;
|
|
2353
2462
|
if (!ok && r.status === null) {
|
|
2354
2463
|
logEvent('plugkit', 'browser.runner-timeout', { session_id: pwSessionId, timeout_ms: timeoutMs, body_bytes: evalBody.length });
|
|
@@ -2375,7 +2484,14 @@ function makeHostFunctions(instanceRef) {
|
|
|
2375
2484
|
timeout_ms_used: timeoutMs,
|
|
2376
2485
|
};
|
|
2377
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
|
+
}
|
|
2378
2491
|
envelope.navigation_requested = !!startUrl;
|
|
2492
|
+
if (__openedSessionIds.size > 1) {
|
|
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.`;
|
|
2494
|
+
}
|
|
2379
2495
|
if (landedOnBlank) {
|
|
2380
2496
|
envelope.landed_on_blank = true;
|
|
2381
2497
|
envelope.hint = "page is about:blank: this dispatch did not navigate, so the expression evaluated against an empty page. Prefix the body with 'url=<target>' (or send a bare 'https://...' URL) to open the page you want before evaluating.";
|
|
@@ -2488,6 +2604,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
2488
2604
|
} catch (_) {}
|
|
2489
2605
|
|
|
2490
2606
|
try { reapOrphanBrowserSessions(findBrowserRunner(), process.cwd(), process.env.CLAUDE_SESSION_ID || 'claude-loop-iter', 'watcher-boot'); } catch (_) {}
|
|
2607
|
+
try { reapOrphanChromiums(process.cwd(), 'watcher-boot'); } catch (_) {}
|
|
2491
2608
|
|
|
2492
2609
|
|
|
2493
2610
|
const LOCK_PATH = path.join(spoolDir, '.watcher.lock');
|
|
@@ -2759,6 +2876,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
2759
2876
|
}
|
|
2760
2877
|
try { fs.unlinkSync(portsFile); } catch (_) {}
|
|
2761
2878
|
try { fs.unlinkSync(sessionsFile); } catch (_) {}
|
|
2879
|
+
try { __inflightDispatch.clear(); __launchingPids.clear(); reapOrphanChromiums(process.cwd(), `teardown:${reason}`); } catch (_) {}
|
|
2762
2880
|
} catch (_) {}
|
|
2763
2881
|
|
|
2764
2882
|
try {
|
|
@@ -3124,7 +3242,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
3124
3242
|
}
|
|
3125
3243
|
}, 60_000);
|
|
3126
3244
|
|
|
3127
|
-
const BROWSER_IDLE_LIMIT_MS = parseInt(process.env.PLUGKIT_BROWSER_IDLE_LIMIT_MS, 10) ||
|
|
3245
|
+
const BROWSER_IDLE_LIMIT_MS = parseInt(process.env.PLUGKIT_BROWSER_IDLE_LIMIT_MS, 10) || 15 * 60 * 1000;
|
|
3128
3246
|
setInterval(() => {
|
|
3129
3247
|
try {
|
|
3130
3248
|
const portsFile = browserPortsFile(process.cwd());
|
|
@@ -3132,13 +3250,15 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
3132
3250
|
const ports = readJsonFile(portsFile, {});
|
|
3133
3251
|
const sessions = readJsonFile(sessionsFile, {});
|
|
3134
3252
|
const now = Date.now();
|
|
3135
|
-
const
|
|
3253
|
+
const { sids: inflightSids } = inflightPids();
|
|
3254
|
+
const idle = selectIdleBrowserSessions(ports, now, BROWSER_IDLE_LIMIT_MS).filter((x) => !inflightSids.has(x.sid));
|
|
3136
3255
|
const idleSids = new Set(idle.map((x) => x.sid));
|
|
3137
3256
|
let mutated = false;
|
|
3138
3257
|
for (const { sid, entry, idleMs } of idle) {
|
|
3139
3258
|
if (Number.isFinite(entry.pid) && isProcessAliveSync(entry.pid)) {
|
|
3140
3259
|
try { gracefulCloseBrowser(entry, 'browser-idle'); } catch (_) {}
|
|
3141
3260
|
}
|
|
3261
|
+
try { __idleClosedSessions.add(sid); } catch (_) {}
|
|
3142
3262
|
delete ports[sid];
|
|
3143
3263
|
delete sessions[sid];
|
|
3144
3264
|
mutated = true;
|
|
@@ -3168,6 +3288,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
3168
3288
|
try { writeJsonFile(sessionsFile, sessions); } catch (_) {}
|
|
3169
3289
|
}
|
|
3170
3290
|
try { reapOrphanBrowserSessions(findBrowserRunner(), process.cwd(), process.env.CLAUDE_SESSION_ID || 'claude-loop-iter', 'idle-sweep'); } catch (_) {}
|
|
3291
|
+
try { reapOrphanChromiums(process.cwd(), 'idle-sweep'); } catch (_) {}
|
|
3171
3292
|
} catch (e) {
|
|
3172
3293
|
console.error(`[browser-idle] error: ${e.message}`);
|
|
3173
3294
|
}
|