@brunobracaioli/aibrain 0.3.0-beta.13 → 0.3.0-beta.15

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;
@@ -94,6 +115,12 @@ async function main() {
94
115
  let monitorLineCount = 0;
95
116
  let monitorByteCount = 0;
96
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;
97
124
  let terminalExitCode = null;
98
125
  let server;
99
126
  let webSocketServer;
@@ -110,10 +137,12 @@ async function main() {
110
137
  };
111
138
  const READ_ONLY_DISCOVERY = new Set([
112
139
  'account/read',
140
+ 'account/bedrock/checkGovCloudRequirements',
141
+ 'config/read',
142
+ 'configRequirements/read',
113
143
  'thread/list',
114
144
  'thread/read',
115
145
  'model/list',
116
- 'configRequirements/read',
117
146
  'collaborationMode/list',
118
147
  'hooks/list',
119
148
  'thread/turns/list',
@@ -131,6 +160,8 @@ async function main() {
131
160
  'initialize',
132
161
  'initialized',
133
162
  'account/read',
163
+ 'account/bedrock/checkGovCloudRequirements',
164
+ 'config/read',
134
165
  'thread/list',
135
166
  'thread/read',
136
167
  'model/list',
@@ -166,7 +197,10 @@ async function main() {
166
197
  child: null,
167
198
  connection: null,
168
199
  retiring: false,
169
- monitorBuffer: Buffer.alloc(0),
200
+ monitorFrameParts: [],
201
+ monitorFrameBytes: 0,
202
+ monitorPendingChunk: Buffer.alloc(0),
203
+ monitorPaused: false,
170
204
  monitorWritable: true,
171
205
  pendingToMonitor: [],
172
206
  queuedFrames: 0,
@@ -192,6 +226,10 @@ async function main() {
192
226
  rejectCandidate(generation.connection, 1011, 'candidate_monitor_failed');
193
227
  return;
194
228
  }
229
+ if (generation.connection?.isAuxiliary) {
230
+ rejectAuxiliary(generation.connection, 'auxiliary_monitor_failed');
231
+ return;
232
+ }
195
233
  monitorExitCode = code;
196
234
  void cleanup().finally(() => process.exit(code));
197
235
  };
@@ -205,22 +243,30 @@ async function main() {
205
243
  });
206
244
  child.stdout.on('data', (chunk) => {
207
245
  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');
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;
218
261
  }
262
+ processMonitorChunk(generation, part);
219
263
  });
220
264
  child.on('exit', (code) => {
221
265
  if (generation.retiring || shuttingDown) {
222
266
  generation.pendingToMonitor.length = 0;
223
- generation.monitorBuffer = Buffer.alloc(0);
267
+ generation.monitorFrameParts = [];
268
+ generation.monitorFrameBytes = 0;
269
+ generation.monitorPendingChunk = Buffer.alloc(0);
224
270
  return;
225
271
  }
226
272
  fail(code ?? 0);
@@ -229,6 +275,56 @@ async function main() {
229
275
  };
230
276
 
231
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
+ };
232
328
  const flushGeneration = (generation) => {
233
329
  while (generation.monitorWritable && generation.pendingToMonitor.length > 0) {
234
330
  const frame = generation.pendingToMonitor.shift();
@@ -260,16 +356,39 @@ async function main() {
260
356
  generation.retirementTimer = setTimeout(() => {
261
357
  killOwned(generation.child, 'SIGKILL');
262
358
  generation.pendingToMonitor.length = 0;
263
- generation.monitorBuffer = Buffer.alloc(0);
359
+ generation.monitorFrameParts = [];
360
+ generation.monitorFrameBytes = 0;
361
+ generation.monitorPendingChunk = Buffer.alloc(0);
264
362
  generations.delete(generation);
265
363
  }, 500);
266
364
  };
267
- const abortGeneration = (generation, reason) => {
365
+ const abortGeneration = (generation, reason, bytes = null, method = null) => {
268
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
+ }
269
384
  if (connection?.isCandidate) {
270
385
  rejectCandidate(connection, 1013, reason);
271
386
  return;
272
387
  }
388
+ if (connection?.isAuxiliary) {
389
+ rejectAuxiliary(connection, reason);
390
+ return;
391
+ }
273
392
  if (connection) connection.retiring = true;
274
393
  cleanupGeneration(generation);
275
394
  connection?.socket.close(1013, reason);
@@ -310,7 +429,7 @@ async function main() {
310
429
  ),
311
430
  new Promise((resolve) => setTimeout(resolve, 500)),
312
431
  ]);
313
- for (const connection of [activeConnection, pendingCandidate]) {
432
+ for (const connection of [activeConnection, pendingCandidate, ...auxiliaryConnections]) {
314
433
  if (connection?.socket.readyState === WebSocket.OPEN)
315
434
  connection.socket.close(1001, 'bridge stopped');
316
435
  connection?.socket.terminate();
@@ -343,6 +462,12 @@ async function main() {
343
462
  monitor_line_count: monitorLineCount,
344
463
  monitor_byte_count: monitorByteCount,
345
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,
346
471
  terminal_exit_code: terminalExitCode,
347
472
  handoff_count: handoffCount,
348
473
  handoff_attempt_count: handoffAttemptCount,
@@ -350,6 +475,13 @@ async function main() {
350
475
  socket_upgrade_count: socketUpgradeCount,
351
476
  second_connection_reject_count: secondConnectionRejectCount,
352
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,
353
485
  cleanup_initiator: cleanupInitiator,
354
486
  last_socket_close_code: lastSocketCloseCode,
355
487
  terminal_alive_at_socket_close: terminalAliveAtSocketClose,
@@ -410,6 +542,24 @@ async function main() {
410
542
  connection.initialized === true &&
411
543
  connection.initializeAck === true &&
412
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
+ };
413
563
  const rejectCandidate = (connection, statusCode = 1008, reason = 'handoff_rejected') => {
414
564
  if (!connection || connection.retiring) return;
415
565
  if (connection?.candidateTimer) clearTimeout(connection.candidateTimer);
@@ -467,14 +617,20 @@ async function main() {
467
617
  if (line.length === 0) return;
468
618
  const connection = generation.connection;
469
619
  if (!connection || connection.retiring) return;
470
- if (line.length > MAX_FRAME_BYTES) {
471
- abortGeneration(generation, 'monitor_frame_limit');
620
+ if (line.length > MAX_MONITOR_RESPONSE_BYTES) {
621
+ abortGeneration(generation, 'monitor_frame_limit', line.length);
472
622
  return;
473
623
  }
474
624
  let parsed;
625
+ let responseMethod;
475
626
  try {
476
627
  parsed = JSON.parse(line.toString('utf8'));
477
- 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;
478
634
  initializeResponseSeen = true;
479
635
  initializeErrorCode = parsed.error?.code ?? null;
480
636
  if (parsed.error === undefined && parsed.result !== undefined) {
@@ -501,12 +657,32 @@ async function main() {
501
657
  const isJsonRpcResponse =
502
658
  Object.prototype.hasOwnProperty.call(parsed, 'result') ||
503
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
+ }
504
679
  if (
505
680
  isJsonRpcResponse &&
506
681
  parsed.id !== undefined &&
507
682
  connection.pendingRequests.has(parsed.id)
508
683
  ) {
509
684
  const method = connection.pendingRequests.get(parsed.id);
685
+ responseMethod = method;
510
686
  connection.pendingRequests.delete(parsed.id);
511
687
  if (parsed.error === undefined && PRIMARY_DISCOVERY.has(method)) {
512
688
  connection.discoverySatisfied = true;
@@ -537,15 +713,28 @@ async function main() {
537
713
  : 'native_internal_error';
538
714
  }
539
715
  }
716
+ if (line.length > maxMonitorFrameBytes) {
717
+ maxMonitorFrameBytes = line.length;
718
+ maxMonitorFrameMethod = responseMethod ?? null;
719
+ }
540
720
  } catch {
541
- parsed = null;
721
+ abortGeneration(generation, 'monitor_frame_invalid', line.length, responseMethod);
722
+ return;
542
723
  }
543
- if (connection.socket.readyState !== WebSocket.OPEN) return;
544
- if (connection.socket.bufferedAmount + line.length > MAX_QUEUED_BYTES) {
545
- abortGeneration(generation, 'bridge_queue_limit');
724
+ if (connection.isAuxiliary && parsed === null) {
725
+ auxiliaryUnknownResponseCount += 1;
726
+ rejectAuxiliary(connection, 'auxiliary_response_rejected');
546
727
  return;
547
728
  }
548
- 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
+ });
549
738
  };
550
739
  const handleClientMessage = (connection, data, isBinary) => {
551
740
  if (shuttingDown) return;
@@ -555,18 +744,29 @@ async function main() {
555
744
  const parsed = JSON.parse(frame.toString('utf8'));
556
745
  if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
557
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
+ }
558
757
  const normalizedFrame = Buffer.from(JSON.stringify(parsed), 'utf8');
758
+ const wasInitialized = connection.initialized;
559
759
  if (!connection.initialized) {
560
760
  const initialize = parseInitialize(normalizedFrame.toString('utf8'));
561
761
  if (
562
- connection.isCandidate &&
762
+ (connection.isCandidate || connection.isAuxiliary) &&
563
763
  !sameClient(activeConnection?.clientInfo, initialize.params.clientInfo)
564
764
  )
565
765
  throw new Error('handoff_client_mismatch');
566
766
  connection.initialized = true;
567
767
  connection.initializeId = initialize.id;
568
768
  connection.clientInfo = initialize.params.clientInfo;
569
- if (!connection.isCandidate) {
769
+ if (!connection.isCandidate && !connection.isAuxiliary) {
570
770
  initializeReceived = true;
571
771
  experimentalApi = initialize.params.capabilities.experimentalApi === true;
572
772
  }
@@ -575,8 +775,30 @@ async function main() {
575
775
  connection.generation = spawnMonitor();
576
776
  connection.generation.connection = connection;
577
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
+ );
578
795
  }
579
- 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;
580
802
  if (typeof parsed.method === 'string') {
581
803
  if (DIAGNOSTIC_METHODS.has(parsed.method)) {
582
804
  if (observedMethods.size < MAX_DIAGNOSTIC_ENTRIES) observedMethods.add(parsed.method);
@@ -585,7 +807,33 @@ async function main() {
585
807
  }
586
808
  if (parsed.method === 'thread/resume') threadResumeCount += 1;
587
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
+ }
588
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);
589
837
  } else if (READ_ONLY_DISCOVERY.has(parsed.method)) {
590
838
  const validRequestId =
591
839
  (typeof parsed.id === 'string' && parsed.id.length > 0 && parsed.id.length <= 128) ||
@@ -627,18 +875,22 @@ async function main() {
627
875
  }
628
876
  } catch (error) {
629
877
  if (connection.isCandidate) rejectCandidate(connection, 1008, safeError(error));
878
+ else if (connection.isAuxiliary) rejectAuxiliary(connection, safeError(error));
630
879
  else connection.socket.close(1008, safeError(error));
631
880
  }
632
881
  };
633
- const attachConnection = (webSocket, isCandidate) => {
882
+ const attachConnection = (webSocket, isCandidate, isAuxiliary = false) => {
634
883
  const connection = {
635
884
  socket: webSocket,
636
- generation: isCandidate ? null : initialGeneration,
885
+ generation: isCandidate || isAuxiliary ? null : initialGeneration,
637
886
  isCandidate,
638
- isActive: !isCandidate,
887
+ isAuxiliary,
888
+ isActive: !isCandidate && !isAuxiliary,
639
889
  retiring: false,
640
890
  initialized: false,
641
891
  initializeId: null,
892
+ initializeResponseConsumed: false,
893
+ initializedNotificationSeen: false,
642
894
  initializeAck: false,
643
895
  clientInfo: null,
644
896
  pendingRequests: new Map(),
@@ -651,15 +903,27 @@ async function main() {
651
903
  handoffComplete: false,
652
904
  discoveryClosed: false,
653
905
  candidateTimer: null,
906
+ auxiliaryTimer: null,
907
+ auxiliaryMessageCount: 0,
908
+ auxiliaryNotificationCount: 0,
654
909
  };
655
- if (!isCandidate) {
910
+ if (!isCandidate && !isAuxiliary) {
656
911
  activeConnection = connection;
657
912
  initialGeneration.connection = connection;
658
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
+ }
659
922
  webSocket.on('message', (data, isBinary) => handleClientMessage(connection, data, isBinary));
660
923
  webSocket.on('error', () => {
661
924
  if (connection.retiring || shuttingDown) return;
662
925
  if (connection.isCandidate) rejectCandidate(connection, 1011, 'candidate_socket_error');
926
+ else if (connection.isAuxiliary) rejectAuxiliary(connection, 'auxiliary_socket_error');
663
927
  else {
664
928
  cleanupInitiator = 'socket_error';
665
929
  recordLifecycle('socket_error');
@@ -672,6 +936,12 @@ async function main() {
672
936
  lastSocketCloseCode = code;
673
937
  terminalAliveAtSocketClose = terminal?.exitCode === null;
674
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
+ }
675
945
  if (connection.isCandidate) {
676
946
  rejectCandidate(connection);
677
947
  return;
@@ -725,14 +995,23 @@ async function main() {
725
995
  closeSocket(socket, 403, 'origin_rejected');
726
996
  return;
727
997
  }
728
- const isCandidate = activeConnection !== null;
729
- 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
+ ) {
730
1009
  secondConnectionRejectCount += 1;
731
1010
  closeSocket(socket, 409, 'second_connection_rejected');
732
1011
  return;
733
1012
  }
734
1013
  webSocketServer.handleUpgrade(request, socket, head, (webSocket) => {
735
- const connection = attachConnection(webSocket, isCandidate);
1014
+ const connection = attachConnection(webSocket, isCandidate, isAuxiliary);
736
1015
  if (isCandidate) {
737
1016
  pendingCandidate = connection;
738
1017
  connection.candidateTimer = setTimeout(