@brunobracaioli/aibrain 0.3.0-beta.7 → 0.4.0-beta.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.
@@ -9,9 +9,16 @@ import { fileURLToPath } from 'node:url';
9
9
  import { WebSocketServer, WebSocket } from 'ws';
10
10
 
11
11
  const MAX_FRAME_BYTES = 16 * 1024 * 1024;
12
+ const MAX_MONITOR_RESPONSE_BYTES = 64 * 1024 * 1024;
13
+ const MAX_MONITOR_PENDING_BYTES = 32 * 1024 * 1024;
12
14
  const MAX_QUEUED_FRAMES = 64;
13
15
  const MAX_QUEUED_BYTES = 32 * 1024 * 1024;
14
16
  const STARTUP_HANDOFF_GRACE_MS = 5_000;
17
+ const MAX_AUXILIARY_CONNECTIONS = 1;
18
+ const MAX_AUXILIARY_MESSAGES = 64;
19
+ const MAX_AUXILIARY_IN_FLIGHT = 32;
20
+ const MAX_AUXILIARY_NOTIFICATIONS = 256;
21
+ const DEFAULT_AUXILIARY_IDLE_TIMEOUT_MS = 120_000;
15
22
  const NORMAL_DISCOVERY_CLOSE_CODES = new Set([1000, 1001, 1005]);
16
23
 
17
24
  function repoRoot() {
@@ -60,7 +67,13 @@ async function main() {
60
67
  const codexBinary = process.env.CODEX_BIN ?? 'codex';
61
68
  const nativeArgs =
62
69
  process.argv.slice(2)[0] === '--' ? process.argv.slice(3) : process.argv.slice(2);
63
- const allowStartupHandoff = nativeArgs[0] === 'resume';
70
+ // The bridge owns an interactive Codex TUI transport. Any native command
71
+ // that reaches this bridge may perform a short-lived read-only bootstrap
72
+ // before attaching its conversation transport. The protocol boundary below
73
+ // is the authority: initialize, same-client identity, successful allowlisted
74
+ // discovery, no started operation, one candidate, and a five-second deadline.
75
+ // Non-interactive Codex subcommands never open the socket and exit normally.
76
+ const allowStartupHandoff = true;
64
77
  const childEnv = { ...process.env };
65
78
  delete childEnv.JEV_API_KEY;
66
79
  delete childEnv.TYPESAFE_API_KEY;
@@ -71,11 +84,19 @@ async function main() {
71
84
  const generations = new Set();
72
85
  let activeConnection = null;
73
86
  let pendingCandidate = null;
87
+ const auxiliaryConnections = new Set();
74
88
  let handoffCount = 0;
75
89
  let handoffAttemptCount = 0;
76
90
  let socketUpgradeCount = 0;
77
91
  let secondConnectionRejectCount = 0;
78
92
  let candidateRejectCount = 0;
93
+ let auxiliaryConnectionCount = 0;
94
+ let auxiliaryRejectCount = 0;
95
+ let auxiliaryMutationRejectCount = 0;
96
+ let auxiliaryUnknownResponseCount = 0;
97
+ let auxiliaryIdleTimeoutCount = 0;
98
+ let auxiliaryMessageCount = 0;
99
+ let auxiliaryNotificationCount = 0;
79
100
  let shuttingDown = false;
80
101
  const reportPath = process.env.CODEX_TERMINAL_BRIDGE_REPORT;
81
102
  let initializeReceived = false;
@@ -86,12 +107,20 @@ async function main() {
86
107
  let turnStartCount = 0;
87
108
  let threadResumeCount = 0;
88
109
  let threadResumeResponseSeen = false;
110
+ let threadResumeErrorCode = null;
111
+ let threadResumeErrorCategory = null;
89
112
  let handoffInitializeAck = false;
90
113
  const observedMethods = new Set();
91
114
  const observedThreadIds = new Set();
92
115
  let monitorLineCount = 0;
93
116
  let monitorByteCount = 0;
94
117
  let monitorExitCode = null;
118
+ let transportAbortReason = null;
119
+ let transportAbortBytes = null;
120
+ let transportAbortMethod = null;
121
+ let maxMonitorFrameBytes = 0;
122
+ let maxMonitorFrameMethod = null;
123
+ let outputPauseCount = 0;
95
124
  let terminalExitCode = null;
96
125
  let server;
97
126
  let webSocketServer;
@@ -108,10 +137,12 @@ async function main() {
108
137
  };
109
138
  const READ_ONLY_DISCOVERY = new Set([
110
139
  'account/read',
140
+ 'account/bedrock/checkGovCloudRequirements',
141
+ 'config/read',
142
+ 'configRequirements/read',
111
143
  'thread/list',
112
144
  'thread/read',
113
145
  'model/list',
114
- 'configRequirements/read',
115
146
  'collaborationMode/list',
116
147
  'hooks/list',
117
148
  'thread/turns/list',
@@ -129,6 +160,8 @@ async function main() {
129
160
  'initialize',
130
161
  'initialized',
131
162
  'account/read',
163
+ 'account/bedrock/checkGovCloudRequirements',
164
+ 'config/read',
132
165
  'thread/list',
133
166
  'thread/read',
134
167
  'model/list',
@@ -164,7 +197,10 @@ async function main() {
164
197
  child: null,
165
198
  connection: null,
166
199
  retiring: false,
167
- monitorBuffer: Buffer.alloc(0),
200
+ monitorFrameParts: [],
201
+ monitorFrameBytes: 0,
202
+ monitorPendingChunk: Buffer.alloc(0),
203
+ monitorPaused: false,
168
204
  monitorWritable: true,
169
205
  pendingToMonitor: [],
170
206
  queuedFrames: 0,
@@ -190,6 +226,10 @@ async function main() {
190
226
  rejectCandidate(generation.connection, 1011, 'candidate_monitor_failed');
191
227
  return;
192
228
  }
229
+ if (generation.connection?.isAuxiliary) {
230
+ rejectAuxiliary(generation.connection, 'auxiliary_monitor_failed');
231
+ return;
232
+ }
193
233
  monitorExitCode = code;
194
234
  void cleanup().finally(() => process.exit(code));
195
235
  };
@@ -203,22 +243,30 @@ async function main() {
203
243
  });
204
244
  child.stdout.on('data', (chunk) => {
205
245
  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');
246
+ const part = Buffer.from(chunk);
247
+ if (generation.monitorPaused) {
248
+ if (generation.monitorPendingChunk.length + part.length > MAX_MONITOR_PENDING_BYTES) {
249
+ abortGeneration(
250
+ generation,
251
+ 'monitor_backpressure_limit',
252
+ generation.monitorPendingChunk.length + part.length,
253
+ );
254
+ return;
255
+ }
256
+ generation.monitorPendingChunk =
257
+ generation.monitorPendingChunk.length === 0
258
+ ? part
259
+ : Buffer.concat([generation.monitorPendingChunk, part]);
260
+ return;
216
261
  }
262
+ processMonitorChunk(generation, part);
217
263
  });
218
264
  child.on('exit', (code) => {
219
265
  if (generation.retiring || shuttingDown) {
220
266
  generation.pendingToMonitor.length = 0;
221
- generation.monitorBuffer = Buffer.alloc(0);
267
+ generation.monitorFrameParts = [];
268
+ generation.monitorFrameBytes = 0;
269
+ generation.monitorPendingChunk = Buffer.alloc(0);
222
270
  return;
223
271
  }
224
272
  fail(code ?? 0);
@@ -227,6 +275,56 @@ async function main() {
227
275
  };
228
276
 
229
277
  let initialGeneration;
278
+ const resumeMonitor = (generation) => {
279
+ if (!generation.monitorPaused || generation.retiring || shuttingDown) return;
280
+ generation.monitorPaused = false;
281
+ processMonitorBuffer(generation);
282
+ if (!generation.retiring && !shuttingDown && !generation.monitorPaused)
283
+ generation.child.stdout.resume();
284
+ };
285
+ const pauseMonitor = (generation) => {
286
+ if (generation.monitorPaused || generation.retiring || shuttingDown) return;
287
+ generation.monitorPaused = true;
288
+ outputPauseCount += 1;
289
+ generation.child.stdout.pause();
290
+ };
291
+ const processMonitorChunk = (generation, incoming) => {
292
+ if (generation.retiring || shuttingDown) return;
293
+ let offset = 0;
294
+ while (offset < incoming.length && !generation.monitorPaused && !generation.retiring) {
295
+ const newline = incoming.indexOf(10, offset);
296
+ const end = newline < 0 ? incoming.length : newline;
297
+ const part = incoming.subarray(offset, end);
298
+ if (generation.monitorFrameBytes + part.length > MAX_MONITOR_RESPONSE_BYTES) {
299
+ abortGeneration(
300
+ generation,
301
+ 'monitor_frame_limit',
302
+ generation.monitorFrameBytes + part.length,
303
+ );
304
+ return;
305
+ }
306
+ if (part.length > 0) {
307
+ generation.monitorFrameParts.push(part);
308
+ generation.monitorFrameBytes += part.length;
309
+ }
310
+ offset = newline < 0 ? incoming.length : newline + 1;
311
+ if (newline >= 0) {
312
+ const line = Buffer.concat(generation.monitorFrameParts, generation.monitorFrameBytes);
313
+ generation.monitorFrameParts = [];
314
+ generation.monitorFrameBytes = 0;
315
+ handleMonitorLine(generation, line);
316
+ }
317
+ }
318
+ if (offset < incoming.length) generation.monitorPendingChunk = incoming.subarray(offset);
319
+ };
320
+ const processMonitorBuffer = (generation) => {
321
+ if (generation.retiring || shuttingDown) return;
322
+ if (generation.monitorPendingChunk.length > 0) {
323
+ const pending = generation.monitorPendingChunk;
324
+ generation.monitorPendingChunk = Buffer.alloc(0);
325
+ processMonitorChunk(generation, pending);
326
+ }
327
+ };
230
328
  const flushGeneration = (generation) => {
231
329
  while (generation.monitorWritable && generation.pendingToMonitor.length > 0) {
232
330
  const frame = generation.pendingToMonitor.shift();
@@ -258,16 +356,39 @@ async function main() {
258
356
  generation.retirementTimer = setTimeout(() => {
259
357
  killOwned(generation.child, 'SIGKILL');
260
358
  generation.pendingToMonitor.length = 0;
261
- generation.monitorBuffer = Buffer.alloc(0);
359
+ generation.monitorFrameParts = [];
360
+ generation.monitorFrameBytes = 0;
361
+ generation.monitorPendingChunk = Buffer.alloc(0);
262
362
  generations.delete(generation);
263
363
  }, 500);
264
364
  };
265
- const abortGeneration = (generation, reason) => {
365
+ const abortGeneration = (generation, reason, bytes = null, method = null) => {
266
366
  const connection = generation.connection;
367
+ const primaryConnection = connection?.isCandidate !== true && connection?.isAuxiliary !== true;
368
+ if (transportAbortReason === null || primaryConnection) {
369
+ transportAbortReason = reason;
370
+ transportAbortBytes = Number.isSafeInteger(bytes) ? bytes : null;
371
+ transportAbortMethod = typeof method === 'string' ? method : null;
372
+ }
373
+ if (primaryConnection && cleanupInitiator === 'unknown') {
374
+ cleanupInitiator = `transport_${reason}`;
375
+ process.stderr.write(
376
+ `${JSON.stringify({
377
+ event: 'codex_terminal_bridge_error',
378
+ category: reason,
379
+ ...(transportAbortBytes === null ? {} : { bytes: transportAbortBytes }),
380
+ ...(transportAbortMethod === null ? {} : { method: transportAbortMethod }),
381
+ })}\n`,
382
+ );
383
+ }
267
384
  if (connection?.isCandidate) {
268
385
  rejectCandidate(connection, 1013, reason);
269
386
  return;
270
387
  }
388
+ if (connection?.isAuxiliary) {
389
+ rejectAuxiliary(connection, reason);
390
+ return;
391
+ }
271
392
  if (connection) connection.retiring = true;
272
393
  cleanupGeneration(generation);
273
394
  connection?.socket.close(1013, reason);
@@ -308,7 +429,7 @@ async function main() {
308
429
  ),
309
430
  new Promise((resolve) => setTimeout(resolve, 500)),
310
431
  ]);
311
- for (const connection of [activeConnection, pendingCandidate]) {
432
+ for (const connection of [activeConnection, pendingCandidate, ...auxiliaryConnections]) {
312
433
  if (connection?.socket.readyState === WebSocket.OPEN)
313
434
  connection.socket.close(1001, 'bridge stopped');
314
435
  connection?.socket.terminate();
@@ -333,12 +454,20 @@ async function main() {
333
454
  turn_start_count: turnStartCount,
334
455
  thread_resume_count: threadResumeCount,
335
456
  thread_resume_response_seen: threadResumeResponseSeen,
457
+ thread_resume_error_code: threadResumeErrorCode,
458
+ thread_resume_error_category: threadResumeErrorCategory,
336
459
  handoff_initialize_ack: handoffInitializeAck,
337
460
  observed_methods: [...observedMethods],
338
461
  observed_thread_ids: [...observedThreadIds],
339
462
  monitor_line_count: monitorLineCount,
340
463
  monitor_byte_count: monitorByteCount,
341
464
  monitor_exit_code: monitorExitCode,
465
+ transport_abort_reason: transportAbortReason,
466
+ transport_abort_bytes: transportAbortBytes,
467
+ transport_abort_method: transportAbortMethod,
468
+ max_monitor_frame_bytes: maxMonitorFrameBytes,
469
+ max_monitor_frame_method: maxMonitorFrameMethod,
470
+ output_pause_count: outputPauseCount,
342
471
  terminal_exit_code: terminalExitCode,
343
472
  handoff_count: handoffCount,
344
473
  handoff_attempt_count: handoffAttemptCount,
@@ -346,6 +475,13 @@ async function main() {
346
475
  socket_upgrade_count: socketUpgradeCount,
347
476
  second_connection_reject_count: secondConnectionRejectCount,
348
477
  candidate_reject_count: candidateRejectCount,
478
+ auxiliary_connection_count: auxiliaryConnectionCount,
479
+ auxiliary_reject_count: auxiliaryRejectCount,
480
+ auxiliary_mutation_reject_count: auxiliaryMutationRejectCount,
481
+ auxiliary_unknown_response_count: auxiliaryUnknownResponseCount,
482
+ auxiliary_idle_timeout_count: auxiliaryIdleTimeoutCount,
483
+ auxiliary_message_count: auxiliaryMessageCount,
484
+ auxiliary_notification_count: auxiliaryNotificationCount,
349
485
  cleanup_initiator: cleanupInitiator,
350
486
  last_socket_close_code: lastSocketCloseCode,
351
487
  terminal_alive_at_socket_close: terminalAliveAtSocketClose,
@@ -406,6 +542,24 @@ async function main() {
406
542
  connection.initialized === true &&
407
543
  connection.initializeAck === true &&
408
544
  (connection.socket.readyState === WebSocket.OPEN || connection.discoveryClosed === true);
545
+ const auxiliaryIdleTimeoutMs = (() => {
546
+ const value = Number.parseInt(process.env.CODEX_AUXILIARY_IDLE_TIMEOUT_MS ?? '', 10);
547
+ return Number.isSafeInteger(value) && value >= 1_000 && value <= 300_000
548
+ ? value
549
+ : DEFAULT_AUXILIARY_IDLE_TIMEOUT_MS;
550
+ })();
551
+ const rejectAuxiliary = (connection, reason = 'auxiliary_rejected') => {
552
+ if (!connection || connection.retiring || !connection.isAuxiliary) return;
553
+ connection.retiring = true;
554
+ if (connection.auxiliaryTimer !== null) clearTimeout(connection.auxiliaryTimer);
555
+ auxiliaryConnections.delete(connection);
556
+ auxiliaryRejectCount += 1;
557
+ if (reason === 'auxiliary_idle_timeout') auxiliaryIdleTimeoutCount += 1;
558
+ if (connection.socket.readyState === WebSocket.OPEN)
559
+ connection.socket.close(1008, reason);
560
+ else connection.socket.terminate();
561
+ cleanupGeneration(connection.generation);
562
+ };
409
563
  const rejectCandidate = (connection, statusCode = 1008, reason = 'handoff_rejected') => {
410
564
  if (!connection || connection.retiring) return;
411
565
  if (connection?.candidateTimer) clearTimeout(connection.candidateTimer);
@@ -463,14 +617,20 @@ async function main() {
463
617
  if (line.length === 0) return;
464
618
  const connection = generation.connection;
465
619
  if (!connection || connection.retiring) return;
466
- if (line.length > MAX_FRAME_BYTES) {
467
- abortGeneration(generation, 'monitor_frame_limit');
620
+ if (line.length > MAX_MONITOR_RESPONSE_BYTES) {
621
+ abortGeneration(generation, 'monitor_frame_limit', line.length);
468
622
  return;
469
623
  }
470
624
  let parsed;
625
+ let responseMethod;
471
626
  try {
472
627
  parsed = JSON.parse(line.toString('utf8'));
473
- if (connection.initializeId !== null && parsed.id === connection.initializeId) {
628
+ const isInitializeResponse =
629
+ connection.initializeId !== null &&
630
+ parsed.id === connection.initializeId &&
631
+ connection.initializeResponseConsumed === false;
632
+ if (isInitializeResponse) {
633
+ connection.initializeResponseConsumed = true;
474
634
  initializeResponseSeen = true;
475
635
  initializeErrorCode = parsed.error?.code ?? null;
476
636
  if (parsed.error === undefined && parsed.result !== undefined) {
@@ -497,12 +657,32 @@ async function main() {
497
657
  const isJsonRpcResponse =
498
658
  Object.prototype.hasOwnProperty.call(parsed, 'result') ||
499
659
  Object.prototype.hasOwnProperty.call(parsed, 'error');
660
+ if (connection.isAuxiliary && parsed.method !== undefined && parsed.id === undefined) {
661
+ auxiliaryNotificationCount += 1;
662
+ connection.auxiliaryNotificationCount += 1;
663
+ if (connection.auxiliaryNotificationCount > MAX_AUXILIARY_NOTIFICATIONS) {
664
+ rejectAuxiliary(connection, 'auxiliary_notification_limit');
665
+ }
666
+ return;
667
+ }
668
+ if (
669
+ connection.isAuxiliary &&
670
+ (parsed.method !== undefined ||
671
+ (isJsonRpcResponse &&
672
+ !isInitializeResponse &&
673
+ !connection.pendingRequests.has(parsed.id)))
674
+ ) {
675
+ auxiliaryUnknownResponseCount += 1;
676
+ rejectAuxiliary(connection, 'auxiliary_response_rejected');
677
+ return;
678
+ }
500
679
  if (
501
680
  isJsonRpcResponse &&
502
681
  parsed.id !== undefined &&
503
682
  connection.pendingRequests.has(parsed.id)
504
683
  ) {
505
684
  const method = connection.pendingRequests.get(parsed.id);
685
+ responseMethod = method;
506
686
  connection.pendingRequests.delete(parsed.id);
507
687
  if (parsed.error === undefined && PRIMARY_DISCOVERY.has(method)) {
508
688
  connection.discoverySatisfied = true;
@@ -516,16 +696,45 @@ async function main() {
516
696
  ) {
517
697
  connection.threadResumeRequestIds.delete(parsed.id);
518
698
  if (parsed.error === undefined) threadResumeResponseSeen = true;
699
+ else {
700
+ if (Number.isSafeInteger(parsed.error?.code)) threadResumeErrorCode = parsed.error.code;
701
+ const message =
702
+ typeof parsed.error?.message === 'string' ? parsed.error.message.toLowerCase() : '';
703
+ threadResumeErrorCategory = /no.?rollout|rollout.*not.?found/u.test(message)
704
+ ? 'no_rollout_found'
705
+ : /thread.*not.?found|unknown.*thread/u.test(message)
706
+ ? 'thread_not_found'
707
+ : /sqlite|database|locked|busy/u.test(message)
708
+ ? 'database_state'
709
+ : /config|invalid.*param|parameter/u.test(message)
710
+ ? 'protocol_configuration'
711
+ : /model|provider|auth|credential/u.test(message)
712
+ ? 'model_or_auth'
713
+ : 'native_internal_error';
714
+ }
715
+ }
716
+ if (line.length > maxMonitorFrameBytes) {
717
+ maxMonitorFrameBytes = line.length;
718
+ maxMonitorFrameMethod = responseMethod ?? null;
519
719
  }
520
720
  } catch {
521
- parsed = null;
721
+ abortGeneration(generation, 'monitor_frame_invalid', line.length, responseMethod);
722
+ return;
522
723
  }
523
- if (connection.socket.readyState !== WebSocket.OPEN) return;
524
- if (connection.socket.bufferedAmount + line.length > MAX_QUEUED_BYTES) {
525
- abortGeneration(generation, 'bridge_queue_limit');
724
+ if (connection.isAuxiliary && parsed === null) {
725
+ auxiliaryUnknownResponseCount += 1;
726
+ rejectAuxiliary(connection, 'auxiliary_response_rejected');
526
727
  return;
527
728
  }
528
- connection.socket.send(line.toString('utf8'), { binary: false });
729
+ if (connection.socket.readyState !== WebSocket.OPEN) return;
730
+ pauseMonitor(generation);
731
+ connection.socket.send(line.toString('utf8'), { binary: false }, (error) => {
732
+ if (error && !generation.retiring && !shuttingDown) {
733
+ abortGeneration(generation, 'bridge_output_failed', line.length, responseMethod);
734
+ return;
735
+ }
736
+ resumeMonitor(generation);
737
+ });
529
738
  };
530
739
  const handleClientMessage = (connection, data, isBinary) => {
531
740
  if (shuttingDown) return;
@@ -535,18 +744,29 @@ async function main() {
535
744
  const parsed = JSON.parse(frame.toString('utf8'));
536
745
  if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
537
746
  throw new Error('bridge_message_not_object');
747
+ if (
748
+ connection.isAuxiliary &&
749
+ (typeof parsed.method !== 'string' ||
750
+ Object.prototype.hasOwnProperty.call(parsed, 'result') ||
751
+ Object.prototype.hasOwnProperty.call(parsed, 'error'))
752
+ ) {
753
+ auxiliaryMutationRejectCount += 1;
754
+ rejectAuxiliary(connection, 'auxiliary_client_frame_rejected');
755
+ return;
756
+ }
538
757
  const normalizedFrame = Buffer.from(JSON.stringify(parsed), 'utf8');
758
+ const wasInitialized = connection.initialized;
539
759
  if (!connection.initialized) {
540
760
  const initialize = parseInitialize(normalizedFrame.toString('utf8'));
541
761
  if (
542
- connection.isCandidate &&
762
+ (connection.isCandidate || connection.isAuxiliary) &&
543
763
  !sameClient(activeConnection?.clientInfo, initialize.params.clientInfo)
544
764
  )
545
765
  throw new Error('handoff_client_mismatch');
546
766
  connection.initialized = true;
547
767
  connection.initializeId = initialize.id;
548
768
  connection.clientInfo = initialize.params.clientInfo;
549
- if (!connection.isCandidate) {
769
+ if (!connection.isCandidate && !connection.isAuxiliary) {
550
770
  initializeReceived = true;
551
771
  experimentalApi = initialize.params.capabilities.experimentalApi === true;
552
772
  }
@@ -555,8 +775,30 @@ async function main() {
555
775
  connection.generation = spawnMonitor();
556
776
  connection.generation.connection = connection;
557
777
  }
778
+ if (connection.isAuxiliary && connection.generation === null) {
779
+ connection.generation = spawnMonitor();
780
+ connection.generation.connection = connection;
781
+ }
782
+ }
783
+ if (connection.isAuxiliary) {
784
+ auxiliaryMessageCount += 1;
785
+ connection.auxiliaryMessageCount += 1;
786
+ if (connection.auxiliaryMessageCount > MAX_AUXILIARY_MESSAGES) {
787
+ rejectAuxiliary(connection, 'auxiliary_message_limit');
788
+ return;
789
+ }
790
+ if (connection.auxiliaryTimer !== null) clearTimeout(connection.auxiliaryTimer);
791
+ connection.auxiliaryTimer = setTimeout(
792
+ () => rejectAuxiliary(connection, 'auxiliary_idle_timeout'),
793
+ auxiliaryIdleTimeoutMs,
794
+ );
558
795
  }
559
- if (parsed.method === 'turn/start') turnStartCount += 1;
796
+ if (connection.isAuxiliary && wasInitialized && parsed.method === 'initialize') {
797
+ auxiliaryMutationRejectCount += 1;
798
+ rejectAuxiliary(connection, 'auxiliary_duplicate_initialize');
799
+ return;
800
+ }
801
+ if (parsed.method === 'turn/start' && !connection.isAuxiliary) turnStartCount += 1;
560
802
  if (typeof parsed.method === 'string') {
561
803
  if (DIAGNOSTIC_METHODS.has(parsed.method)) {
562
804
  if (observedMethods.size < MAX_DIAGNOSTIC_ENTRIES) observedMethods.add(parsed.method);
@@ -565,7 +807,33 @@ async function main() {
565
807
  }
566
808
  if (parsed.method === 'thread/resume') threadResumeCount += 1;
567
809
  if (parsed.method === 'initialize' || parsed.method === 'initialized') {
810
+ if (connection.isAuxiliary && parsed.method === 'initialized') {
811
+ if (connection.initializedNotificationSeen) {
812
+ auxiliaryMutationRejectCount += 1;
813
+ rejectAuxiliary(connection, 'auxiliary_duplicate_initialized');
814
+ return;
815
+ }
816
+ connection.initializedNotificationSeen = true;
817
+ }
568
818
  // The notification is part of the handshake and does not affect startup eligibility.
819
+ } else if (connection.isAuxiliary) {
820
+ if (!READ_ONLY_DISCOVERY.has(parsed.method)) {
821
+ auxiliaryMutationRejectCount += 1;
822
+ rejectAuxiliary(connection, 'auxiliary_mutation_rejected');
823
+ return;
824
+ }
825
+ const validRequestId =
826
+ (typeof parsed.id === 'string' && parsed.id.length > 0 && parsed.id.length <= 128) ||
827
+ (Number.isSafeInteger(parsed.id) && parsed.id >= 0);
828
+ if (
829
+ !validRequestId ||
830
+ connection.pendingRequests.has(parsed.id) ||
831
+ connection.pendingRequests.size >= MAX_AUXILIARY_IN_FLIGHT
832
+ ) {
833
+ rejectAuxiliary(connection, 'auxiliary_request_limit');
834
+ return;
835
+ }
836
+ connection.pendingRequests.set(parsed.id, parsed.method);
569
837
  } else if (READ_ONLY_DISCOVERY.has(parsed.method)) {
570
838
  const validRequestId =
571
839
  (typeof parsed.id === 'string' && parsed.id.length > 0 && parsed.id.length <= 128) ||
@@ -607,18 +875,22 @@ async function main() {
607
875
  }
608
876
  } catch (error) {
609
877
  if (connection.isCandidate) rejectCandidate(connection, 1008, safeError(error));
878
+ else if (connection.isAuxiliary) rejectAuxiliary(connection, safeError(error));
610
879
  else connection.socket.close(1008, safeError(error));
611
880
  }
612
881
  };
613
- const attachConnection = (webSocket, isCandidate) => {
882
+ const attachConnection = (webSocket, isCandidate, isAuxiliary = false) => {
614
883
  const connection = {
615
884
  socket: webSocket,
616
- generation: isCandidate ? null : initialGeneration,
885
+ generation: isCandidate || isAuxiliary ? null : initialGeneration,
617
886
  isCandidate,
618
- isActive: !isCandidate,
887
+ isAuxiliary,
888
+ isActive: !isCandidate && !isAuxiliary,
619
889
  retiring: false,
620
890
  initialized: false,
621
891
  initializeId: null,
892
+ initializeResponseConsumed: false,
893
+ initializedNotificationSeen: false,
622
894
  initializeAck: false,
623
895
  clientInfo: null,
624
896
  pendingRequests: new Map(),
@@ -631,15 +903,27 @@ async function main() {
631
903
  handoffComplete: false,
632
904
  discoveryClosed: false,
633
905
  candidateTimer: null,
906
+ auxiliaryTimer: null,
907
+ auxiliaryMessageCount: 0,
908
+ auxiliaryNotificationCount: 0,
634
909
  };
635
- if (!isCandidate) {
910
+ if (!isCandidate && !isAuxiliary) {
636
911
  activeConnection = connection;
637
912
  initialGeneration.connection = connection;
638
913
  }
914
+ if (isAuxiliary) {
915
+ auxiliaryConnections.add(connection);
916
+ auxiliaryConnectionCount += 1;
917
+ connection.auxiliaryTimer = setTimeout(
918
+ () => rejectAuxiliary(connection, 'auxiliary_idle_timeout'),
919
+ auxiliaryIdleTimeoutMs,
920
+ );
921
+ }
639
922
  webSocket.on('message', (data, isBinary) => handleClientMessage(connection, data, isBinary));
640
923
  webSocket.on('error', () => {
641
924
  if (connection.retiring || shuttingDown) return;
642
925
  if (connection.isCandidate) rejectCandidate(connection, 1011, 'candidate_socket_error');
926
+ else if (connection.isAuxiliary) rejectAuxiliary(connection, 'auxiliary_socket_error');
643
927
  else {
644
928
  cleanupInitiator = 'socket_error';
645
929
  recordLifecycle('socket_error');
@@ -652,6 +936,12 @@ async function main() {
652
936
  lastSocketCloseCode = code;
653
937
  terminalAliveAtSocketClose = terminal?.exitCode === null;
654
938
  recordLifecycle('socket_close');
939
+ if (connection.isAuxiliary) {
940
+ if (connection.auxiliaryTimer !== null) clearTimeout(connection.auxiliaryTimer);
941
+ auxiliaryConnections.delete(connection);
942
+ cleanupGeneration(connection.generation);
943
+ return;
944
+ }
655
945
  if (connection.isCandidate) {
656
946
  rejectCandidate(connection);
657
947
  return;
@@ -705,14 +995,23 @@ async function main() {
705
995
  closeSocket(socket, 403, 'origin_rejected');
706
996
  return;
707
997
  }
708
- const isCandidate = activeConnection !== null;
709
- if (isCandidate && !canHandoff(activeConnection)) {
998
+ const isCandidate = activeConnection !== null && canHandoff(activeConnection);
999
+ const isAuxiliary =
1000
+ activeConnection !== null &&
1001
+ !isCandidate &&
1002
+ activeConnection.initialized === true &&
1003
+ activeConnection.startedOperation === true;
1004
+ if (
1005
+ activeConnection !== null &&
1006
+ !isCandidate &&
1007
+ (!isAuxiliary || auxiliaryConnections.size >= MAX_AUXILIARY_CONNECTIONS)
1008
+ ) {
710
1009
  secondConnectionRejectCount += 1;
711
1010
  closeSocket(socket, 409, 'second_connection_rejected');
712
1011
  return;
713
1012
  }
714
1013
  webSocketServer.handleUpgrade(request, socket, head, (webSocket) => {
715
- const connection = attachConnection(webSocket, isCandidate);
1014
+ const connection = attachConnection(webSocket, isCandidate, isAuxiliary);
716
1015
  if (isCandidate) {
717
1016
  pendingCandidate = connection;
718
1017
  connection.candidateTimer = setTimeout(