chrome-ai-bridge 2.3.5 → 2.3.7
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/build/src/Mutex.js +15 -5
- package/build/src/config.js +7 -3
- package/build/src/main.js +44 -7
- package/package.json +1 -1
- package/scripts/cli.mjs +37 -12
package/build/src/Mutex.js
CHANGED
|
@@ -7,22 +7,31 @@ export class Mutex {
|
|
|
7
7
|
static Guard = class Guard {
|
|
8
8
|
#mutex;
|
|
9
9
|
#onRelease;
|
|
10
|
+
#released = false;
|
|
10
11
|
constructor(mutex, onRelease) {
|
|
11
12
|
this.#mutex = mutex;
|
|
12
13
|
this.#onRelease = onRelease;
|
|
13
14
|
}
|
|
14
15
|
dispose() {
|
|
16
|
+
if (this.#released) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
this.#released = true;
|
|
15
20
|
this.#onRelease?.();
|
|
16
21
|
return this.#mutex.release();
|
|
17
22
|
}
|
|
18
23
|
};
|
|
19
|
-
#
|
|
24
|
+
#maxConcurrency;
|
|
25
|
+
#active = 0;
|
|
20
26
|
#acquirers = [];
|
|
27
|
+
constructor(maxConcurrency = 1) {
|
|
28
|
+
this.#maxConcurrency = Math.max(1, Math.floor(maxConcurrency));
|
|
29
|
+
}
|
|
21
30
|
// This is FIFO.
|
|
22
31
|
async acquire(onRelease) {
|
|
23
|
-
if (
|
|
24
|
-
this.#
|
|
25
|
-
return new Mutex.Guard(this);
|
|
32
|
+
if (this.#active < this.#maxConcurrency) {
|
|
33
|
+
this.#active += 1;
|
|
34
|
+
return new Mutex.Guard(this, onRelease);
|
|
26
35
|
}
|
|
27
36
|
const { resolve, promise } = Promise.withResolvers();
|
|
28
37
|
this.#acquirers.push(resolve);
|
|
@@ -32,9 +41,10 @@ export class Mutex {
|
|
|
32
41
|
release() {
|
|
33
42
|
const resolve = this.#acquirers.shift();
|
|
34
43
|
if (!resolve) {
|
|
35
|
-
this.#
|
|
44
|
+
this.#active = Math.max(0, this.#active - 1);
|
|
36
45
|
return;
|
|
37
46
|
}
|
|
47
|
+
// Hand the same concurrency slot to the next waiter.
|
|
38
48
|
resolve();
|
|
39
49
|
}
|
|
40
50
|
}
|
package/build/src/config.js
CHANGED
|
@@ -68,22 +68,26 @@ export function getSessionConfig() {
|
|
|
68
68
|
export function getIpcGuardConfig() {
|
|
69
69
|
const raw = {
|
|
70
70
|
maxSessions: Number(process.env.CAI_IPC_MAX_SESSIONS),
|
|
71
|
+
reservedInitSlots: Number(process.env.CAI_IPC_RESERVED_INIT_SLOTS),
|
|
71
72
|
maxQueue: Number(process.env.CAI_IPC_MAX_QUEUE),
|
|
72
73
|
queueWaitTimeoutMs: Number(process.env.CAI_IPC_QUEUE_WAIT_TIMEOUT_MS),
|
|
73
74
|
sessionIdleMs: Number(process.env.CAI_IPC_SESSION_IDLE_MS),
|
|
74
75
|
startupDelayJitterMs: Number(process.env.CAI_STARTUP_DELAY_JITTER_MS),
|
|
75
76
|
startupProcessThreshold: Number(process.env.CAI_STARTUP_PROCESS_THRESHOLD),
|
|
76
77
|
primaryIdleMs: Number(process.env.CAI_PRIMARY_IDLE_MS),
|
|
78
|
+
execMaxConcurrency: Number(process.env.CAI_EXEC_MAX_CONCURRENCY),
|
|
77
79
|
};
|
|
78
80
|
return {
|
|
79
|
-
maxSessions: raw.maxSessions > 0 ? Math.floor(raw.maxSessions) :
|
|
81
|
+
maxSessions: raw.maxSessions > 0 ? Math.floor(raw.maxSessions) : 20,
|
|
82
|
+
reservedInitSlots: raw.reservedInitSlots >= 0 ? Math.floor(raw.reservedInitSlots) : 2,
|
|
80
83
|
maxQueue: raw.maxQueue > 0 ? Math.floor(raw.maxQueue) : 64,
|
|
81
|
-
queueWaitTimeoutMs: raw.queueWaitTimeoutMs > 0 ? Math.floor(raw.queueWaitTimeoutMs) :
|
|
82
|
-
sessionIdleMs: raw.sessionIdleMs > 0 ? Math.floor(raw.sessionIdleMs) :
|
|
84
|
+
queueWaitTimeoutMs: raw.queueWaitTimeoutMs > 0 ? Math.floor(raw.queueWaitTimeoutMs) : 45_000,
|
|
85
|
+
sessionIdleMs: raw.sessionIdleMs > 0 ? Math.floor(raw.sessionIdleMs) : 120_000,
|
|
83
86
|
startupDelayJitterMs: raw.startupDelayJitterMs > 0 ? Math.floor(raw.startupDelayJitterMs) : 1_500,
|
|
84
87
|
startupProcessThreshold: raw.startupProcessThreshold > 0
|
|
85
88
|
? Math.floor(raw.startupProcessThreshold)
|
|
86
89
|
: 8,
|
|
87
90
|
primaryIdleMs: raw.primaryIdleMs > 0 ? Math.floor(raw.primaryIdleMs) : 300_000,
|
|
91
|
+
execMaxConcurrency: raw.execMaxConcurrency > 0 ? Math.floor(raw.execMaxConcurrency) : 3,
|
|
88
92
|
};
|
|
89
93
|
}
|
package/build/src/main.js
CHANGED
|
@@ -68,6 +68,8 @@ const HEALTH_CHECK_INTERVAL_MS = 500;
|
|
|
68
68
|
const ipcGuardConfig = getIpcGuardConfig();
|
|
69
69
|
const instanceId = randomUUID();
|
|
70
70
|
let becamePrimary = false;
|
|
71
|
+
let stdinClosed = false;
|
|
72
|
+
let getActiveIpcSessionCount = () => 0;
|
|
71
73
|
function countLocalBridgeInstances() {
|
|
72
74
|
try {
|
|
73
75
|
const output = execFileSync('ps', ['-axo', 'command'], {
|
|
@@ -170,7 +172,7 @@ const logDisclaimers = () => {
|
|
|
170
172
|
Make sure the chrome-ai-bridge extension is installed and Chrome is running.
|
|
171
173
|
Available tools: ask_chatgpt_web, ask_gemini_web, ask_chatgpt_gemini_web, take_cdp_snapshot, get_page_dom, ask_gemini_image`);
|
|
172
174
|
};
|
|
173
|
-
const toolMutex = new Mutex();
|
|
175
|
+
const toolMutex = new Mutex(ipcGuardConfig.execMaxConcurrency);
|
|
174
176
|
function registerTool(tool) {
|
|
175
177
|
server.registerTool(tool.name, {
|
|
176
178
|
description: tool.description,
|
|
@@ -260,7 +262,9 @@ logDisclaimers();
|
|
|
260
262
|
const ipcSessionLastActivity = new Map();
|
|
261
263
|
const initQueue = [];
|
|
262
264
|
let initializingCount = 0;
|
|
263
|
-
const
|
|
265
|
+
const getActiveSessionCount = () => Object.keys(ipcTransports).length;
|
|
266
|
+
getActiveIpcSessionCount = getActiveSessionCount;
|
|
267
|
+
const getSessionLoad = () => getActiveSessionCount() + initializingCount;
|
|
264
268
|
const touchIpcSession = (sessionId) => {
|
|
265
269
|
ipcSessionLastActivity.set(sessionId, Date.now());
|
|
266
270
|
};
|
|
@@ -270,6 +274,7 @@ logDisclaimers();
|
|
|
270
274
|
}
|
|
271
275
|
ipcSessionLastActivity.delete(sessionId);
|
|
272
276
|
drainInitQueue();
|
|
277
|
+
maybeShutdownAfterStdinClose('ipc session cleanup');
|
|
273
278
|
};
|
|
274
279
|
function sendJsonRpcError(res, code, message, id = null, statusCode = 400) {
|
|
275
280
|
res.writeHead(statusCode).end(JSON.stringify({
|
|
@@ -291,6 +296,10 @@ logDisclaimers();
|
|
|
291
296
|
if (getSessionLoad() < ipcGuardConfig.maxSessions) {
|
|
292
297
|
return;
|
|
293
298
|
}
|
|
299
|
+
const initWaiterLimit = Math.max(0, Math.min(ipcGuardConfig.reservedInitSlots, ipcGuardConfig.maxQueue));
|
|
300
|
+
if (initQueue.length >= initWaiterLimit) {
|
|
301
|
+
throw new Error('SERVER_CAPACITY_EXCEEDED');
|
|
302
|
+
}
|
|
294
303
|
if (initQueue.length >= ipcGuardConfig.maxQueue) {
|
|
295
304
|
throw new Error('SERVER_QUEUE_FULL');
|
|
296
305
|
}
|
|
@@ -339,9 +348,11 @@ logDisclaimers();
|
|
|
339
348
|
pid: process.pid,
|
|
340
349
|
version,
|
|
341
350
|
instanceId,
|
|
342
|
-
activeSessions:
|
|
351
|
+
activeSessions: getActiveSessionCount(),
|
|
343
352
|
queuedInitializations: initQueue.length,
|
|
344
353
|
sessionCapacity: ipcGuardConfig.maxSessions,
|
|
354
|
+
reservedInitSlots: ipcGuardConfig.reservedInitSlots,
|
|
355
|
+
execMaxConcurrency: ipcGuardConfig.execMaxConcurrency,
|
|
345
356
|
}));
|
|
346
357
|
return;
|
|
347
358
|
}
|
|
@@ -385,7 +396,10 @@ logDisclaimers();
|
|
|
385
396
|
}
|
|
386
397
|
catch (error) {
|
|
387
398
|
const message = error instanceof Error ? error.message : 'SERVER_BUSY_TIMEOUT';
|
|
388
|
-
if (message === '
|
|
399
|
+
if (message === 'SERVER_CAPACITY_EXCEEDED') {
|
|
400
|
+
sendJsonRpcError(res, -32003, message, null, 503);
|
|
401
|
+
}
|
|
402
|
+
else if (message === 'SERVER_QUEUE_FULL') {
|
|
389
403
|
sendJsonRpcError(res, -32002, message, null, 503);
|
|
390
404
|
}
|
|
391
405
|
else {
|
|
@@ -491,7 +505,7 @@ logDisclaimers();
|
|
|
491
505
|
ipcServer.listen(IPC_CONFIG.port, IPC_CONFIG.host, onListening);
|
|
492
506
|
// Primary idle auto-exit: exit when no activity and no active IPC sessions
|
|
493
507
|
const primaryIdleCheckTimer = setInterval(() => {
|
|
494
|
-
const activeSessionCount =
|
|
508
|
+
const activeSessionCount = getActiveSessionCount();
|
|
495
509
|
if (Date.now() - primaryLastActivityAt > ipcGuardConfig.primaryIdleMs &&
|
|
496
510
|
activeSessionCount === 0) {
|
|
497
511
|
logger(`[main] Primary idle for ${Math.round((Date.now() - primaryLastActivityAt) / 1000)}s with 0 active sessions. Auto-exiting.`);
|
|
@@ -540,9 +554,32 @@ async function shutdown(reason) {
|
|
|
540
554
|
clearTimeout(forceExitTimer);
|
|
541
555
|
process.exit(0);
|
|
542
556
|
}
|
|
557
|
+
function maybeShutdownAfterStdinClose(trigger) {
|
|
558
|
+
if (!stdinClosed || isShuttingDown) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const activeSessionCount = getActiveIpcSessionCount();
|
|
562
|
+
if (activeSessionCount > 0) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
logger(`[main] ${trigger}: stdin already closed and all IPC sessions drained. Shutting down.`);
|
|
566
|
+
void shutdown('stdin closed (all IPC sessions drained)');
|
|
567
|
+
}
|
|
568
|
+
function handleStdinClosed(reason) {
|
|
569
|
+
stdinClosed = true;
|
|
570
|
+
if (isShuttingDown) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const activeSessionCount = getActiveIpcSessionCount();
|
|
574
|
+
if (activeSessionCount > 0) {
|
|
575
|
+
logger(`[main] ${reason}: deferring shutdown while ${activeSessionCount} IPC session(s) remain.`);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
void shutdown(reason);
|
|
579
|
+
}
|
|
543
580
|
// stdin close = Claude Code disconnected (most reliable on Windows too)
|
|
544
|
-
process.stdin.on('end', () =>
|
|
545
|
-
process.stdin.on('close', () =>
|
|
581
|
+
process.stdin.on('end', () => handleStdinClosed('stdin ended'));
|
|
582
|
+
process.stdin.on('close', () => handleStdinClosed('stdin closed'));
|
|
546
583
|
// Signal handlers
|
|
547
584
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
548
585
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
package/package.json
CHANGED
package/scripts/cli.mjs
CHANGED
|
@@ -2,24 +2,49 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* CLI Entry Point for chrome-ai-bridge
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Launches the MCP server with browser-globals mock in a child process.
|
|
6
|
+
*
|
|
7
|
+
* Why child process:
|
|
8
|
+
* - main.js may intentionally enter a never-returning proxy path.
|
|
9
|
+
* - awaiting dynamic import(main.js) in-process can trigger unsettled
|
|
10
|
+
* top-level-await warnings and startup failure in that path.
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
13
|
import path from 'node:path';
|
|
10
14
|
import process from 'node:process';
|
|
11
|
-
import {
|
|
15
|
+
import {spawn} from 'node:child_process';
|
|
16
|
+
import {fileURLToPath} from 'node:url';
|
|
12
17
|
|
|
13
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
19
|
const mockPath = path.join(__dirname, 'browser-globals-mock.mjs');
|
|
15
20
|
const mainPath = path.join(__dirname, '..', 'build', 'src', 'main.js');
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
const child = spawn(
|
|
23
|
+
process.execPath,
|
|
24
|
+
[
|
|
25
|
+
'--import',
|
|
26
|
+
mockPath,
|
|
27
|
+
mainPath,
|
|
28
|
+
...process.argv.slice(2),
|
|
29
|
+
],
|
|
30
|
+
{
|
|
31
|
+
stdio: 'inherit',
|
|
32
|
+
env: process.env,
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
child.on('exit', (code, signal) => {
|
|
37
|
+
if (signal) {
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
process.exit(code ?? 0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
process.on('exit', () => {
|
|
44
|
+
if (!child.killed) {
|
|
45
|
+
child.kill('SIGTERM');
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
process.on('SIGTERM', () => child?.kill('SIGTERM'));
|
|
50
|
+
process.on('SIGINT', () => child?.kill('SIGINT'));
|