@brunobracaioli/aibrain 0.2.0-beta.1 → 0.3.0-beta.13

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