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