@brunobracaioli/aibrain 0.2.0-beta.1 → 0.3.0-beta.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.
@@ -11,6 +11,8 @@ import { WebSocketServer, WebSocket } from 'ws';
11
11
  const MAX_FRAME_BYTES = 16 * 1024 * 1024;
12
12
  const MAX_QUEUED_FRAMES = 64;
13
13
  const MAX_QUEUED_BYTES = 32 * 1024 * 1024;
14
+ const STARTUP_HANDOFF_GRACE_MS = 5_000;
15
+ const NORMAL_DISCOVERY_CLOSE_CODES = new Set([1000, 1001, 1005]);
14
16
 
15
17
  function repoRoot() {
16
18
  return fileURLToPath(new URL('..', import.meta.url)).replace(/\/$/u, '');
@@ -54,28 +56,38 @@ async function main() {
54
56
  const root = repoRoot();
55
57
  const monitorCommand =
56
58
  process.env.CODEX_MONITOR_COMMAND ?? join(root, 'scripts/jev-paid-pilot-codex.sh');
59
+ const monitorCwd = process.env.CODEX_MONITOR_CWD ?? process.cwd();
57
60
  const codexBinary = process.env.CODEX_BIN ?? 'codex';
58
61
  const nativeArgs =
59
62
  process.argv.slice(2)[0] === '--' ? process.argv.slice(3) : process.argv.slice(2);
63
+ const allowStartupHandoff = nativeArgs[0] === 'resume';
60
64
  const childEnv = { ...process.env };
61
65
  delete childEnv.JEV_API_KEY;
62
66
  delete childEnv.TYPESAFE_API_KEY;
63
67
  const bridgeRoot = await mkdtemp(join(tmpdir(), 'ai-brain-codex-terminal-'));
64
68
  await chmod(bridgeRoot, 0o700);
65
69
  const socketPath = join(bridgeRoot, 'codex.sock');
66
- let monitor = null;
67
70
  let terminal = null;
68
- let activeSocket = null;
69
- let accepted = false;
71
+ const generations = new Set();
72
+ let activeConnection = null;
73
+ let pendingCandidate = null;
74
+ let handoffCount = 0;
75
+ let handoffAttemptCount = 0;
76
+ let socketUpgradeCount = 0;
77
+ let secondConnectionRejectCount = 0;
78
+ let candidateRejectCount = 0;
70
79
  let shuttingDown = false;
71
80
  const reportPath = process.env.CODEX_TERMINAL_BRIDGE_REPORT;
72
81
  let initializeReceived = false;
73
82
  let experimentalApi = false;
74
- let initializeId = null;
75
83
  let initializeAck = false;
76
84
  let initializeResponseSeen = false;
77
85
  let initializeErrorCode = null;
78
86
  let turnStartCount = 0;
87
+ let threadResumeCount = 0;
88
+ let threadResumeResponseSeen = false;
89
+ let handoffInitializeAck = false;
90
+ const observedMethods = new Set();
79
91
  const observedThreadIds = new Set();
80
92
  let monitorLineCount = 0;
81
93
  let monitorByteCount = 0;
@@ -84,6 +96,59 @@ async function main() {
84
96
  let server;
85
97
  let webSocketServer;
86
98
  let cleanupPromise = null;
99
+ let failureReported = false;
100
+ let startupGraceTimer = null;
101
+ let cleanupInitiator = 'unknown';
102
+ let lastSocketCloseCode = null;
103
+ let terminalAliveAtSocketClose = null;
104
+ let terminalExitSignal = null;
105
+ const lifecycleEvents = [];
106
+ const recordLifecycle = (event) => {
107
+ if (lifecycleEvents.length < 32) lifecycleEvents.push(event);
108
+ };
109
+ const READ_ONLY_DISCOVERY = new Set([
110
+ 'account/read',
111
+ 'thread/list',
112
+ 'thread/read',
113
+ 'model/list',
114
+ 'configRequirements/read',
115
+ 'collaborationMode/list',
116
+ 'hooks/list',
117
+ 'thread/turns/list',
118
+ 'thread/items/list',
119
+ 'thread/loaded/list',
120
+ 'thread/goal/get',
121
+ 'skills/list',
122
+ 'plugin/list',
123
+ 'app/installed',
124
+ ]);
125
+ const PRIMARY_DISCOVERY = new Set(['account/read', 'thread/list', 'thread/read']);
126
+ const MAX_PENDING_DISCOVERY = 128;
127
+ const MAX_DIAGNOSTIC_ENTRIES = 128;
128
+ const DIAGNOSTIC_METHODS = new Set([
129
+ 'initialize',
130
+ 'initialized',
131
+ 'account/read',
132
+ 'thread/list',
133
+ 'thread/read',
134
+ 'model/list',
135
+ 'configRequirements/read',
136
+ 'collaborationMode/list',
137
+ 'hooks/list',
138
+ 'thread/turns/list',
139
+ 'thread/items/list',
140
+ 'thread/loaded/list',
141
+ 'thread/goal/get',
142
+ 'skills/list',
143
+ 'plugin/list',
144
+ 'app/installed',
145
+ 'thread/resume',
146
+ 'thread/start',
147
+ 'thread/fork',
148
+ 'turn/start',
149
+ 'thread/unsubscribe',
150
+ ]);
151
+ let unknownMethodCount = 0;
87
152
  const killOwned = (child, signal) => {
88
153
  if (!child) return;
89
154
  try {
@@ -94,28 +159,160 @@ async function main() {
94
159
  }
95
160
  };
96
161
 
162
+ const spawnMonitor = () => {
163
+ const generation = {
164
+ child: null,
165
+ connection: null,
166
+ retiring: false,
167
+ monitorBuffer: Buffer.alloc(0),
168
+ monitorWritable: true,
169
+ pendingToMonitor: [],
170
+ queuedFrames: 0,
171
+ queuedBytes: 0,
172
+ retirementTimer: null,
173
+ };
174
+ const child = spawn(monitorCommand, [], {
175
+ cwd: monitorCwd,
176
+ env: childEnv,
177
+ stdio: ['pipe', 'pipe', 'pipe'],
178
+ detached: process.platform !== 'win32',
179
+ });
180
+ generation.child = child;
181
+ generations.add(generation);
182
+ // Native app-server diagnostics may contain terminal control sequences or
183
+ // private operational text. Keep the monitor outside the user's TTY while
184
+ // preserving protocol frames and child exit/error handling separately.
185
+ child.stderr?.on('data', () => undefined);
186
+ child.stderr?.resume?.();
187
+ const fail = (code = 1) => {
188
+ if (shuttingDown || generation.retiring) return;
189
+ if (generation.connection?.isCandidate) {
190
+ rejectCandidate(generation.connection, 1011, 'candidate_monitor_failed');
191
+ return;
192
+ }
193
+ monitorExitCode = code;
194
+ void cleanup().finally(() => process.exit(code));
195
+ };
196
+ child.on('error', () => fail(1));
197
+ child.stdin.on('error', () => fail(1));
198
+ child.stdout.on('error', () => fail(1));
199
+ child.stdin.on('drain', () => {
200
+ if (generation.retiring || shuttingDown) return;
201
+ generation.monitorWritable = true;
202
+ flushGeneration(generation);
203
+ });
204
+ child.stdout.on('data', (chunk) => {
205
+ if (generation.retiring || shuttingDown) return;
206
+ generation.monitorBuffer = Buffer.concat([generation.monitorBuffer, Buffer.from(chunk)]);
207
+ while (true) {
208
+ const newline = generation.monitorBuffer.indexOf(10);
209
+ if (newline < 0) break;
210
+ const line = generation.monitorBuffer.subarray(0, newline);
211
+ generation.monitorBuffer = generation.monitorBuffer.subarray(newline + 1);
212
+ handleMonitorLine(generation, line);
213
+ }
214
+ if (generation.monitorBuffer.length > MAX_FRAME_BYTES) {
215
+ abortGeneration(generation, 'monitor_frame_limit');
216
+ }
217
+ });
218
+ child.on('exit', (code) => {
219
+ if (generation.retiring || shuttingDown) {
220
+ generation.pendingToMonitor.length = 0;
221
+ generation.monitorBuffer = Buffer.alloc(0);
222
+ return;
223
+ }
224
+ fail(code ?? 0);
225
+ });
226
+ return generation;
227
+ };
228
+
229
+ let initialGeneration;
230
+ const flushGeneration = (generation) => {
231
+ while (generation.monitorWritable && generation.pendingToMonitor.length > 0) {
232
+ const frame = generation.pendingToMonitor.shift();
233
+ generation.queuedFrames -= 1;
234
+ generation.queuedBytes -= frame.length;
235
+ generation.monitorWritable = generation.child.stdin.write(`${frame.toString('utf8')}\n`);
236
+ }
237
+ };
238
+
239
+ const enqueueForMonitor = (connection, frame) => {
240
+ const generation = connection.generation;
241
+ if (generation === null || generation.retiring) throw new Error('bridge_monitor_unavailable');
242
+ if (
243
+ frame.length > MAX_FRAME_BYTES ||
244
+ generation.queuedFrames >= MAX_QUEUED_FRAMES ||
245
+ generation.queuedBytes + frame.length > MAX_QUEUED_BYTES
246
+ )
247
+ throw new Error('bridge_queue_limit');
248
+ generation.pendingToMonitor.push(frame);
249
+ generation.queuedFrames += 1;
250
+ generation.queuedBytes += frame.length;
251
+ flushGeneration(generation);
252
+ };
253
+
254
+ const cleanupGeneration = (generation) => {
255
+ if (!generation || generation.retiring) return;
256
+ generation.retiring = true;
257
+ killOwned(generation.child, 'SIGTERM');
258
+ generation.retirementTimer = setTimeout(() => {
259
+ killOwned(generation.child, 'SIGKILL');
260
+ generation.pendingToMonitor.length = 0;
261
+ generation.monitorBuffer = Buffer.alloc(0);
262
+ generations.delete(generation);
263
+ }, 500);
264
+ };
265
+ const abortGeneration = (generation, reason) => {
266
+ const connection = generation.connection;
267
+ if (connection?.isCandidate) {
268
+ rejectCandidate(connection, 1013, reason);
269
+ return;
270
+ }
271
+ if (connection) connection.retiring = true;
272
+ cleanupGeneration(generation);
273
+ connection?.socket.close(1013, reason);
274
+ void cleanup().finally(() => process.exit(1));
275
+ };
276
+ const clearStartupGrace = () => {
277
+ if (startupGraceTimer !== null) clearTimeout(startupGraceTimer);
278
+ startupGraceTimer = null;
279
+ };
280
+ const armStartupGrace = (connection) => {
281
+ if (startupGraceTimer !== null || !allowStartupHandoff || terminal?.exitCode !== null) return;
282
+ recordLifecycle('startup_grace_started');
283
+ startupGraceTimer = setTimeout(() => {
284
+ startupGraceTimer = null;
285
+ if (!shuttingDown && activeConnection === connection && terminal?.exitCode === null) {
286
+ recordLifecycle('startup_handoff_timeout');
287
+ failBridge('startup_handoff_timeout');
288
+ }
289
+ }, STARTUP_HANDOFF_GRACE_MS);
290
+ };
291
+
97
292
  const cleanup = () => {
98
293
  if (cleanupPromise) return cleanupPromise;
99
294
  cleanupPromise = (async () => {
100
295
  shuttingDown = true;
101
- for (const child of [terminal, monitor]) {
102
- killOwned(child, 'SIGTERM');
103
- }
296
+ recordLifecycle('cleanup_started');
297
+ clearStartupGrace();
298
+ for (const generation of generations) cleanupGeneration(generation);
299
+ killOwned(terminal, 'SIGTERM');
104
300
  await new Promise((resolve) => setTimeout(resolve, 500));
105
- for (const child of [terminal, monitor]) {
106
- killOwned(child, 'SIGKILL');
107
- }
301
+ for (const generation of generations) killOwned(generation.child, 'SIGKILL');
302
+ killOwned(terminal, 'SIGKILL');
108
303
  await Promise.race([
109
304
  Promise.all(
110
- [terminal, monitor]
305
+ [terminal, ...[...generations].map((generation) => generation.child)]
111
306
  .filter((child) => child?.exitCode === null)
112
307
  .map((child) => new Promise((resolve) => child.once('exit', resolve))),
113
308
  ),
114
309
  new Promise((resolve) => setTimeout(resolve, 500)),
115
310
  ]);
116
- if (activeSocket && activeSocket.readyState === WebSocket.OPEN)
117
- activeSocket.close(1001, 'bridge stopped');
118
- activeSocket?.terminate();
311
+ for (const connection of [activeConnection, pendingCandidate]) {
312
+ if (connection?.socket.readyState === WebSocket.OPEN)
313
+ connection.socket.close(1001, 'bridge stopped');
314
+ connection?.socket.terminate();
315
+ }
119
316
  if (server)
120
317
  await Promise.race([
121
318
  new Promise((resolve) => server.close(() => resolve())),
@@ -134,11 +331,26 @@ async function main() {
134
331
  initialize_response_seen: initializeResponseSeen,
135
332
  initialize_error_code: initializeErrorCode,
136
333
  turn_start_count: turnStartCount,
334
+ thread_resume_count: threadResumeCount,
335
+ thread_resume_response_seen: threadResumeResponseSeen,
336
+ handoff_initialize_ack: handoffInitializeAck,
337
+ observed_methods: [...observedMethods],
137
338
  observed_thread_ids: [...observedThreadIds],
138
339
  monitor_line_count: monitorLineCount,
139
340
  monitor_byte_count: monitorByteCount,
140
341
  monitor_exit_code: monitorExitCode,
141
342
  terminal_exit_code: terminalExitCode,
343
+ handoff_count: handoffCount,
344
+ handoff_attempt_count: handoffAttemptCount,
345
+ unknown_method_count: unknownMethodCount,
346
+ socket_upgrade_count: socketUpgradeCount,
347
+ second_connection_reject_count: secondConnectionRejectCount,
348
+ candidate_reject_count: candidateRejectCount,
349
+ cleanup_initiator: cleanupInitiator,
350
+ last_socket_close_code: lastSocketCloseCode,
351
+ terminal_alive_at_socket_close: terminalAliveAtSocketClose,
352
+ terminal_exit_signal: terminalExitSignal,
353
+ lifecycle_events: lifecycleEvents,
142
354
  cleanup_complete: true,
143
355
  })}\n`,
144
356
  { mode: 0o600, flag: 'wx' },
@@ -147,23 +359,22 @@ async function main() {
147
359
  return cleanupPromise;
148
360
  };
149
361
 
150
- const shutdown = () => void cleanup().finally(() => process.exit(0));
362
+ const shutdown = () => {
363
+ cleanupInitiator = 'signal';
364
+ clearStartupGrace();
365
+ void cleanup().finally(() => process.exit(0));
366
+ };
367
+ const failBridge = (category) => {
368
+ if (failureReported || shuttingDown) return;
369
+ failureReported = true;
370
+ cleanupInitiator = category;
371
+ process.stderr.write(`${JSON.stringify({ event: 'codex_terminal_bridge_error', category })}\n`);
372
+ void cleanup().finally(() => process.exit(1));
373
+ };
151
374
  process.once('SIGINT', shutdown);
152
375
  process.once('SIGTERM', shutdown);
153
376
 
154
- monitor = spawn(monitorCommand, [], {
155
- cwd: process.cwd(),
156
- env: childEnv,
157
- stdio: ['pipe', 'pipe', 'inherit'],
158
- detached: process.platform !== 'win32',
159
- });
160
- monitor.on('error', () => void cleanup().finally(() => process.exit(1)));
161
- monitor.stdin.on('error', () => void cleanup().finally(() => process.exit(1)));
162
- monitor.stdout.on('error', () => void cleanup().finally(() => process.exit(1)));
163
- monitor.on('exit', (code) => {
164
- monitorExitCode = code;
165
- if (!shuttingDown) void cleanup().finally(() => process.exit(code ?? 0));
166
- });
377
+ initialGeneration = spawnMonitor();
167
378
 
168
379
  server = createServer((_request, response) => {
169
380
  response.writeHead(404);
@@ -174,111 +385,341 @@ async function main() {
174
385
  maxPayload: MAX_FRAME_BYTES,
175
386
  perMessageDeflate: false,
176
387
  });
177
- server.on('upgrade', (request, socket, head) => {
178
- if (request.headers.origin !== undefined) {
179
- closeSocket(socket, 403, 'origin_rejected');
388
+ const sameClient = (left, right) => {
389
+ const leftInfo = left ?? {};
390
+ const rightInfo = right ?? {};
391
+ const hasOriginalIdentity = ['name', 'version'].some((key) => leftInfo[key] !== undefined);
392
+ if (!hasOriginalIdentity) return true;
393
+ if (!right) return false;
394
+ for (const key of ['name', 'version']) {
395
+ if (leftInfo[key] !== undefined && rightInfo?.[key] !== leftInfo[key]) return false;
396
+ }
397
+ return true;
398
+ };
399
+ const canHandoff = (connection) =>
400
+ !shuttingDown &&
401
+ allowStartupHandoff &&
402
+ handoffAttemptCount === 0 &&
403
+ pendingCandidate === null &&
404
+ connection?.handoffEligible === true &&
405
+ connection.startedOperation === false &&
406
+ connection.initialized === true &&
407
+ connection.initializeAck === true &&
408
+ (connection.socket.readyState === WebSocket.OPEN || connection.discoveryClosed === true);
409
+ const rejectCandidate = (connection, statusCode = 1008, reason = 'handoff_rejected') => {
410
+ if (!connection || connection.retiring) return;
411
+ if (connection?.candidateTimer) clearTimeout(connection.candidateTimer);
412
+ if (pendingCandidate === connection) pendingCandidate = null;
413
+ candidateRejectCount += 1;
414
+ connection.retiring = true;
415
+ connection.pendingClientFrames.length = 0;
416
+ connection.pendingClientBytes = 0;
417
+ cleanupGeneration(connection.generation);
418
+ if (connection.socket.readyState === WebSocket.OPEN)
419
+ connection.socket.close(statusCode, reason);
420
+ else connection.socket.terminate();
421
+ if (activeConnection?.socket.readyState !== WebSocket.OPEN && pendingCandidate === null)
422
+ failBridge('startup_handoff_failed');
423
+ };
424
+ const retireConnection = (connection) => {
425
+ if (!connection) return;
426
+ connection.retiring = true;
427
+ if (connection.candidateTimer) clearTimeout(connection.candidateTimer);
428
+ cleanupGeneration(connection.generation);
429
+ if (connection.socket.readyState === WebSocket.OPEN)
430
+ connection.socket.close(1001, 'startup handoff');
431
+ connection.socket.terminate();
432
+ };
433
+ const completeHandoff = (candidate) => {
434
+ if (candidate.retiring || pendingCandidate !== candidate || candidate.initializeAck !== true)
435
+ return;
436
+ candidate.handoffComplete = true;
437
+ if (candidate.candidateTimer) clearTimeout(candidate.candidateTimer);
438
+ const previous = activeConnection;
439
+ if (
440
+ previous === null ||
441
+ previous.handoffEligible !== true ||
442
+ previous.startedOperation === true
443
+ ) {
444
+ rejectCandidate(candidate, 1008, 'handoff_no_longer_eligible');
180
445
  return;
181
446
  }
182
- if (accepted) {
183
- closeSocket(socket, 409, 'second_connection_rejected');
447
+ pendingCandidate = null;
448
+ handoffCount += 1;
449
+ clearStartupGrace();
450
+ recordLifecycle('handoff_ack');
451
+ if (previous) retireConnection(previous);
452
+ activeConnection = candidate;
453
+ candidate.isCandidate = false;
454
+ candidate.isActive = true;
455
+ const pendingFrames = candidate.pendingClientFrames.splice(0);
456
+ candidate.pendingClientBytes = 0;
457
+ for (const frame of pendingFrames) enqueueForMonitor(candidate, frame);
458
+ };
459
+ const handleMonitorLine = (generation, line) => {
460
+ if (shuttingDown) return;
461
+ monitorLineCount += 1;
462
+ monitorByteCount += line.length + 1;
463
+ if (line.length === 0) return;
464
+ const connection = generation.connection;
465
+ if (!connection || connection.retiring) return;
466
+ if (line.length > MAX_FRAME_BYTES) {
467
+ abortGeneration(generation, 'monitor_frame_limit');
184
468
  return;
185
469
  }
186
- webSocketServer.handleUpgrade(request, socket, head, (webSocket) => {
187
- accepted = true;
188
- activeSocket = webSocket;
189
- let initialized = false;
190
- let queuedFrames = 0;
191
- let queuedBytes = 0;
192
- const pendingToMonitor = [];
193
- let monitorWritable = true;
194
- const flush = () => {
195
- while (monitorWritable && pendingToMonitor.length > 0) {
196
- const frame = pendingToMonitor.shift();
197
- queuedFrames -= 1;
198
- queuedBytes -= frame.length;
199
- monitorWritable = monitor.stdin.write(`${frame.toString('utf8')}\n`);
470
+ let parsed;
471
+ try {
472
+ parsed = JSON.parse(line.toString('utf8'));
473
+ if (connection.initializeId !== null && parsed.id === connection.initializeId) {
474
+ initializeResponseSeen = true;
475
+ initializeErrorCode = parsed.error?.code ?? null;
476
+ if (parsed.error === undefined && parsed.result !== undefined) {
477
+ initializeAck = true;
478
+ connection.initializeAck = true;
479
+ if (connection.isCandidate) {
480
+ handoffInitializeAck = true;
481
+ completeHandoff(connection);
482
+ }
483
+ }
484
+ }
485
+ if (
486
+ typeof parsed.params?.threadId === 'string' &&
487
+ parsed.params.threadId.length <= 128 &&
488
+ observedThreadIds.size < MAX_DIAGNOSTIC_ENTRIES
489
+ )
490
+ observedThreadIds.add(parsed.params.threadId);
491
+ if (
492
+ typeof parsed.result?.thread?.id === 'string' &&
493
+ parsed.result.thread.id.length <= 128 &&
494
+ observedThreadIds.size < MAX_DIAGNOSTIC_ENTRIES
495
+ )
496
+ observedThreadIds.add(parsed.result.thread.id);
497
+ const isJsonRpcResponse =
498
+ Object.prototype.hasOwnProperty.call(parsed, 'result') ||
499
+ Object.prototype.hasOwnProperty.call(parsed, 'error');
500
+ if (
501
+ isJsonRpcResponse &&
502
+ parsed.id !== undefined &&
503
+ connection.pendingRequests.has(parsed.id)
504
+ ) {
505
+ const method = connection.pendingRequests.get(parsed.id);
506
+ connection.pendingRequests.delete(parsed.id);
507
+ if (parsed.error === undefined && PRIMARY_DISCOVERY.has(method)) {
508
+ connection.discoverySatisfied = true;
509
+ if (!connection.startedOperation) connection.handoffEligible = true;
510
+ }
511
+ }
512
+ if (
513
+ isJsonRpcResponse &&
514
+ parsed.id !== undefined &&
515
+ connection.threadResumeRequestIds.has(parsed.id)
516
+ ) {
517
+ connection.threadResumeRequestIds.delete(parsed.id);
518
+ if (parsed.error === undefined) threadResumeResponseSeen = true;
519
+ }
520
+ } catch {
521
+ parsed = null;
522
+ }
523
+ if (connection.socket.readyState !== WebSocket.OPEN) return;
524
+ if (connection.socket.bufferedAmount + line.length > MAX_QUEUED_BYTES) {
525
+ abortGeneration(generation, 'bridge_queue_limit');
526
+ return;
527
+ }
528
+ connection.socket.send(line.toString('utf8'), { binary: false });
529
+ };
530
+ const handleClientMessage = (connection, data, isBinary) => {
531
+ if (shuttingDown) return;
532
+ try {
533
+ const frame = Buffer.isBuffer(data) ? data : Buffer.from(data);
534
+ if (isBinary || frame.length > MAX_FRAME_BYTES) throw new Error('bridge_frame_invalid');
535
+ const parsed = JSON.parse(frame.toString('utf8'));
536
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
537
+ throw new Error('bridge_message_not_object');
538
+ const normalizedFrame = Buffer.from(JSON.stringify(parsed), 'utf8');
539
+ if (!connection.initialized) {
540
+ const initialize = parseInitialize(normalizedFrame.toString('utf8'));
541
+ if (
542
+ connection.isCandidate &&
543
+ !sameClient(activeConnection?.clientInfo, initialize.params.clientInfo)
544
+ )
545
+ throw new Error('handoff_client_mismatch');
546
+ connection.initialized = true;
547
+ connection.initializeId = initialize.id;
548
+ connection.clientInfo = initialize.params.clientInfo;
549
+ if (!connection.isCandidate) {
550
+ initializeReceived = true;
551
+ experimentalApi = initialize.params.capabilities.experimentalApi === true;
552
+ }
553
+ if (connection.isCandidate && connection.generation === null) {
554
+ handoffAttemptCount += 1;
555
+ connection.generation = spawnMonitor();
556
+ connection.generation.connection = connection;
557
+ }
558
+ }
559
+ if (parsed.method === 'turn/start') turnStartCount += 1;
560
+ if (typeof parsed.method === 'string') {
561
+ if (DIAGNOSTIC_METHODS.has(parsed.method)) {
562
+ if (observedMethods.size < MAX_DIAGNOSTIC_ENTRIES) observedMethods.add(parsed.method);
563
+ } else {
564
+ unknownMethodCount += 1;
200
565
  }
201
- };
202
- webSocket.on('message', (data, isBinary) => {
203
- try {
204
- const frame = Buffer.isBuffer(data) ? data : Buffer.from(data);
205
- if (isBinary || frame.length > MAX_FRAME_BYTES) throw new Error('bridge_frame_invalid');
206
- const parsed = JSON.parse(frame.toString('utf8'));
207
- if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
208
- throw new Error('bridge_message_not_object');
209
- const normalizedFrame = Buffer.from(JSON.stringify(parsed), 'utf8');
210
- if (!initialized) {
211
- const initialize = parseInitialize(normalizedFrame.toString('utf8'));
212
- initializeReceived = true;
213
- experimentalApi = initialize.params.capabilities.experimentalApi === true;
214
- initializeId = initialize.id;
215
- initialized = true;
566
+ if (parsed.method === 'thread/resume') threadResumeCount += 1;
567
+ if (parsed.method === 'initialize' || parsed.method === 'initialized') {
568
+ // The notification is part of the handshake and does not affect startup eligibility.
569
+ } else if (READ_ONLY_DISCOVERY.has(parsed.method)) {
570
+ const validRequestId =
571
+ (typeof parsed.id === 'string' && parsed.id.length > 0 && parsed.id.length <= 128) ||
572
+ (Number.isSafeInteger(parsed.id) && parsed.id >= 0);
573
+ if (validRequestId && connection.pendingRequests.size < MAX_PENDING_DISCOVERY)
574
+ connection.pendingRequests.set(parsed.id, parsed.method);
575
+ else {
576
+ connection.startedOperation = true;
577
+ connection.handoffEligible = false;
216
578
  }
217
- if (parsed.method === 'turn/start') turnStartCount += 1;
579
+ } else {
580
+ connection.startedOperation = true;
581
+ connection.handoffEligible = false;
218
582
  if (
219
- normalizedFrame.length > MAX_FRAME_BYTES ||
220
- queuedFrames >= MAX_QUEUED_FRAMES ||
221
- queuedBytes + normalizedFrame.length > MAX_QUEUED_BYTES
583
+ parsed.method === 'thread/resume' &&
584
+ ((typeof parsed.id === 'string' && parsed.id.length > 0 && parsed.id.length <= 128) ||
585
+ (Number.isSafeInteger(parsed.id) && parsed.id >= 0)) &&
586
+ connection.threadResumeRequestIds.size < MAX_PENDING_DISCOVERY
222
587
  )
223
- throw new Error('bridge_queue_limit');
224
- pendingToMonitor.push(normalizedFrame);
225
- queuedFrames += 1;
226
- queuedBytes += normalizedFrame.length;
227
- flush();
228
- } catch (error) {
229
- webSocket.close(1008, safeError(error));
230
- }
231
- });
232
- webSocket.on('error', () => void cleanup());
233
- monitor.stdin.on('drain', () => {
234
- monitorWritable = true;
235
- flush();
236
- });
237
- let monitorBuffer = Buffer.alloc(0);
238
- monitor.stdout.on('data', (chunk) => {
239
- monitorBuffer = Buffer.concat([monitorBuffer, Buffer.from(chunk)]);
240
- while (true) {
241
- const newline = monitorBuffer.indexOf(10);
242
- if (newline < 0) break;
243
- const line = monitorBuffer.subarray(0, newline);
244
- monitorBuffer = monitorBuffer.subarray(newline + 1);
245
- monitorLineCount += 1;
246
- monitorByteCount += line.length + 1;
247
- if (line.length === 0) continue;
248
- if (line.length > MAX_FRAME_BYTES) {
249
- webSocket.close(1013, 'monitor_frame_limit');
250
- return;
251
- }
252
- if (webSocket.readyState !== WebSocket.OPEN) continue;
253
- try {
254
- const parsed = JSON.parse(line.toString('utf8'));
255
- if (initializeId !== null && parsed.id === initializeId) {
256
- initializeResponseSeen = true;
257
- initializeErrorCode = parsed.error?.code ?? null;
258
- }
259
- if (typeof parsed.params?.threadId === 'string')
260
- observedThreadIds.add(parsed.params.threadId);
261
- if (typeof parsed.result?.thread?.id === 'string')
262
- observedThreadIds.add(parsed.result.thread.id);
263
- if (
264
- initializeId !== null &&
265
- parsed.id === initializeId &&
266
- parsed.error === undefined &&
267
- parsed.result !== undefined
268
- )
269
- initializeAck = true;
270
- } catch {
271
- // The monitor protocol line is forwarded unchanged; diagnostics stay content-free.
272
- }
273
- if (webSocket.bufferedAmount + line.length > MAX_QUEUED_BYTES) {
274
- webSocket.close(1013, 'bridge_queue_limit');
275
- return;
276
- }
277
- webSocket.send(line.toString('utf8'), { binary: false });
588
+ connection.threadResumeRequestIds.add(parsed.id);
278
589
  }
279
- if (monitorBuffer.length > MAX_FRAME_BYTES) webSocket.close(1013, 'monitor_frame_limit');
280
- });
281
- webSocket.once('close', () => void cleanup().finally(() => process.exit(0)));
590
+ }
591
+ if (connection.discoverySatisfied === true && connection.startedOperation === false)
592
+ connection.handoffEligible = true;
593
+ if (
594
+ connection.isCandidate &&
595
+ connection.initializeAck !== true &&
596
+ parsed.method !== 'initialize'
597
+ ) {
598
+ if (
599
+ connection.pendingClientFrames.length >= MAX_QUEUED_FRAMES ||
600
+ connection.pendingClientBytes + normalizedFrame.length > MAX_QUEUED_BYTES
601
+ )
602
+ throw new Error('handoff_queue_limit');
603
+ connection.pendingClientFrames.push(normalizedFrame);
604
+ connection.pendingClientBytes += normalizedFrame.length;
605
+ } else {
606
+ enqueueForMonitor(connection, normalizedFrame);
607
+ }
608
+ } catch (error) {
609
+ if (connection.isCandidate) rejectCandidate(connection, 1008, safeError(error));
610
+ else connection.socket.close(1008, safeError(error));
611
+ }
612
+ };
613
+ const attachConnection = (webSocket, isCandidate) => {
614
+ const connection = {
615
+ socket: webSocket,
616
+ generation: isCandidate ? null : initialGeneration,
617
+ isCandidate,
618
+ isActive: !isCandidate,
619
+ retiring: false,
620
+ initialized: false,
621
+ initializeId: null,
622
+ initializeAck: false,
623
+ clientInfo: null,
624
+ pendingRequests: new Map(),
625
+ threadResumeRequestIds: new Set(),
626
+ pendingClientFrames: [],
627
+ pendingClientBytes: 0,
628
+ discoverySatisfied: false,
629
+ handoffEligible: false,
630
+ startedOperation: false,
631
+ handoffComplete: false,
632
+ discoveryClosed: false,
633
+ candidateTimer: null,
634
+ };
635
+ if (!isCandidate) {
636
+ activeConnection = connection;
637
+ initialGeneration.connection = connection;
638
+ }
639
+ webSocket.on('message', (data, isBinary) => handleClientMessage(connection, data, isBinary));
640
+ webSocket.on('error', () => {
641
+ if (connection.retiring || shuttingDown) return;
642
+ if (connection.isCandidate) rejectCandidate(connection, 1011, 'candidate_socket_error');
643
+ else {
644
+ cleanupInitiator = 'socket_error';
645
+ recordLifecycle('socket_error');
646
+ clearStartupGrace();
647
+ void cleanup().finally(() => process.exit(1));
648
+ }
649
+ });
650
+ webSocket.once('close', (code) => {
651
+ if (connection.retiring || shuttingDown) return;
652
+ lastSocketCloseCode = code;
653
+ terminalAliveAtSocketClose = terminal?.exitCode === null;
654
+ recordLifecycle('socket_close');
655
+ if (connection.isCandidate) {
656
+ rejectCandidate(connection);
657
+ return;
658
+ }
659
+ if (
660
+ pendingCandidate !== null &&
661
+ connection.handoffEligible === true &&
662
+ connection.startedOperation === false &&
663
+ NORMAL_DISCOVERY_CLOSE_CODES.has(code) &&
664
+ terminal?.exitCode === null
665
+ ) {
666
+ connection.discoveryClosed = true;
667
+ cleanupGeneration(connection.generation);
668
+ return;
669
+ }
670
+ if (pendingCandidate !== null) {
671
+ cleanupInitiator = 'socket_abnormal_close';
672
+ cleanupGeneration(connection.generation);
673
+ failBridge('startup_handoff_failed');
674
+ return;
675
+ }
676
+ if (
677
+ allowStartupHandoff &&
678
+ handoffCount === 0 &&
679
+ handoffAttemptCount === 0 &&
680
+ connection.initialized === true &&
681
+ connection.initializeAck === true &&
682
+ connection.handoffEligible === true &&
683
+ connection.startedOperation === false &&
684
+ NORMAL_DISCOVERY_CLOSE_CODES.has(code) &&
685
+ terminal?.exitCode === null
686
+ ) {
687
+ connection.discoveryClosed = true;
688
+ cleanupGeneration(connection.generation);
689
+ armStartupGrace(connection);
690
+ return;
691
+ }
692
+ cleanupInitiator = code === 1006 ? 'socket_abnormal_close' : 'socket_close';
693
+ clearStartupGrace();
694
+ void cleanup().finally(() => process.exit(0));
695
+ });
696
+ return connection;
697
+ };
698
+ server.on('upgrade', (request, socket, head) => {
699
+ socketUpgradeCount += 1;
700
+ if (shuttingDown) {
701
+ closeSocket(socket, 503, 'bridge_stopped');
702
+ return;
703
+ }
704
+ if (request.headers.origin !== undefined) {
705
+ closeSocket(socket, 403, 'origin_rejected');
706
+ return;
707
+ }
708
+ const isCandidate = activeConnection !== null;
709
+ if (isCandidate && !canHandoff(activeConnection)) {
710
+ secondConnectionRejectCount += 1;
711
+ closeSocket(socket, 409, 'second_connection_rejected');
712
+ return;
713
+ }
714
+ webSocketServer.handleUpgrade(request, socket, head, (webSocket) => {
715
+ const connection = attachConnection(webSocket, isCandidate);
716
+ if (isCandidate) {
717
+ pendingCandidate = connection;
718
+ connection.candidateTimer = setTimeout(
719
+ () => rejectCandidate(connection, 1008, 'handoff_timeout'),
720
+ 5_000,
721
+ );
722
+ }
282
723
  });
283
724
  });
284
725
  try {
@@ -308,8 +749,13 @@ async function main() {
308
749
  terminal.on('error', () => void cleanup().finally(() => process.exit(1)));
309
750
  terminal.on('exit', (code, signal) => {
310
751
  terminalExitCode = code;
311
- if (!shuttingDown)
752
+ terminalExitSignal = signal;
753
+ recordLifecycle('terminal_exit');
754
+ clearStartupGrace();
755
+ if (!shuttingDown) {
756
+ if (cleanupInitiator === 'unknown') cleanupInitiator = 'terminal_exit';
312
757
  void cleanup().finally(() => process.exit(signal === null ? (code ?? 0) : 1));
758
+ }
313
759
  });
314
760
  }
315
761