chrome-ai-bridge 2.3.1 → 2.3.2

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.
@@ -58,7 +58,7 @@ export function getSessionConfig() {
58
58
  };
59
59
  return {
60
60
  sessionTtlMinutes: raw.ttl > 0 ? raw.ttl : 30,
61
- maxAgents: raw.max > 0 ? Math.floor(raw.max) : 10,
61
+ maxAgents: raw.max > 0 ? Math.floor(raw.max) : 20,
62
62
  cleanupIntervalMinutes: raw.interval > 0 ? raw.interval : 5,
63
63
  };
64
64
  }
package/build/src/main.js CHANGED
@@ -32,7 +32,7 @@ import { cleanupAllConnections } from './fast-cdp/fast-chat.js';
32
32
  import { generateAgentId, setAgentId } from './fast-cdp/agent-context.js';
33
33
  import { cleanupStaleSessions } from './fast-cdp/session-manager.js';
34
34
  import { getSessionConfig, IPC_CONFIG } from './config.js';
35
- import { acquireLock, releaseLock, killSiblings, checkExistingPrimary, updateLockPort } from './process-lock.js';
35
+ import { releaseLock, tryAcquireLockSafe, checkExistingPrimary, updateLockPort } from './process-lock.js';
36
36
  import { checkPrimaryHealth, startProxyMode } from './stdio-http-proxy.js';
37
37
  function readPackageJson() {
38
38
  const currentDir = import.meta.dirname;
@@ -56,28 +56,62 @@ logger(`Starting Chrome AI Bridge v${version} (Extension-only mode)`);
56
56
  // Initialize agent ID for Agent Teams support
57
57
  const agentId = generateAgentId();
58
58
  setAgentId(agentId);
59
- // ─── Multi-client routing ───
60
- // Check if a Primary instance is already running.
61
- // If yes and healthy, enter proxy mode (never returns).
62
- const existingPrimary = checkExistingPrimary();
63
- if (existingPrimary && existingPrimary.port > 0) {
64
- const healthy = await checkPrimaryHealth(existingPrimary.port);
65
- if (healthy) {
66
- logger(`[main] Primary is healthy (port=${existingPrimary.port}). Entering proxy mode.`);
67
- await startProxyMode(existingPrimary.port); // never returns
59
+ // ─── Multi-client routing with retry ───
60
+ // Handles concurrent startup of many processes (e.g. tproj 16-pane scenario).
61
+ // Each process tries to become Primary or fall back to Proxy mode,
62
+ // with exponential backoff + jitter to avoid thundering herd.
63
+ const MAX_STARTUP_ATTEMPTS = 5;
64
+ const BASE_DELAY_MS = 300;
65
+ const HEALTH_CHECK_RETRIES = 3;
66
+ const HEALTH_CHECK_INTERVAL_MS = 500;
67
+ const instanceId = randomUUID();
68
+ let becamePrimary = false;
69
+ for (let attempt = 0; attempt < MAX_STARTUP_ATTEMPTS; attempt++) {
70
+ // 1. Try to become Primary (non-throwing)
71
+ const lockAcquired = await tryAcquireLockSafe(IPC_CONFIG.port, instanceId);
72
+ if (lockAcquired) {
73
+ becamePrimary = true;
74
+ break;
75
+ }
76
+ // 2. Lock held by another process — try to connect as Proxy
77
+ const existingPrimary = checkExistingPrimary();
78
+ if (existingPrimary && existingPrimary.port > 0) {
79
+ for (let hc = 0; hc < HEALTH_CHECK_RETRIES; hc++) {
80
+ const healthy = await checkPrimaryHealth(existingPrimary.port);
81
+ if (healthy) {
82
+ logger(`[main] Primary is healthy (port=${existingPrimary.port}). Entering proxy mode.`);
83
+ await startProxyMode(existingPrimary.port); // never returns
84
+ }
85
+ if (hc < HEALTH_CHECK_RETRIES - 1) {
86
+ const jitter = Math.random() * HEALTH_CHECK_INTERVAL_MS;
87
+ await new Promise(r => setTimeout(r, HEALTH_CHECK_INTERVAL_MS + jitter));
88
+ }
89
+ }
90
+ logger(`[main] Primary (port=${existingPrimary.port}) not healthy after ${HEALTH_CHECK_RETRIES} retries.`);
91
+ }
92
+ // 3. Neither Primary nor Proxy — backoff with jitter and retry
93
+ if (attempt < MAX_STARTUP_ATTEMPTS - 1) {
94
+ const backoff = BASE_DELAY_MS * Math.pow(2, attempt);
95
+ const jitter = Math.random() * BASE_DELAY_MS;
96
+ const delay = backoff + jitter;
97
+ logger(`[main] Startup attempt ${attempt + 1}/${MAX_STARTUP_ATTEMPTS} failed. Retrying in ${Math.round(delay)}ms...`);
98
+ await new Promise(r => setTimeout(r, delay));
68
99
  }
69
- logger(`[main] Primary (port=${existingPrimary.port}) not healthy. Starting as Primary.`);
70
100
  }
71
- // ─── Primary mode ───
72
- // Kill all stale sibling processes first
73
- const killed = await killSiblings();
74
- if (killed > 0) {
75
- logger(`[process-lock] Killed ${killed} stale sibling process(es)`);
101
+ if (!becamePrimary) {
102
+ // Final fallback: one last proxy attempt before giving up
103
+ const existingPrimary = checkExistingPrimary();
104
+ if (existingPrimary && existingPrimary.port > 0) {
105
+ const healthy = await checkPrimaryHealth(existingPrimary.port);
106
+ if (healthy) {
107
+ logger(`[main] Final fallback: entering proxy mode (port=${existingPrimary.port}).`);
108
+ await startProxyMode(existingPrimary.port); // never returns
109
+ }
110
+ }
111
+ logger('[main] Failed to start as Primary or Proxy after all retries. Exiting.');
112
+ process.exit(1);
76
113
  }
77
- // Generate a unique instance ID (survives PID reuse)
78
- const instanceId = randomUUID();
79
- // Acquire exclusive process lock (writes port + instanceId to lock file)
80
- await acquireLock(IPC_CONFIG.port, instanceId);
114
+ // ─── Primary mode ───
81
115
  // Start session cleanup timer
82
116
  const sessionConfig = getSessionConfig();
83
117
  const cleanupTimer = setInterval(async () => {
@@ -133,6 +133,30 @@ async function handleExistingLock() {
133
133
  logger(`[process-lock] Primary is alive (pid=${info.pid}, port=${info.port}). Cannot acquire lock.`);
134
134
  return false;
135
135
  }
136
+ /**
137
+ * Try to acquire lock without throwing on failure.
138
+ * Returns true if lock acquired, false if another process holds it.
139
+ * Used by the retry-based startup loop in main.ts.
140
+ */
141
+ export async function tryAcquireLockSafe(port, instanceId) {
142
+ const fd = tryCreateLock(port, instanceId);
143
+ if (fd !== null) {
144
+ lockFd = fd;
145
+ logger(`[process-lock] Lock acquired (pid=${process.pid}, port=${port}, instanceId=${instanceId.slice(0, 8)})`);
146
+ return true;
147
+ }
148
+ const canRetry = await handleExistingLock();
149
+ if (!canRetry) {
150
+ return false;
151
+ }
152
+ const fd2 = tryCreateLock(port, instanceId);
153
+ if (fd2 !== null) {
154
+ lockFd = fd2;
155
+ logger(`[process-lock] Lock acquired after cleanup (pid=${process.pid}, port=${port})`);
156
+ return true;
157
+ }
158
+ return false;
159
+ }
136
160
  /**
137
161
  * Acquire an exclusive process lock. Call once at startup for Primary mode.
138
162
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-ai-bridge",
3
- "version": "2.3.1",
3
+ "version": "2.3.2",
4
4
  "description": "MCP server bridging Chrome extension and AI assistants (ChatGPT, Gemini). Extension-only mode - no Puppeteer.",
5
5
  "type": "module",
6
6
  "bin": "./scripts/cli.mjs",