@brunobracaioli/aibrain 0.1.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.
- package/README.md +87 -6
- package/dist/assets/claude/.claude-plugin/plugin.json +103 -1
- package/dist/assets/claude/hooks/context-compaction.ts +6263 -677
- package/dist/assets/claude/hooks/provider-window.js +263 -0
- package/dist/assets/claude/hooks/state-fitting.d.mts +47 -0
- package/dist/assets/claude/hooks/state-fitting.mjs +324 -0
- package/dist/assets/codex-context-compaction.mjs +2874 -262
- package/dist/assets/context-selection-fitting.mjs +194 -0
- package/dist/assets/context-state-fitting.mjs +324 -0
- package/dist/assets/context-telemetry-reporter.mjs +305 -0
- package/dist/assets/jev-paid-pilot-terminal.mjs +591 -125
- package/dist/claude-checkpoint.mjs +1338 -0
- package/dist/claude-observation.mjs +1336 -0
- package/dist/cli.mjs +396 -29
- package/dist/codex-compatibility.mjs +467 -0
- package/dist/codex-diagnostics.mjs +426 -0
- package/dist/codex-permissions.mjs +590 -0
- package/dist/codex-protocol-fingerprint.json +131 -0
- package/dist/context-telemetry.mjs +257 -0
- package/dist/launch.mjs +369 -23
- package/dist/mcp.mjs +577 -5
- package/dist/monitor.mjs +61 -1
- package/dist/package.json +1 -1
- package/dist/platform.mjs +33 -3
- package/dist/policy.mjs +218 -0
- package/dist/state.mjs +132 -25
- package/dist/telemetry-report.mjs +133 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
69
|
-
let
|
|
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
|
-
|
|
102
|
-
|
|
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
|
|
106
|
-
|
|
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,
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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 = () =>
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
183
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
599
|
+
} else {
|
|
600
|
+
connection.startedOperation = true;
|
|
601
|
+
connection.handoffEligible = false;
|
|
218
602
|
if (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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
|
|