@raysonmeng/agentbridge 0.1.4 → 0.1.6
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/.claude-plugin/marketplace.json +1 -1
- package/README.md +2 -3
- package/README.zh-CN.md +2 -3
- package/dist/cli.js +349 -90
- package/package.json +3 -2
- package/plugins/agentbridge/.claude-plugin/plugin.json +1 -1
- package/plugins/agentbridge/server/bridge-server.js +156 -118
- package/plugins/agentbridge/server/daemon.js +639 -176
- package/scripts/postinstall.cjs +44 -4
|
@@ -10,6 +10,58 @@ import { createInterface } from "readline";
|
|
|
10
10
|
import { EventEmitter } from "events";
|
|
11
11
|
import { appendFileSync } from "fs";
|
|
12
12
|
|
|
13
|
+
// src/state-dir.ts
|
|
14
|
+
import { mkdirSync, existsSync } from "fs";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
import { homedir, platform } from "os";
|
|
17
|
+
|
|
18
|
+
class StateDirResolver {
|
|
19
|
+
stateDir;
|
|
20
|
+
constructor(envOverride) {
|
|
21
|
+
const override = envOverride ?? process.env.AGENTBRIDGE_STATE_DIR;
|
|
22
|
+
if (override) {
|
|
23
|
+
this.stateDir = override;
|
|
24
|
+
} else if (platform() === "darwin") {
|
|
25
|
+
this.stateDir = join(homedir(), "Library", "Application Support", "AgentBridge");
|
|
26
|
+
} else {
|
|
27
|
+
const xdgState = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
|
|
28
|
+
this.stateDir = join(xdgState, "agentbridge");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
ensure() {
|
|
32
|
+
if (!existsSync(this.stateDir)) {
|
|
33
|
+
mkdirSync(this.stateDir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
get dir() {
|
|
37
|
+
return this.stateDir;
|
|
38
|
+
}
|
|
39
|
+
get pidFile() {
|
|
40
|
+
return join(this.stateDir, "daemon.pid");
|
|
41
|
+
}
|
|
42
|
+
get tuiPidFile() {
|
|
43
|
+
return join(this.stateDir, "codex-tui.pid");
|
|
44
|
+
}
|
|
45
|
+
get lockFile() {
|
|
46
|
+
return join(this.stateDir, "daemon.lock");
|
|
47
|
+
}
|
|
48
|
+
get statusFile() {
|
|
49
|
+
return join(this.stateDir, "status.json");
|
|
50
|
+
}
|
|
51
|
+
get portsFile() {
|
|
52
|
+
return join(this.stateDir, "ports.json");
|
|
53
|
+
}
|
|
54
|
+
get logFile() {
|
|
55
|
+
return join(this.stateDir, "agentbridge.log");
|
|
56
|
+
}
|
|
57
|
+
get codexWrapperLogFile() {
|
|
58
|
+
return join(this.stateDir, "codex-wrapper.log");
|
|
59
|
+
}
|
|
60
|
+
get killedFile() {
|
|
61
|
+
return join(this.stateDir, "killed");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
13
65
|
// src/app-server-protocol.ts
|
|
14
66
|
var APP_SERVER_TRACKED_REQUEST_METHODS = [
|
|
15
67
|
"thread/start",
|
|
@@ -54,8 +106,6 @@ function isAppServerResponseMessage(value) {
|
|
|
54
106
|
}
|
|
55
107
|
|
|
56
108
|
// src/codex-adapter.ts
|
|
57
|
-
var LOG_FILE = "/tmp/agentbridge.log";
|
|
58
|
-
|
|
59
109
|
class CodexAdapter extends EventEmitter {
|
|
60
110
|
static RESPONSE_TRACKING_TTL_MS = 30000;
|
|
61
111
|
proc = null;
|
|
@@ -66,7 +116,10 @@ class CodexAdapter extends EventEmitter {
|
|
|
66
116
|
nextInjectionId = -1;
|
|
67
117
|
appPort;
|
|
68
118
|
proxyPort;
|
|
119
|
+
logFile;
|
|
69
120
|
tuiConnId = 0;
|
|
121
|
+
connIdCounter = 0;
|
|
122
|
+
secondaryConnections = new Map;
|
|
70
123
|
agentMessageBuffers = new Map;
|
|
71
124
|
pendingRequests = new Map;
|
|
72
125
|
activeTurnIds = new Set;
|
|
@@ -74,15 +127,29 @@ class CodexAdapter extends EventEmitter {
|
|
|
74
127
|
nextProxyId = 1e5;
|
|
75
128
|
upstreamToClient = new Map;
|
|
76
129
|
serverRequestToProxy = new Map;
|
|
77
|
-
serverRequestTtlTimers = new Map;
|
|
78
130
|
pendingServerRequests = [];
|
|
131
|
+
pendingServerResponses = new Map;
|
|
79
132
|
staleProxyIds = new Map;
|
|
80
133
|
bridgeRequestIds = new Map;
|
|
81
134
|
intentionalDisconnect = false;
|
|
82
|
-
|
|
135
|
+
pendingTuiMessages = [];
|
|
136
|
+
reconnectingForNewSession = false;
|
|
137
|
+
replayingBufferedMessages = false;
|
|
138
|
+
appServerGeneration = 0;
|
|
139
|
+
outageQueue = [];
|
|
140
|
+
outageTimer = null;
|
|
141
|
+
static OUTAGE_QUEUE_MAX = 64;
|
|
142
|
+
static OUTAGE_TIMEOUT_MS = 5000;
|
|
143
|
+
lastInitializeRaw = null;
|
|
144
|
+
lastInitializedRaw = null;
|
|
145
|
+
sessionRestoreInProgress = false;
|
|
146
|
+
replayPending = new Map;
|
|
147
|
+
static SESSION_REPLAY_TIMEOUT_MS = 5000;
|
|
148
|
+
constructor(appPort = 4500, proxyPort = 4501, logFile = new StateDirResolver().logFile) {
|
|
83
149
|
super();
|
|
84
150
|
this.appPort = appPort;
|
|
85
151
|
this.proxyPort = proxyPort;
|
|
152
|
+
this.logFile = logFile;
|
|
86
153
|
}
|
|
87
154
|
get appServerUrl() {
|
|
88
155
|
return `ws://127.0.0.1:${this.appPort}`;
|
|
@@ -117,8 +184,16 @@ class CodexAdapter extends EventEmitter {
|
|
|
117
184
|
clearTimeout(this.reconnectTimer);
|
|
118
185
|
this.reconnectTimer = null;
|
|
119
186
|
}
|
|
187
|
+
this.outageQueue = [];
|
|
188
|
+
this.clearOutageTimer();
|
|
120
189
|
this.appServerWs?.close();
|
|
121
190
|
this.appServerWs = null;
|
|
191
|
+
for (const [id, sec] of this.secondaryConnections) {
|
|
192
|
+
try {
|
|
193
|
+
sec.appServerWs?.close();
|
|
194
|
+
} catch {}
|
|
195
|
+
this.secondaryConnections.delete(id);
|
|
196
|
+
}
|
|
122
197
|
this.proxyServer?.stop();
|
|
123
198
|
this.proxyServer = null;
|
|
124
199
|
this.clearResponseTrackingState();
|
|
@@ -179,16 +254,32 @@ class CodexAdapter extends EventEmitter {
|
|
|
179
254
|
throw new Error("Codex app-server failed to become healthy");
|
|
180
255
|
}
|
|
181
256
|
connectToAppServer(isReconnect = false) {
|
|
257
|
+
const generation = ++this.appServerGeneration;
|
|
182
258
|
return new Promise((resolve, reject) => {
|
|
183
259
|
const appWs = new WebSocket(this.appServerUrl);
|
|
184
260
|
appWs.onopen = () => {
|
|
261
|
+
if (this.appServerGeneration !== generation) {
|
|
262
|
+
appWs.close();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
185
265
|
this.appServerWs = appWs;
|
|
186
266
|
this.intentionalDisconnect = false;
|
|
187
267
|
this.reconnectAttempts = 0;
|
|
188
|
-
this.log(isReconnect ? "Reconnected to app-server" : "Connected to app-server
|
|
268
|
+
this.log(isReconnect ? "Reconnected to app-server" : "Connected to app-server");
|
|
269
|
+
this.flushPendingServerResponses();
|
|
270
|
+
if (isReconnect) {
|
|
271
|
+
this.handleSessionRestoreAfterReconnect().finally(() => this.drainOutageQueue()).catch((e) => {
|
|
272
|
+
const m = e instanceof Error ? e.message : String(e);
|
|
273
|
+
this.log(`session restore unexpected error: ${m}`);
|
|
274
|
+
});
|
|
275
|
+
} else {
|
|
276
|
+
this.drainOutageQueue();
|
|
277
|
+
}
|
|
189
278
|
resolve();
|
|
190
279
|
};
|
|
191
280
|
appWs.onmessage = (event) => {
|
|
281
|
+
if (this.appServerGeneration !== generation)
|
|
282
|
+
return;
|
|
192
283
|
const data = typeof event.data === "string" ? event.data : event.data.toString();
|
|
193
284
|
const forwarded = this.handleAppServerPayload(data);
|
|
194
285
|
if (forwarded === null)
|
|
@@ -204,22 +295,58 @@ class CodexAdapter extends EventEmitter {
|
|
|
204
295
|
}
|
|
205
296
|
};
|
|
206
297
|
appWs.onerror = () => {
|
|
298
|
+
if (this.appServerGeneration !== generation)
|
|
299
|
+
return;
|
|
207
300
|
this.log("App-server connection error");
|
|
208
301
|
if (!isReconnect)
|
|
209
302
|
reject(new Error("Failed to connect to app-server"));
|
|
210
303
|
};
|
|
211
304
|
appWs.onclose = () => {
|
|
212
|
-
this.
|
|
213
|
-
|
|
214
|
-
this.
|
|
215
|
-
this.activeTurnIds.clear();
|
|
216
|
-
this.turnInProgress = false;
|
|
217
|
-
if (!this.intentionalDisconnect) {
|
|
218
|
-
this.scheduleReconnect();
|
|
219
|
-
}
|
|
305
|
+
if (this.appServerGeneration !== generation)
|
|
306
|
+
return;
|
|
307
|
+
this.handleAppServerClose();
|
|
220
308
|
};
|
|
221
309
|
});
|
|
222
310
|
}
|
|
311
|
+
async reconnectAppServerForNewSession(tuiWs) {
|
|
312
|
+
this.appServerGeneration++;
|
|
313
|
+
this.intentionalDisconnect = true;
|
|
314
|
+
if (this.reconnectTimer) {
|
|
315
|
+
clearTimeout(this.reconnectTimer);
|
|
316
|
+
this.reconnectTimer = null;
|
|
317
|
+
}
|
|
318
|
+
const oldWs = this.appServerWs;
|
|
319
|
+
this.appServerWs = null;
|
|
320
|
+
if (oldWs) {
|
|
321
|
+
try {
|
|
322
|
+
oldWs.close();
|
|
323
|
+
} catch {}
|
|
324
|
+
}
|
|
325
|
+
this.clearResponseTrackingStateForAppServerReconnect();
|
|
326
|
+
this.activeTurnIds.clear();
|
|
327
|
+
this.turnInProgress = false;
|
|
328
|
+
try {
|
|
329
|
+
await this.connectToAppServer(false);
|
|
330
|
+
this.log("App-server reconnected for new TUI session \u2014 replaying buffered messages");
|
|
331
|
+
const messages = this.pendingTuiMessages;
|
|
332
|
+
this.pendingTuiMessages = [];
|
|
333
|
+
this.reconnectingForNewSession = false;
|
|
334
|
+
this.replayingBufferedMessages = true;
|
|
335
|
+
try {
|
|
336
|
+
for (const msg of messages) {
|
|
337
|
+
this.onTuiMessage(tuiWs, msg);
|
|
338
|
+
}
|
|
339
|
+
} finally {
|
|
340
|
+
this.replayingBufferedMessages = false;
|
|
341
|
+
}
|
|
342
|
+
} catch (err) {
|
|
343
|
+
this.log(`Failed to reconnect app-server for new session: ${err.message}`);
|
|
344
|
+
this.pendingTuiMessages = [];
|
|
345
|
+
this.reconnectingForNewSession = false;
|
|
346
|
+
this.intentionalDisconnect = false;
|
|
347
|
+
this.scheduleReconnect();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
223
350
|
reconnectAttempts = 0;
|
|
224
351
|
reconnectTimer = null;
|
|
225
352
|
static MAX_RECONNECT_ATTEMPTS = 10;
|
|
@@ -245,6 +372,176 @@ class CodexAdapter extends EventEmitter {
|
|
|
245
372
|
}
|
|
246
373
|
}, delay);
|
|
247
374
|
}
|
|
375
|
+
handleAppServerClose() {
|
|
376
|
+
const intentional = this.intentionalDisconnect;
|
|
377
|
+
const tuiConnected = this.tuiWs !== null;
|
|
378
|
+
this.log(`App-server connection closed (intentional=${intentional}, tuiConnected=${tuiConnected}, turnInProgress=${this.turnInProgress})`);
|
|
379
|
+
this.appServerWs = null;
|
|
380
|
+
this.clearResponseTrackingState();
|
|
381
|
+
this.activeTurnIds.clear();
|
|
382
|
+
this.turnInProgress = false;
|
|
383
|
+
if (!intentional) {
|
|
384
|
+
this.scheduleReconnect();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
bufferDuringOutage(ws, raw) {
|
|
388
|
+
if (this.outageQueue.length >= CodexAdapter.OUTAGE_QUEUE_MAX) {
|
|
389
|
+
this.log(`ERROR: outage queue overflow (${this.outageQueue.length}/${CodexAdapter.OUTAGE_QUEUE_MAX}) \u2014 closing TUI with 1011`);
|
|
390
|
+
this.outageQueue = [];
|
|
391
|
+
this.clearOutageTimer();
|
|
392
|
+
if (this.tuiWs && this.tuiWs === ws) {
|
|
393
|
+
try {
|
|
394
|
+
ws.close(1011, "agentbridge: app-server unavailable; pending TUI queue overflow");
|
|
395
|
+
} catch (e) {
|
|
396
|
+
this.log(`Failed to close TUI WS after outage queue overflow: ${e.message}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
this.outageQueue.push({ raw, connId: ws.data.connId });
|
|
402
|
+
this.log(`DIAGNOSTIC: buffered TUI message while app-server unavailable (queue size=${this.outageQueue.length}/${CodexAdapter.OUTAGE_QUEUE_MAX})`);
|
|
403
|
+
this.ensureOutageTimer();
|
|
404
|
+
}
|
|
405
|
+
ensureOutageTimer() {
|
|
406
|
+
if (this.outageTimer !== null)
|
|
407
|
+
return;
|
|
408
|
+
this.outageTimer = setTimeout(() => {
|
|
409
|
+
this.outageTimer = null;
|
|
410
|
+
const buffered = this.outageQueue.length;
|
|
411
|
+
this.outageQueue = [];
|
|
412
|
+
this.log(`ERROR: app-server did not return within ${CodexAdapter.OUTAGE_TIMEOUT_MS}ms (buffered=${buffered}) \u2014 closing TUI with 1011`);
|
|
413
|
+
const ws = this.tuiWs;
|
|
414
|
+
if (ws) {
|
|
415
|
+
try {
|
|
416
|
+
ws.close(1011, `agentbridge: app-server unavailable after ${CodexAdapter.OUTAGE_TIMEOUT_MS}ms; buffered=${buffered}`);
|
|
417
|
+
} catch (e) {
|
|
418
|
+
this.log(`Failed to close TUI WS on outage timeout: ${e.message}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}, CodexAdapter.OUTAGE_TIMEOUT_MS);
|
|
422
|
+
}
|
|
423
|
+
clearOutageTimer() {
|
|
424
|
+
if (this.outageTimer !== null) {
|
|
425
|
+
clearTimeout(this.outageTimer);
|
|
426
|
+
this.outageTimer = null;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async handleSessionRestoreAfterReconnect() {
|
|
430
|
+
if (!this.lastInitializeRaw) {
|
|
431
|
+
this.log("DIAGNOSTIC: no cached initialize to replay after unintentional reconnect");
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (!this.appServerWs || this.appServerWs.readyState !== WebSocket.OPEN) {
|
|
435
|
+
this.log("DIAGNOSTIC: app-server not open at session restore start \u2014 skipping");
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
this.sessionRestoreInProgress = true;
|
|
439
|
+
try {
|
|
440
|
+
this.log(`DIAGNOSTIC: replaying cached initialize to restore session (threadId=${this.threadId ?? "none"})`);
|
|
441
|
+
await this.sendReplayAndAwait(this.lastInitializeRaw, "initialize");
|
|
442
|
+
if (this.lastInitializedRaw && this.appServerWs.readyState === WebSocket.OPEN) {
|
|
443
|
+
this.appServerWs.send(this.lastInitializedRaw);
|
|
444
|
+
}
|
|
445
|
+
if (this.threadId && this.appServerWs.readyState === WebSocket.OPEN) {
|
|
446
|
+
const replayId = `agentbridge-replay-thread-resume-${Date.now()}`;
|
|
447
|
+
const resumeRaw = JSON.stringify({
|
|
448
|
+
jsonrpc: "2.0",
|
|
449
|
+
id: replayId,
|
|
450
|
+
method: "thread/resume",
|
|
451
|
+
params: { threadId: this.threadId }
|
|
452
|
+
});
|
|
453
|
+
await this.sendReplayAndAwait(resumeRaw, "thread/resume");
|
|
454
|
+
}
|
|
455
|
+
this.log(`DIAGNOSTIC: session restored after unintentional reconnect (threadId=${this.threadId ?? "none"})`);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
458
|
+
this.log(`ERROR: session restore failed (${msg}) \u2014 closing TUI with 1011`);
|
|
459
|
+
const tuiWs = this.tuiWs;
|
|
460
|
+
if (tuiWs) {
|
|
461
|
+
try {
|
|
462
|
+
tuiWs.close(1011, `agentbridge: session restore failed: ${msg}`);
|
|
463
|
+
} catch (closeErr) {
|
|
464
|
+
const cm = closeErr instanceof Error ? closeErr.message : String(closeErr);
|
|
465
|
+
this.log(`Failed to close TUI after session restore failure: ${cm}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
} finally {
|
|
469
|
+
this.sessionRestoreInProgress = false;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
sendReplayAndAwait(raw, method) {
|
|
473
|
+
if (!this.appServerWs || this.appServerWs.readyState !== WebSocket.OPEN) {
|
|
474
|
+
return Promise.reject(new Error("app-server not open"));
|
|
475
|
+
}
|
|
476
|
+
let id;
|
|
477
|
+
try {
|
|
478
|
+
const parsed = JSON.parse(raw);
|
|
479
|
+
if (parsed.id === undefined) {
|
|
480
|
+
return Promise.reject(new Error(`replay payload for ${method} has no id`));
|
|
481
|
+
}
|
|
482
|
+
id = parsed.id;
|
|
483
|
+
} catch (e) {
|
|
484
|
+
const m = e instanceof Error ? e.message : String(e);
|
|
485
|
+
return Promise.reject(new Error(`replay parse failed for ${method}: ${m}`));
|
|
486
|
+
}
|
|
487
|
+
return new Promise((resolve, reject) => {
|
|
488
|
+
const timer = setTimeout(() => {
|
|
489
|
+
this.replayPending.delete(id);
|
|
490
|
+
reject(new Error(`replay timeout (${CodexAdapter.SESSION_REPLAY_TIMEOUT_MS}ms) for ${method} id=${JSON.stringify(id)}`));
|
|
491
|
+
}, CodexAdapter.SESSION_REPLAY_TIMEOUT_MS);
|
|
492
|
+
this.replayPending.set(id, { method, resolve, reject, timer });
|
|
493
|
+
try {
|
|
494
|
+
this.appServerWs.send(raw);
|
|
495
|
+
} catch (e) {
|
|
496
|
+
clearTimeout(timer);
|
|
497
|
+
this.replayPending.delete(id);
|
|
498
|
+
const m = e instanceof Error ? e.message : String(e);
|
|
499
|
+
reject(new Error(`replay send failed for ${method}: ${m}`));
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
tryConsumeReplayResponse(payload) {
|
|
504
|
+
const id = payload.id;
|
|
505
|
+
if (id === undefined)
|
|
506
|
+
return false;
|
|
507
|
+
const pending = this.replayPending.get(id);
|
|
508
|
+
if (!pending)
|
|
509
|
+
return false;
|
|
510
|
+
clearTimeout(pending.timer);
|
|
511
|
+
this.replayPending.delete(id);
|
|
512
|
+
if (payload.error !== undefined) {
|
|
513
|
+
const errMsg = typeof payload.error === "object" && payload.error !== null && "message" in payload.error ? String(payload.error.message ?? "unknown") : JSON.stringify(payload.error);
|
|
514
|
+
pending.reject(new Error(`${pending.method} rejected: ${errMsg}`));
|
|
515
|
+
} else {
|
|
516
|
+
pending.resolve(payload);
|
|
517
|
+
}
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
drainOutageQueue() {
|
|
521
|
+
if (this.outageQueue.length === 0) {
|
|
522
|
+
this.clearOutageTimer();
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (!this.appServerWs || this.appServerWs.readyState !== WebSocket.OPEN)
|
|
526
|
+
return;
|
|
527
|
+
const ws = this.tuiWs;
|
|
528
|
+
if (!ws) {
|
|
529
|
+
this.outageQueue = [];
|
|
530
|
+
this.clearOutageTimer();
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const messages = this.outageQueue;
|
|
534
|
+
this.outageQueue = [];
|
|
535
|
+
this.clearOutageTimer();
|
|
536
|
+
this.log(`DIAGNOSTIC: replaying ${messages.length} buffered TUI messages after app-server reconnect`);
|
|
537
|
+
for (const msg of messages) {
|
|
538
|
+
try {
|
|
539
|
+
this.onTuiMessage(ws, msg.raw);
|
|
540
|
+
} catch (e) {
|
|
541
|
+
this.log(`Failed to replay buffered TUI message (conn #${msg.connId}): ${e.message}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
248
545
|
startProxy() {
|
|
249
546
|
const self = this;
|
|
250
547
|
this.proxyServer = Bun.serve({
|
|
@@ -252,40 +549,109 @@ class CodexAdapter extends EventEmitter {
|
|
|
252
549
|
hostname: "127.0.0.1",
|
|
253
550
|
fetch(req, server) {
|
|
254
551
|
const url = new URL(req.url);
|
|
552
|
+
const isUpgrade = req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
553
|
+
self.log(`HTTP ${req.method} ${url.pathname} (upgrade=${isUpgrade})`);
|
|
255
554
|
if (url.pathname === "/healthz" || url.pathname === "/readyz") {
|
|
256
555
|
return fetch(`http://127.0.0.1:${self.appPort}${url.pathname}`);
|
|
257
556
|
}
|
|
258
557
|
if (server.upgrade(req, { data: { connId: 0 } }))
|
|
259
558
|
return;
|
|
559
|
+
self.log(`WARNING: non-upgrade HTTP request not handled: ${req.method} ${url.pathname}`);
|
|
260
560
|
return new Response("AgentBridge Codex Proxy");
|
|
261
561
|
},
|
|
262
562
|
websocket: {
|
|
263
563
|
open: (ws) => self.onTuiConnect(ws),
|
|
264
|
-
close: (ws) =>
|
|
564
|
+
close: (ws, code, reason) => {
|
|
565
|
+
self.log(`WebSocket close event: conn #${ws.data.connId}, code=${code}, reason=${reason || "none"}`);
|
|
566
|
+
self.onTuiDisconnect(ws);
|
|
567
|
+
},
|
|
265
568
|
message: (ws, msg) => self.onTuiMessage(ws, msg)
|
|
266
569
|
}
|
|
267
570
|
});
|
|
268
571
|
}
|
|
269
572
|
onTuiConnect(ws) {
|
|
270
|
-
this.
|
|
271
|
-
ws.data.connId =
|
|
573
|
+
const connId = ++this.connIdCounter;
|
|
574
|
+
ws.data.connId = connId;
|
|
575
|
+
if (this.tuiWs) {
|
|
576
|
+
this.log(`Secondary TUI connected (conn #${connId}, primary is #${this.tuiConnId})`);
|
|
577
|
+
this.setupSecondaryConnection(ws, connId);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const previousConnId = this.tuiConnId > 0 ? this.tuiConnId : null;
|
|
581
|
+
this.tuiConnId = connId;
|
|
272
582
|
this.tuiWs = ws;
|
|
583
|
+
this.threadId = null;
|
|
273
584
|
this.log(`TUI connected (conn #${this.tuiConnId})`);
|
|
274
585
|
this.emit("tuiConnected", this.tuiConnId);
|
|
586
|
+
if (previousConnId !== null) {
|
|
587
|
+
this.retireConnectionState(previousConnId);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
setupSecondaryConnection(ws, connId) {
|
|
591
|
+
const appWs = new WebSocket(this.appServerUrl);
|
|
592
|
+
const entry = { tuiWs: ws, appServerWs: appWs, buffer: [] };
|
|
593
|
+
this.secondaryConnections.set(connId, entry);
|
|
594
|
+
appWs.onopen = () => {
|
|
595
|
+
if (!this.secondaryConnections.has(connId)) {
|
|
596
|
+
appWs.close();
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
this.log(`Secondary conn #${connId}: app-server WS connected, flushing ${entry.buffer.length} buffered messages`);
|
|
600
|
+
for (const msg of entry.buffer) {
|
|
601
|
+
try {
|
|
602
|
+
appWs.send(msg);
|
|
603
|
+
} catch {}
|
|
604
|
+
}
|
|
605
|
+
entry.buffer = [];
|
|
606
|
+
};
|
|
607
|
+
appWs.onmessage = (event) => {
|
|
608
|
+
if (!this.secondaryConnections.has(connId))
|
|
609
|
+
return;
|
|
610
|
+
const data = typeof event.data === "string" ? event.data : event.data.toString();
|
|
611
|
+
try {
|
|
612
|
+
ws.send(data);
|
|
613
|
+
} catch {}
|
|
614
|
+
};
|
|
615
|
+
appWs.onerror = () => {
|
|
616
|
+
this.log(`Secondary conn #${connId}: app-server WS error`);
|
|
617
|
+
};
|
|
618
|
+
appWs.onclose = () => {
|
|
619
|
+
this.log(`Secondary conn #${connId}: app-server WS closed`);
|
|
620
|
+
const sec = this.secondaryConnections.get(connId);
|
|
621
|
+
if (sec) {
|
|
622
|
+
this.secondaryConnections.delete(connId);
|
|
623
|
+
try {
|
|
624
|
+
sec.tuiWs.close();
|
|
625
|
+
} catch {}
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
replayPendingForThread(resumedThreadId, ws) {
|
|
275
630
|
const remaining = [];
|
|
276
631
|
for (const buffered of this.pendingServerRequests) {
|
|
632
|
+
const belongsToThread = buffered.threadId === null || buffered.threadId === resumedThreadId;
|
|
633
|
+
if (!belongsToThread) {
|
|
634
|
+
remaining.push(buffered);
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
277
637
|
const proxyId = this.nextProxyId++;
|
|
278
638
|
try {
|
|
279
639
|
const parsed = JSON.parse(buffered.raw);
|
|
280
640
|
parsed.id = proxyId;
|
|
281
641
|
ws.send(JSON.stringify(parsed));
|
|
282
642
|
this.serverRequestToProxy.set(proxyId, {
|
|
643
|
+
raw: buffered.raw,
|
|
283
644
|
serverId: buffered.serverId,
|
|
284
645
|
connId: this.tuiConnId,
|
|
285
646
|
method: buffered.method,
|
|
286
|
-
timestamp: Date.now()
|
|
647
|
+
timestamp: Date.now(),
|
|
648
|
+
threadId: buffered.threadId
|
|
287
649
|
});
|
|
288
|
-
|
|
650
|
+
if (buffered.threadId === null) {
|
|
651
|
+
this.log(`WARNING: Replaying pending server request with unknown threadId (experimental fallback, may surface orphan UI on wrong thread): ${buffered.method} (server id=${buffered.serverId} \u2192 proxy id=${proxyId})`);
|
|
652
|
+
} else {
|
|
653
|
+
this.log(`Replayed buffered server request on thread/resume: ${buffered.method} (server id=${buffered.serverId} \u2192 proxy id=${proxyId}, threadId=${buffered.threadId})`);
|
|
654
|
+
}
|
|
289
655
|
} catch (e) {
|
|
290
656
|
this.log(`Failed to replay buffered server request: ${buffered.method} (server id=${buffered.serverId}): ${e.message}`);
|
|
291
657
|
remaining.push(buffered);
|
|
@@ -293,11 +659,47 @@ class CodexAdapter extends EventEmitter {
|
|
|
293
659
|
}
|
|
294
660
|
this.pendingServerRequests = remaining;
|
|
295
661
|
}
|
|
662
|
+
dropOrphanPendingRequests(reason, matchThreadId = null) {
|
|
663
|
+
if (this.pendingServerRequests.length === 0)
|
|
664
|
+
return;
|
|
665
|
+
const remaining = [];
|
|
666
|
+
for (const buffered of this.pendingServerRequests) {
|
|
667
|
+
const shouldDrop = matchThreadId === null ? true : buffered.threadId !== null && buffered.threadId !== matchThreadId;
|
|
668
|
+
if (shouldDrop) {
|
|
669
|
+
this.log(`Dropped orphan pending server request: ${buffered.method} (server id=${buffered.serverId}, threadId=${buffered.threadId ?? "unknown"}, reason=${reason})`);
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
remaining.push(buffered);
|
|
673
|
+
}
|
|
674
|
+
this.pendingServerRequests = remaining;
|
|
675
|
+
}
|
|
296
676
|
onTuiDisconnect(ws) {
|
|
297
677
|
const connId = ws.data.connId;
|
|
678
|
+
const secondary = this.secondaryConnections.get(connId);
|
|
679
|
+
if (secondary) {
|
|
680
|
+
this.log(`Secondary TUI disconnected (conn #${connId})`);
|
|
681
|
+
this.secondaryConnections.delete(connId);
|
|
682
|
+
if (secondary.appServerWs) {
|
|
683
|
+
try {
|
|
684
|
+
secondary.appServerWs.close();
|
|
685
|
+
} catch {}
|
|
686
|
+
}
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
298
689
|
if (this.tuiWs === ws) {
|
|
299
|
-
this.
|
|
690
|
+
const appServerOpen = this.appServerWs?.readyState === WebSocket.OPEN;
|
|
691
|
+
this.log(`TUI disconnected (conn #${connId}, appServerOpen=${appServerOpen}, turnInProgress=${this.turnInProgress}, pendingTuiMessages=${this.pendingTuiMessages.length}, outageQueue=${this.outageQueue.length}, reconnectingForNewSession=${this.reconnectingForNewSession})`);
|
|
300
692
|
this.tuiWs = null;
|
|
693
|
+
if (this.reconnectingForNewSession) {
|
|
694
|
+
this.log("Clearing pending TUI message buffer (TUI disconnected during app-server reconnect)");
|
|
695
|
+
this.pendingTuiMessages = [];
|
|
696
|
+
this.reconnectingForNewSession = false;
|
|
697
|
+
}
|
|
698
|
+
if (this.outageQueue.length > 0 || this.outageTimer !== null) {
|
|
699
|
+
this.log(`Clearing outage queue on TUI disconnect (buffered=${this.outageQueue.length})`);
|
|
700
|
+
this.outageQueue = [];
|
|
701
|
+
this.clearOutageTimer();
|
|
702
|
+
}
|
|
301
703
|
this.emit("tuiDisconnected", connId);
|
|
302
704
|
} else {
|
|
303
705
|
this.log(`Stale TUI disconnected (conn #${connId}, current is #${this.tuiConnId})`);
|
|
@@ -307,6 +709,17 @@ class CodexAdapter extends EventEmitter {
|
|
|
307
709
|
onTuiMessage(ws, msg) {
|
|
308
710
|
const data = typeof msg === "string" ? msg : msg.toString();
|
|
309
711
|
const connId = ws.data.connId;
|
|
712
|
+
const secondary = this.secondaryConnections.get(connId);
|
|
713
|
+
if (secondary) {
|
|
714
|
+
if (secondary.appServerWs && secondary.appServerWs.readyState === WebSocket.OPEN) {
|
|
715
|
+
try {
|
|
716
|
+
secondary.appServerWs.send(data);
|
|
717
|
+
} catch {}
|
|
718
|
+
} else {
|
|
719
|
+
secondary.buffer.push(data);
|
|
720
|
+
}
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
310
723
|
if (connId !== this.tuiConnId) {
|
|
311
724
|
this.log(`Dropping message from stale TUI conn #${connId} (current is #${this.tuiConnId})`);
|
|
312
725
|
return;
|
|
@@ -315,29 +728,63 @@ class CodexAdapter extends EventEmitter {
|
|
|
315
728
|
const parsed = JSON.parse(data);
|
|
316
729
|
if (parsed.id !== undefined && !parsed.method) {
|
|
317
730
|
const normalizedId = this.normalizeNumericId(parsed.id);
|
|
731
|
+
if (!isNaN(normalizedId) && this.pendingServerResponses.has(normalizedId)) {
|
|
732
|
+
this.log(`Ignoring duplicate approval response while app-server reconnect is pending (proxy id=${normalizedId})`);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
318
735
|
const pending = !isNaN(normalizedId) ? this.serverRequestToProxy.get(normalizedId) : undefined;
|
|
319
736
|
if (pending !== undefined) {
|
|
320
737
|
if (pending.connId !== connId) {
|
|
321
738
|
this.log(`Dropping stale server request response (proxy id=${normalizedId}, expected conn #${pending.connId}, got #${connId})`);
|
|
322
739
|
return;
|
|
323
740
|
}
|
|
741
|
+
parsed.id = pending.serverId;
|
|
742
|
+
const forwardedResponse = JSON.stringify(parsed);
|
|
324
743
|
if (!this.appServerWs || this.appServerWs.readyState !== WebSocket.OPEN) {
|
|
325
|
-
this.
|
|
744
|
+
this.bufferPendingServerResponse(normalizedId, pending, forwardedResponse, "app-server disconnected");
|
|
326
745
|
return;
|
|
327
746
|
}
|
|
328
|
-
parsed.id = pending.serverId;
|
|
329
747
|
try {
|
|
330
|
-
this.appServerWs.send(
|
|
748
|
+
this.appServerWs.send(forwardedResponse);
|
|
331
749
|
this.serverRequestToProxy.delete(normalizedId);
|
|
332
750
|
this.log(`TUI \u2192 app-server: ${pending.method} response (proxy id=${normalizedId} \u2192 server id=${pending.serverId})`);
|
|
333
751
|
} catch (e) {
|
|
334
|
-
|
|
335
|
-
this.log(`Failed to forward approval response (proxy id=${normalizedId}): ${e.message}`);
|
|
752
|
+
this.bufferPendingServerResponse(normalizedId, pending, forwardedResponse, `send failed: ${e.message}`);
|
|
336
753
|
}
|
|
337
754
|
return;
|
|
338
755
|
}
|
|
339
756
|
}
|
|
340
757
|
} catch {}
|
|
758
|
+
let detectedMethod;
|
|
759
|
+
try {
|
|
760
|
+
const parsed = JSON.parse(data);
|
|
761
|
+
detectedMethod = typeof parsed.method === "string" ? parsed.method : undefined;
|
|
762
|
+
} catch {}
|
|
763
|
+
if (!this.replayingBufferedMessages) {
|
|
764
|
+
if (detectedMethod === "initialize") {
|
|
765
|
+
this.lastInitializeRaw = data;
|
|
766
|
+
this.log("Detected initialize \u2014 reconnecting app-server for fresh session");
|
|
767
|
+
this.reconnectingForNewSession = true;
|
|
768
|
+
this.pendingTuiMessages = [data];
|
|
769
|
+
this.reconnectAppServerForNewSession(ws);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (this.reconnectingForNewSession) {
|
|
773
|
+
this.pendingTuiMessages.push(data);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (detectedMethod === "initialized") {
|
|
778
|
+
this.lastInitializedRaw = data;
|
|
779
|
+
}
|
|
780
|
+
if (!this.appServerWs || this.appServerWs.readyState !== WebSocket.OPEN || this.sessionRestoreInProgress) {
|
|
781
|
+
if (this.tuiWs && this.tuiWs === ws) {
|
|
782
|
+
this.bufferDuringOutage(ws, data);
|
|
783
|
+
} else {
|
|
784
|
+
this.log(`WARNING: non-primary TUI attempted to send while app-server down \u2014 dropped (connId=${connId})`);
|
|
785
|
+
}
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
341
788
|
let forwarded = data;
|
|
342
789
|
try {
|
|
343
790
|
const parsed = JSON.parse(data);
|
|
@@ -358,14 +805,24 @@ class CodexAdapter extends EventEmitter {
|
|
|
358
805
|
if (this.appServerWs?.readyState === WebSocket.OPEN) {
|
|
359
806
|
this.appServerWs.send(forwarded);
|
|
360
807
|
} else {
|
|
361
|
-
this.log(`WARNING: app-server
|
|
808
|
+
this.log(`WARNING: app-server closed between OPEN check and send \u2014 message lost (connId=${ws.data.connId})`);
|
|
362
809
|
}
|
|
363
810
|
}
|
|
364
811
|
handleAppServerPayload(raw) {
|
|
365
812
|
try {
|
|
366
813
|
const parsed = JSON.parse(raw);
|
|
814
|
+
if (typeof parsed === "object" && parsed !== null && "id" in parsed) {
|
|
815
|
+
if (this.tryConsumeReplayResponse(parsed)) {
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
367
819
|
if (isAppServerNotification(parsed) || typeof parsed === "object" && parsed !== null && !("id" in parsed)) {
|
|
368
820
|
const notificationLike = parsed;
|
|
821
|
+
if (notificationLike.method === "thread/closed") {
|
|
822
|
+
const params = notificationLike.params;
|
|
823
|
+
const threadId = typeof params?.threadId === "string" ? params.threadId : "unknown";
|
|
824
|
+
this.log(`DIAGNOSTIC: app-server emitted thread/closed (threadId=${threadId}) \u2014 TUI will exit(0) silently`);
|
|
825
|
+
}
|
|
369
826
|
const forwarded = this.patchResponse(notificationLike, raw);
|
|
370
827
|
this.interceptServerMessage(notificationLike);
|
|
371
828
|
return forwarded;
|
|
@@ -386,9 +843,10 @@ class CodexAdapter extends EventEmitter {
|
|
|
386
843
|
handleServerRequest(parsed, raw) {
|
|
387
844
|
const serverId = parsed.id;
|
|
388
845
|
const method = parsed.method;
|
|
846
|
+
const threadId = this.extractThreadIdFromParams(parsed.params);
|
|
389
847
|
if (!this.tuiWs) {
|
|
390
|
-
this.pendingServerRequests.push({ raw, serverId, method });
|
|
391
|
-
this.log(`Server request buffered (no TUI): ${method} (server id=${serverId})`);
|
|
848
|
+
this.pendingServerRequests.push({ raw, serverId, method, threadId });
|
|
849
|
+
this.log(`Server request buffered (no TUI): ${method} (server id=${serverId}, threadId=${threadId ?? "unknown"})`);
|
|
392
850
|
return;
|
|
393
851
|
}
|
|
394
852
|
const proxyId = this.nextProxyId++;
|
|
@@ -397,11 +855,24 @@ class CodexAdapter extends EventEmitter {
|
|
|
397
855
|
this.tuiWs.send(JSON.stringify(parsed));
|
|
398
856
|
} catch (e) {
|
|
399
857
|
this.log(`Server request send failed, buffering: ${method} (server id=${serverId}): ${e.message}`);
|
|
400
|
-
this.pendingServerRequests.push({ raw, serverId, method });
|
|
858
|
+
this.pendingServerRequests.push({ raw, serverId, method, threadId });
|
|
401
859
|
return;
|
|
402
860
|
}
|
|
403
|
-
this.serverRequestToProxy.set(proxyId, {
|
|
404
|
-
|
|
861
|
+
this.serverRequestToProxy.set(proxyId, {
|
|
862
|
+
raw,
|
|
863
|
+
serverId,
|
|
864
|
+
connId: this.tuiConnId,
|
|
865
|
+
method,
|
|
866
|
+
timestamp: Date.now(),
|
|
867
|
+
threadId
|
|
868
|
+
});
|
|
869
|
+
this.log(`Server request: ${method} (server id=${serverId} \u2192 proxy id=${proxyId}, conn #${this.tuiConnId}, threadId=${threadId ?? "unknown"})`);
|
|
870
|
+
}
|
|
871
|
+
extractThreadIdFromParams(params) {
|
|
872
|
+
if (typeof params !== "object" || params === null)
|
|
873
|
+
return null;
|
|
874
|
+
const tid = params.threadId;
|
|
875
|
+
return typeof tid === "string" && tid.length > 0 ? tid : null;
|
|
405
876
|
}
|
|
406
877
|
normalizeNumericId(id) {
|
|
407
878
|
if (typeof id === "number")
|
|
@@ -410,6 +881,30 @@ class CodexAdapter extends EventEmitter {
|
|
|
410
881
|
return Number(id);
|
|
411
882
|
return NaN;
|
|
412
883
|
}
|
|
884
|
+
bufferPendingServerResponse(proxyId, pending, forwardedResponse, reason) {
|
|
885
|
+
this.pendingServerResponses.set(proxyId, {
|
|
886
|
+
raw: forwardedResponse,
|
|
887
|
+
serverId: pending.serverId,
|
|
888
|
+
method: pending.method,
|
|
889
|
+
timestamp: Date.now()
|
|
890
|
+
});
|
|
891
|
+
this.serverRequestToProxy.delete(proxyId);
|
|
892
|
+
this.log(`Buffered approval response until app-server reconnect (${reason}) (proxy id=${proxyId} \u2192 server id=${pending.serverId})`);
|
|
893
|
+
}
|
|
894
|
+
flushPendingServerResponses() {
|
|
895
|
+
if (!this.appServerWs || this.appServerWs.readyState !== WebSocket.OPEN)
|
|
896
|
+
return;
|
|
897
|
+
for (const [proxyId, pending] of this.pendingServerResponses.entries()) {
|
|
898
|
+
try {
|
|
899
|
+
this.appServerWs.send(pending.raw);
|
|
900
|
+
this.pendingServerResponses.delete(proxyId);
|
|
901
|
+
this.log(`Flushed buffered approval response after app-server reconnect (proxy id=${proxyId} \u2192 server id=${pending.serverId})`);
|
|
902
|
+
} catch (e) {
|
|
903
|
+
this.log(`Failed to flush buffered approval response (proxy id=${proxyId}): ${e.message}`);
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
413
908
|
handleAppServerResponse(parsed, raw) {
|
|
414
909
|
const responseId = parsed.id;
|
|
415
910
|
const numericId = this.normalizeNumericId(responseId);
|
|
@@ -421,6 +916,7 @@ class CodexAdapter extends EventEmitter {
|
|
|
421
916
|
return null;
|
|
422
917
|
}
|
|
423
918
|
parsed.id = mapping.clientId;
|
|
919
|
+
this.log(`app-server \u2192 TUI: response (proxy id=${numericId} \u2192 client id=${String(mapping.clientId)}, conn #${mapping.connId})`);
|
|
424
920
|
const forwarded = this.patchResponse(parsed, JSON.stringify(parsed));
|
|
425
921
|
this.interceptServerMessage(parsed, mapping.connId);
|
|
426
922
|
return forwarded;
|
|
@@ -460,17 +956,6 @@ class CodexAdapter extends EventEmitter {
|
|
|
460
956
|
}
|
|
461
957
|
});
|
|
462
958
|
}
|
|
463
|
-
if (errMsg.includes("Already initialized")) {
|
|
464
|
-
this.log(`Patching "Already initialized" error (id: ${parsed.id})`);
|
|
465
|
-
return JSON.stringify({
|
|
466
|
-
id: parsed.id,
|
|
467
|
-
result: {
|
|
468
|
-
userAgent: "agent_bridge/0.1.0",
|
|
469
|
-
platformFamily: "unix",
|
|
470
|
-
platformOs: "macos"
|
|
471
|
-
}
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
959
|
}
|
|
475
960
|
return raw;
|
|
476
961
|
}
|
|
@@ -544,7 +1029,6 @@ class CodexAdapter extends EventEmitter {
|
|
|
544
1029
|
const rpcId = "id" in message ? message.id : undefined;
|
|
545
1030
|
const method = "method" in message && typeof message.method === "string" ? message.method : undefined;
|
|
546
1031
|
const key = this.pendingKey(rpcId, connId);
|
|
547
|
-
this.log(`[track] method=${method} id=${rpcId} (type=${typeof rpcId}) key=${key}`);
|
|
548
1032
|
if (!key || !isTrackedAppServerRequestMethod(method))
|
|
549
1033
|
return;
|
|
550
1034
|
const pending = { method };
|
|
@@ -582,12 +1066,17 @@ class CodexAdapter extends EventEmitter {
|
|
|
582
1066
|
if (typeof threadId === "string" && threadId.length > 0) {
|
|
583
1067
|
this.setActiveThreadId(threadId, `thread/start response ${key}`);
|
|
584
1068
|
}
|
|
1069
|
+
this.dropOrphanPendingRequests(`thread/start (new session)`);
|
|
585
1070
|
break;
|
|
586
1071
|
}
|
|
587
1072
|
case "thread/resume": {
|
|
588
1073
|
const threadId = message?.result?.thread?.id;
|
|
589
1074
|
if (typeof threadId === "string" && threadId.length > 0) {
|
|
590
1075
|
this.setActiveThreadId(threadId, `thread/resume response ${key}`);
|
|
1076
|
+
if (this.tuiWs) {
|
|
1077
|
+
this.replayPendingForThread(threadId, this.tuiWs);
|
|
1078
|
+
}
|
|
1079
|
+
this.dropOrphanPendingRequests(`thread/resume to ${threadId}`, threadId);
|
|
591
1080
|
}
|
|
592
1081
|
break;
|
|
593
1082
|
}
|
|
@@ -647,20 +1136,22 @@ class CodexAdapter extends EventEmitter {
|
|
|
647
1136
|
this.upstreamToClient.delete(upId);
|
|
648
1137
|
this.trackStaleProxyId(upId);
|
|
649
1138
|
}
|
|
1139
|
+
const requeuedServerRequests = [];
|
|
650
1140
|
for (const [proxyId, pending] of this.serverRequestToProxy.entries()) {
|
|
651
1141
|
if (pending.connId === connId) {
|
|
652
|
-
this.
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
},
|
|
660
|
-
timer.unref?.();
|
|
661
|
-
this.serverRequestTtlTimers.set(proxyId, timer);
|
|
1142
|
+
this.serverRequestToProxy.delete(proxyId);
|
|
1143
|
+
requeuedServerRequests.push({
|
|
1144
|
+
raw: pending.raw,
|
|
1145
|
+
serverId: pending.serverId,
|
|
1146
|
+
method: pending.method,
|
|
1147
|
+
threadId: pending.threadId
|
|
1148
|
+
});
|
|
1149
|
+
this.log(`Requeued in-flight server request after TUI disconnect (proxy id=${proxyId}, server id=${pending.serverId}, method=${pending.method}, threadId=${pending.threadId ?? "unknown"})`);
|
|
662
1150
|
}
|
|
663
1151
|
}
|
|
1152
|
+
if (requeuedServerRequests.length === 0)
|
|
1153
|
+
return;
|
|
1154
|
+
this.pendingServerRequests.push(...requeuedServerRequests);
|
|
664
1155
|
}
|
|
665
1156
|
trackStaleProxyId(proxyId) {
|
|
666
1157
|
this.clearTrackedId(this.staleProxyIds, proxyId);
|
|
@@ -695,7 +1186,7 @@ class CodexAdapter extends EventEmitter {
|
|
|
695
1186
|
store.delete(id);
|
|
696
1187
|
return true;
|
|
697
1188
|
}
|
|
698
|
-
|
|
1189
|
+
clearTransientResponseTrackingState() {
|
|
699
1190
|
this.pendingRequests.clear();
|
|
700
1191
|
this.upstreamToClient.clear();
|
|
701
1192
|
for (const timer of this.staleProxyIds.values()) {
|
|
@@ -706,17 +1197,36 @@ class CodexAdapter extends EventEmitter {
|
|
|
706
1197
|
clearTimeout(timer);
|
|
707
1198
|
}
|
|
708
1199
|
this.bridgeRequestIds.clear();
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
this.serverRequestTtlTimers.clear();
|
|
1200
|
+
}
|
|
1201
|
+
clearResponseTrackingState() {
|
|
1202
|
+
this.clearTransientResponseTrackingState();
|
|
713
1203
|
this.serverRequestToProxy.clear();
|
|
714
1204
|
this.pendingServerRequests = [];
|
|
1205
|
+
this.pendingServerResponses.clear();
|
|
1206
|
+
}
|
|
1207
|
+
clearResponseTrackingStateForAppServerReconnect() {
|
|
1208
|
+
this.clearTransientResponseTrackingState();
|
|
1209
|
+
for (const pending of this.serverRequestToProxy.values()) {
|
|
1210
|
+
this.pendingServerRequests.push({
|
|
1211
|
+
raw: pending.raw,
|
|
1212
|
+
serverId: pending.serverId,
|
|
1213
|
+
method: pending.method,
|
|
1214
|
+
threadId: pending.threadId
|
|
1215
|
+
});
|
|
1216
|
+
this.log(`Requeued in-flight server request on app-server reconnect (server id=${pending.serverId}, method=${pending.method}, threadId=${pending.threadId ?? "unknown"})`);
|
|
1217
|
+
}
|
|
1218
|
+
this.serverRequestToProxy.clear();
|
|
1219
|
+
this.pendingServerResponses.clear();
|
|
1220
|
+
}
|
|
1221
|
+
static buildPortListenLsofCommand(port) {
|
|
1222
|
+
return `lsof -ti tcp:${port} -sTCP:LISTEN`;
|
|
715
1223
|
}
|
|
716
1224
|
async checkPorts() {
|
|
717
1225
|
for (const port of [this.appPort, this.proxyPort]) {
|
|
718
1226
|
try {
|
|
719
|
-
const pids = execSync(
|
|
1227
|
+
const pids = execSync(CodexAdapter.buildPortListenLsofCommand(port), {
|
|
1228
|
+
encoding: "utf-8"
|
|
1229
|
+
}).trim();
|
|
720
1230
|
if (!pids)
|
|
721
1231
|
continue;
|
|
722
1232
|
const pidList = pids.split(`
|
|
@@ -746,7 +1256,9 @@ class CodexAdapter extends EventEmitter {
|
|
|
746
1256
|
throw new Error(`Port ${port} is already in use by non-Codex process(es): PID(s) ${foreignPids.join(", ")}. ` + `Please stop the process or set a different port via ${port === this.appPort ? "CODEX_WS_PORT" : "CODEX_PROXY_PORT"} env var.`);
|
|
747
1257
|
}
|
|
748
1258
|
try {
|
|
749
|
-
const remaining = execSync(
|
|
1259
|
+
const remaining = execSync(CodexAdapter.buildPortListenLsofCommand(port), {
|
|
1260
|
+
encoding: "utf-8"
|
|
1261
|
+
}).trim();
|
|
750
1262
|
if (remaining) {
|
|
751
1263
|
throw new Error(`Port ${port} is still occupied (PID(s): ${remaining.replace(/\n/g, ", ")}) after cleanup. ` + `Please stop the process or set a different port via ${port === this.appPort ? "CODEX_WS_PORT" : "CODEX_PROXY_PORT"} env var.`);
|
|
752
1264
|
}
|
|
@@ -765,7 +1277,7 @@ class CodexAdapter extends EventEmitter {
|
|
|
765
1277
|
`;
|
|
766
1278
|
process.stderr.write(line);
|
|
767
1279
|
try {
|
|
768
|
-
appendFileSync(
|
|
1280
|
+
appendFileSync(this.logFile, line);
|
|
769
1281
|
} catch {}
|
|
770
1282
|
}
|
|
771
1283
|
}
|
|
@@ -973,7 +1485,7 @@ class TuiConnectionState {
|
|
|
973
1485
|
|
|
974
1486
|
// src/daemon-lifecycle.ts
|
|
975
1487
|
import { spawn as spawn2, execFileSync } from "child_process";
|
|
976
|
-
import { existsSync, readFileSync, unlinkSync, writeFileSync, openSync, closeSync, constants } from "fs";
|
|
1488
|
+
import { existsSync as existsSync2, readFileSync, unlinkSync, writeFileSync, openSync, closeSync, constants } from "fs";
|
|
977
1489
|
import { fileURLToPath } from "url";
|
|
978
1490
|
var DAEMON_ENTRY = process.env.AGENTBRIDGE_DAEMON_ENTRY ?? "./daemon.ts";
|
|
979
1491
|
var DAEMON_PATH = fileURLToPath(new URL(DAEMON_ENTRY, import.meta.url));
|
|
@@ -1111,7 +1623,7 @@ class DaemonLifecycle {
|
|
|
1111
1623
|
} catch {}
|
|
1112
1624
|
}
|
|
1113
1625
|
wasKilled() {
|
|
1114
|
-
return
|
|
1626
|
+
return existsSync2(this.stateDir.killedFile);
|
|
1115
1627
|
}
|
|
1116
1628
|
launch() {
|
|
1117
1629
|
this.stateDir.ensure();
|
|
@@ -1232,116 +1744,62 @@ function isProcessAlive(pid) {
|
|
|
1232
1744
|
}
|
|
1233
1745
|
}
|
|
1234
1746
|
|
|
1235
|
-
// src/state-dir.ts
|
|
1236
|
-
import { mkdirSync, existsSync as existsSync2 } from "fs";
|
|
1237
|
-
import { join } from "path";
|
|
1238
|
-
import { homedir, platform } from "os";
|
|
1239
|
-
|
|
1240
|
-
class StateDirResolver {
|
|
1241
|
-
stateDir;
|
|
1242
|
-
constructor(envOverride) {
|
|
1243
|
-
const override = envOverride ?? process.env.AGENTBRIDGE_STATE_DIR;
|
|
1244
|
-
if (override) {
|
|
1245
|
-
this.stateDir = override;
|
|
1246
|
-
} else if (platform() === "darwin") {
|
|
1247
|
-
this.stateDir = join(homedir(), "Library", "Application Support", "AgentBridge");
|
|
1248
|
-
} else {
|
|
1249
|
-
const xdgState = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
|
|
1250
|
-
this.stateDir = join(xdgState, "agentbridge");
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
ensure() {
|
|
1254
|
-
if (!existsSync2(this.stateDir)) {
|
|
1255
|
-
mkdirSync(this.stateDir, { recursive: true });
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
get dir() {
|
|
1259
|
-
return this.stateDir;
|
|
1260
|
-
}
|
|
1261
|
-
get pidFile() {
|
|
1262
|
-
return join(this.stateDir, "daemon.pid");
|
|
1263
|
-
}
|
|
1264
|
-
get tuiPidFile() {
|
|
1265
|
-
return join(this.stateDir, "codex-tui.pid");
|
|
1266
|
-
}
|
|
1267
|
-
get lockFile() {
|
|
1268
|
-
return join(this.stateDir, "daemon.lock");
|
|
1269
|
-
}
|
|
1270
|
-
get statusFile() {
|
|
1271
|
-
return join(this.stateDir, "status.json");
|
|
1272
|
-
}
|
|
1273
|
-
get portsFile() {
|
|
1274
|
-
return join(this.stateDir, "ports.json");
|
|
1275
|
-
}
|
|
1276
|
-
get logFile() {
|
|
1277
|
-
return join(this.stateDir, "agentbridge.log");
|
|
1278
|
-
}
|
|
1279
|
-
get killedFile() {
|
|
1280
|
-
return join(this.stateDir, "killed");
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
1747
|
// src/config-service.ts
|
|
1285
1748
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
|
|
1286
1749
|
import { join as join2 } from "path";
|
|
1287
1750
|
var DEFAULT_CONFIG = {
|
|
1288
1751
|
version: "1.0",
|
|
1289
|
-
|
|
1290
|
-
|
|
1752
|
+
codex: {
|
|
1753
|
+
appPort: 4500,
|
|
1291
1754
|
proxyPort: 4501
|
|
1292
1755
|
},
|
|
1293
|
-
agents: {
|
|
1294
|
-
claude: {
|
|
1295
|
-
role: "Reviewer, Planner",
|
|
1296
|
-
mode: "push"
|
|
1297
|
-
},
|
|
1298
|
-
codex: {
|
|
1299
|
-
role: "Implementer, Executor"
|
|
1300
|
-
}
|
|
1301
|
-
},
|
|
1302
|
-
markers: ["IMPORTANT", "STATUS", "FYI"],
|
|
1303
1756
|
turnCoordination: {
|
|
1304
|
-
attentionWindowSeconds: 15
|
|
1305
|
-
busyGuard: true
|
|
1757
|
+
attentionWindowSeconds: 15
|
|
1306
1758
|
},
|
|
1307
1759
|
idleShutdownSeconds: 30
|
|
1308
1760
|
};
|
|
1309
|
-
var DEFAULT_COLLABORATION_MD = `# Collaboration Rules
|
|
1310
|
-
|
|
1311
|
-
## Roles
|
|
1312
|
-
- Claude: Reviewer, Planner, Hypothesis Challenger
|
|
1313
|
-
- Codex: Implementer, Executor, Reproducer/Verifier
|
|
1314
|
-
|
|
1315
|
-
## Thinking Patterns
|
|
1316
|
-
- Analytical/review tasks: Independent Analysis & Convergence
|
|
1317
|
-
- Implementation tasks: Architect -> Builder -> Critic
|
|
1318
|
-
- Debugging tasks: Hypothesis -> Experiment -> Interpretation
|
|
1319
|
-
|
|
1320
|
-
## Communication
|
|
1321
|
-
- Use explicit phrases: "My independent view is:", "I agree on:", "I disagree on:", "Current consensus:"
|
|
1322
|
-
- Tag messages with [IMPORTANT], [STATUS], or [FYI]
|
|
1323
|
-
|
|
1324
|
-
## Review Process
|
|
1325
|
-
- Cross-review: author never reviews their own code
|
|
1326
|
-
- All changes go through feature/fix branches + PR
|
|
1327
|
-
- Merge via squash merge
|
|
1328
|
-
|
|
1329
|
-
## Custom Rules
|
|
1330
|
-
<!-- Add your project-specific collaboration rules here -->
|
|
1331
|
-
`;
|
|
1332
1761
|
var CONFIG_DIR = ".agentbridge";
|
|
1333
1762
|
var CONFIG_FILE = "config.json";
|
|
1334
|
-
|
|
1763
|
+
function isRecord(value) {
|
|
1764
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1765
|
+
}
|
|
1766
|
+
function normalizeInteger(value, fallback) {
|
|
1767
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
1768
|
+
return value;
|
|
1769
|
+
if (typeof value === "string") {
|
|
1770
|
+
const parsed = Number(value);
|
|
1771
|
+
if (Number.isFinite(parsed))
|
|
1772
|
+
return parsed;
|
|
1773
|
+
}
|
|
1774
|
+
return fallback;
|
|
1775
|
+
}
|
|
1776
|
+
function normalizeConfig(raw) {
|
|
1777
|
+
if (!isRecord(raw))
|
|
1778
|
+
return null;
|
|
1779
|
+
const config = raw;
|
|
1780
|
+
const codex = isRecord(config.codex) ? config.codex : {};
|
|
1781
|
+
const daemon = isRecord(config.daemon) ? config.daemon : {};
|
|
1782
|
+
const turnCoordination = isRecord(config.turnCoordination) ? config.turnCoordination : {};
|
|
1783
|
+
return {
|
|
1784
|
+
version: typeof config.version === "string" ? config.version : DEFAULT_CONFIG.version,
|
|
1785
|
+
codex: {
|
|
1786
|
+
appPort: normalizeInteger(codex.appPort ?? daemon.port, DEFAULT_CONFIG.codex.appPort),
|
|
1787
|
+
proxyPort: normalizeInteger(codex.proxyPort ?? daemon.proxyPort, DEFAULT_CONFIG.codex.proxyPort)
|
|
1788
|
+
},
|
|
1789
|
+
turnCoordination: {
|
|
1790
|
+
attentionWindowSeconds: normalizeInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds)
|
|
1791
|
+
},
|
|
1792
|
+
idleShutdownSeconds: normalizeInteger(config.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds)
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1335
1795
|
|
|
1336
1796
|
class ConfigService {
|
|
1337
1797
|
configDir;
|
|
1338
1798
|
configPath;
|
|
1339
|
-
collaborationPath;
|
|
1340
1799
|
constructor(projectRoot) {
|
|
1341
1800
|
const root = projectRoot ?? process.cwd();
|
|
1342
1801
|
this.configDir = join2(root, CONFIG_DIR);
|
|
1343
1802
|
this.configPath = join2(this.configDir, CONFIG_FILE);
|
|
1344
|
-
this.collaborationPath = join2(this.configDir, COLLABORATION_FILE);
|
|
1345
1803
|
}
|
|
1346
1804
|
hasConfig() {
|
|
1347
1805
|
return existsSync3(this.configPath);
|
|
@@ -1349,7 +1807,7 @@ class ConfigService {
|
|
|
1349
1807
|
load() {
|
|
1350
1808
|
try {
|
|
1351
1809
|
const raw = readFileSync2(this.configPath, "utf-8");
|
|
1352
|
-
return JSON.parse(raw);
|
|
1810
|
+
return normalizeConfig(JSON.parse(raw));
|
|
1353
1811
|
} catch {
|
|
1354
1812
|
return null;
|
|
1355
1813
|
}
|
|
@@ -1362,17 +1820,6 @@ class ConfigService {
|
|
|
1362
1820
|
writeFileSync2(this.configPath, JSON.stringify(config, null, 2) + `
|
|
1363
1821
|
`, "utf-8");
|
|
1364
1822
|
}
|
|
1365
|
-
loadCollaboration() {
|
|
1366
|
-
try {
|
|
1367
|
-
return readFileSync2(this.collaborationPath, "utf-8");
|
|
1368
|
-
} catch {
|
|
1369
|
-
return null;
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
saveCollaboration(content) {
|
|
1373
|
-
this.ensureConfigDir();
|
|
1374
|
-
writeFileSync2(this.collaborationPath, content, "utf-8");
|
|
1375
|
-
}
|
|
1376
1823
|
initDefaults() {
|
|
1377
1824
|
this.ensureConfigDir();
|
|
1378
1825
|
const created = [];
|
|
@@ -1380,18 +1827,11 @@ class ConfigService {
|
|
|
1380
1827
|
this.save(DEFAULT_CONFIG);
|
|
1381
1828
|
created.push(this.configPath);
|
|
1382
1829
|
}
|
|
1383
|
-
if (!existsSync3(this.collaborationPath)) {
|
|
1384
|
-
this.saveCollaboration(DEFAULT_COLLABORATION_MD);
|
|
1385
|
-
created.push(this.collaborationPath);
|
|
1386
|
-
}
|
|
1387
1830
|
return created;
|
|
1388
1831
|
}
|
|
1389
1832
|
get configFilePath() {
|
|
1390
1833
|
return this.configPath;
|
|
1391
1834
|
}
|
|
1392
|
-
get collaborationFilePath() {
|
|
1393
|
-
return this.collaborationPath;
|
|
1394
|
-
}
|
|
1395
1835
|
ensureConfigDir() {
|
|
1396
1836
|
if (!existsSync3(this.configDir)) {
|
|
1397
1837
|
mkdirSync2(this.configDir, { recursive: true });
|
|
@@ -1399,13 +1839,16 @@ class ConfigService {
|
|
|
1399
1839
|
}
|
|
1400
1840
|
}
|
|
1401
1841
|
|
|
1842
|
+
// src/control-protocol.ts
|
|
1843
|
+
var CLOSE_CODE_REPLACED = 4001;
|
|
1844
|
+
|
|
1402
1845
|
// src/daemon.ts
|
|
1403
1846
|
var stateDir = new StateDirResolver;
|
|
1404
1847
|
stateDir.ensure();
|
|
1405
1848
|
var configService = new ConfigService;
|
|
1406
1849
|
var config = configService.loadOrDefault();
|
|
1407
|
-
var CODEX_APP_PORT = parseInt(process.env.CODEX_WS_PORT ?? String(config.
|
|
1408
|
-
var CODEX_PROXY_PORT = parseInt(process.env.CODEX_PROXY_PORT ?? String(config.
|
|
1850
|
+
var CODEX_APP_PORT = parseInt(process.env.CODEX_WS_PORT ?? String(config.codex.appPort), 10);
|
|
1851
|
+
var CODEX_PROXY_PORT = parseInt(process.env.CODEX_PROXY_PORT ?? String(config.codex.proxyPort), 10);
|
|
1409
1852
|
var CONTROL_PORT = parseInt(process.env.AGENTBRIDGE_CONTROL_PORT ?? "4502", 10);
|
|
1410
1853
|
var TUI_DISCONNECT_GRACE_MS = parseInt(process.env.TUI_DISCONNECT_GRACE_MS ?? "2500", 10);
|
|
1411
1854
|
var CLAUDE_DISCONNECT_GRACE_MS = 5000;
|
|
@@ -1414,7 +1857,7 @@ var FILTER_MODE = process.env.AGENTBRIDGE_FILTER_MODE === "full" ? "full" : "fil
|
|
|
1414
1857
|
var IDLE_SHUTDOWN_MS = parseInt(process.env.AGENTBRIDGE_IDLE_SHUTDOWN_MS ?? String(config.idleShutdownSeconds * 1000), 10);
|
|
1415
1858
|
var ATTENTION_WINDOW_MS = parseInt(process.env.AGENTBRIDGE_ATTENTION_WINDOW_MS ?? String(config.turnCoordination.attentionWindowSeconds * 1000), 10);
|
|
1416
1859
|
var daemonLifecycle = new DaemonLifecycle({ stateDir, controlPort: CONTROL_PORT, log });
|
|
1417
|
-
var codex = new CodexAdapter(CODEX_APP_PORT, CODEX_PROXY_PORT);
|
|
1860
|
+
var codex = new CodexAdapter(CODEX_APP_PORT, CODEX_PROXY_PORT, stateDir.logFile);
|
|
1418
1861
|
var attachCmd = `codex --enable tui_app_server --remote ${codex.proxyUrl}`;
|
|
1419
1862
|
var controlServer = null;
|
|
1420
1863
|
var attachedClaude = null;
|
|
@@ -1430,6 +1873,7 @@ var idleShutdownTimer = null;
|
|
|
1430
1873
|
var claudeDisconnectTimer = null;
|
|
1431
1874
|
var claudeOnlineNoticeSent = false;
|
|
1432
1875
|
var claudeOfflineNoticeShown = false;
|
|
1876
|
+
var codexCollaborationKickoffSent = false;
|
|
1433
1877
|
var lastAttachStatusSentTs = 0;
|
|
1434
1878
|
var ATTACH_STATUS_COOLDOWN_MS = 30000;
|
|
1435
1879
|
var bufferedMessages = [];
|
|
@@ -1496,6 +1940,9 @@ codex.on("turnCompleted", () => {
|
|
|
1496
1940
|
replyReceivedDuringTurn = false;
|
|
1497
1941
|
emitToClaude(systemMessage("system_turn_completed", "\u2705 Codex finished the current turn. You can reply now if needed."));
|
|
1498
1942
|
startAttentionWindow();
|
|
1943
|
+
if (attachedClaude && shouldNotifyCodexClaudeOnline()) {
|
|
1944
|
+
notifyCodexClaudeOnline();
|
|
1945
|
+
}
|
|
1499
1946
|
});
|
|
1500
1947
|
codex.on("ready", (threadId) => {
|
|
1501
1948
|
tuiConnectionState.markBridgeReady();
|
|
@@ -1640,8 +2087,10 @@ function handleControlMessage(ws, raw) {
|
|
|
1640
2087
|
}
|
|
1641
2088
|
}
|
|
1642
2089
|
function attachClaude(ws) {
|
|
1643
|
-
if (attachedClaude && attachedClaude !== ws) {
|
|
1644
|
-
|
|
2090
|
+
if (attachedClaude && attachedClaude !== ws && attachedClaude.readyState !== WebSocket.CLOSED) {
|
|
2091
|
+
log(`Rejecting Claude frontend #${ws.data.clientId} \u2014 another session (#${attachedClaude.data.clientId}) is already attached (readyState=${attachedClaude.readyState})`);
|
|
2092
|
+
ws.close(CLOSE_CODE_REPLACED, "another Claude session is already connected");
|
|
2093
|
+
return;
|
|
1645
2094
|
}
|
|
1646
2095
|
clearPendingClaudeDisconnect("Claude frontend attached");
|
|
1647
2096
|
attachedClaude = ws;
|
|
@@ -1826,9 +2275,23 @@ function currentReadyMessage() {
|
|
|
1826
2275
|
return `\u2705 Codex TUI connected (${codex.activeThreadId}). Bridge ready.`;
|
|
1827
2276
|
}
|
|
1828
2277
|
function notifyCodexClaudeOnline() {
|
|
2278
|
+
const message = !codexCollaborationKickoffSent ? [
|
|
2279
|
+
"\uD83E\uDD1D Claude Code has connected via AgentBridge.",
|
|
2280
|
+
"You are now in a multi-agent collaboration session.",
|
|
2281
|
+
"When you receive a complex task, propose a division of labor to Claude.",
|
|
2282
|
+
"Claude can send you messages \u2014 they will appear as injected user messages.",
|
|
2283
|
+
"Respond naturally and Claude will receive your output via AgentBridge."
|
|
2284
|
+
].join(`
|
|
2285
|
+
`) : "\u2705 AgentBridge connected to Claude Code.";
|
|
2286
|
+
const delivered = codex.injectMessage(message);
|
|
2287
|
+
if (!delivered) {
|
|
2288
|
+
log("Deferred Claude-online notice to Codex \u2014 will retry after current turn completes");
|
|
2289
|
+
return false;
|
|
2290
|
+
}
|
|
1829
2291
|
claudeOnlineNoticeSent = true;
|
|
1830
2292
|
claudeOfflineNoticeShown = false;
|
|
1831
|
-
|
|
2293
|
+
codexCollaborationKickoffSent = true;
|
|
2294
|
+
return true;
|
|
1832
2295
|
}
|
|
1833
2296
|
function shouldNotifyCodexClaudeOnline() {
|
|
1834
2297
|
return !claudeOnlineNoticeSent || claudeOfflineNoticeShown;
|