@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.
- package/README.md +14 -3
- package/dist/assets/claude/.claude-plugin/plugin.json +1 -1
- package/dist/assets/claude/hooks/native-host-contract-runtime.ts +18 -0
- package/dist/assets/claude/hooks/native-host-contract.test.ts +42 -0
- package/dist/assets/jev-paid-pilot-terminal.mjs +314 -35
- package/dist/claude-compatibility.mjs +506 -0
- package/dist/cli.mjs +23 -27
- package/dist/codex-diagnostics.mjs +19 -2
- package/dist/launch.mjs +11 -4
- package/dist/package.json +1 -1
- package/dist/platform.mjs +2 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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.
|
|
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.
|
|
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 >
|
|
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
|
-
|
|
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
|
-
|
|
721
|
+
abortGeneration(generation, 'monitor_frame_invalid', line.length, responseMethod);
|
|
722
|
+
return;
|
|
542
723
|
}
|
|
543
|
-
if (connection.
|
|
544
|
-
|
|
545
|
-
|
|
724
|
+
if (connection.isAuxiliary && parsed === null) {
|
|
725
|
+
auxiliaryUnknownResponseCount += 1;
|
|
726
|
+
rejectAuxiliary(connection, 'auxiliary_response_rejected');
|
|
546
727
|
return;
|
|
547
728
|
}
|
|
548
|
-
connection.socket.
|
|
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 === '
|
|
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
|
-
|
|
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
|
-
|
|
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(
|