@raysonmeng/agentbridge 0.1.3 → 0.1.5
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.
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
{
|
|
13
13
|
"name": "agentbridge",
|
|
14
14
|
"description": "Bridge Claude Code and Codex through a shared daemon, push channel delivery, and reply/get_messages tools.",
|
|
15
|
-
"version": "0.1.
|
|
15
|
+
"version": "0.1.5",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "AgentBridge Contributors",
|
|
18
18
|
"email": "raysonmeng@qq.com"
|
package/dist/cli.js
CHANGED
|
@@ -271,7 +271,7 @@ var exports_dev = {};
|
|
|
271
271
|
__export(exports_dev, {
|
|
272
272
|
runDev: () => runDev
|
|
273
273
|
});
|
|
274
|
-
import { execFileSync as execFileSync3 } from "child_process";
|
|
274
|
+
import { execFileSync as execFileSync3, spawnSync } from "child_process";
|
|
275
275
|
import { resolve } from "path";
|
|
276
276
|
import { existsSync as existsSync3, cpSync, rmSync } from "fs";
|
|
277
277
|
import { homedir } from "os";
|
|
@@ -282,6 +282,28 @@ async function runDev() {
|
|
|
282
282
|
const marketplacePath = resolve(projectRoot, ".claude-plugin", "marketplace.json");
|
|
283
283
|
const pluginDir = resolve(projectRoot, "plugins", "agentbridge");
|
|
284
284
|
const pluginManifest = resolve(pluginDir, ".claude-plugin", "plugin.json");
|
|
285
|
+
console.log("Building CLI from source...");
|
|
286
|
+
const cliBuild = spawnSync("bun", ["run", "build:cli"], {
|
|
287
|
+
cwd: projectRoot,
|
|
288
|
+
stdio: "inherit"
|
|
289
|
+
});
|
|
290
|
+
if (cliBuild.status !== 0) {
|
|
291
|
+
console.error(" ERROR: CLI build failed. Fix build errors and try again.");
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
console.log(` \u2713 CLI built successfully
|
|
295
|
+
`);
|
|
296
|
+
console.log("Building plugin from source...");
|
|
297
|
+
const buildResult = spawnSync("bun", ["run", "build:plugin"], {
|
|
298
|
+
cwd: projectRoot,
|
|
299
|
+
stdio: "inherit"
|
|
300
|
+
});
|
|
301
|
+
if (buildResult.status !== 0) {
|
|
302
|
+
console.error(" ERROR: Plugin build failed. Fix build errors and try again.");
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
console.log(` \u2713 Plugin built successfully
|
|
306
|
+
`);
|
|
285
307
|
if (!existsSync3(pluginManifest)) {
|
|
286
308
|
console.error(` ERROR: Plugin manifest not found at ${pluginManifest}`);
|
|
287
309
|
console.error(" Run 'bun run build:plugin' first, or check your working tree.");
|
|
@@ -1012,7 +1034,7 @@ var init_kill = __esm(() => {
|
|
|
1012
1034
|
var require_package = __commonJS((exports, module) => {
|
|
1013
1035
|
module.exports = {
|
|
1014
1036
|
name: "@raysonmeng/agentbridge",
|
|
1015
|
-
version: "0.1.
|
|
1037
|
+
version: "0.1.5",
|
|
1016
1038
|
description: "Bridge between Claude Code and Codex \u2014 bidirectional agent communication via MCP Channel + JSON-RPC",
|
|
1017
1039
|
type: "module",
|
|
1018
1040
|
bin: {
|
|
@@ -1031,13 +1053,14 @@ var require_package = __commonJS((exports, module) => {
|
|
|
1031
1053
|
start: "bun run src/bridge.ts",
|
|
1032
1054
|
"build:cli": "mkdir -p dist && bun build src/cli.ts --outfile dist/cli.js --target bun && chmod +x dist/cli.js",
|
|
1033
1055
|
"build:plugin": "mkdir -p plugins/agentbridge/server && bun build src/bridge.ts --outfile plugins/agentbridge/server/bridge-server.js --target bun && bun build src/daemon.ts --outfile plugins/agentbridge/server/daemon.js --target bun",
|
|
1056
|
+
"verify:plugin-sync": "node scripts/verify-plugin-sync.cjs",
|
|
1034
1057
|
postinstall: "node scripts/postinstall.cjs",
|
|
1035
1058
|
prepublishOnly: "bun run build:cli && bun run build:plugin",
|
|
1036
1059
|
"validate:plugin": "claude plugin validate plugins/agentbridge && claude plugin validate .claude-plugin/marketplace.json",
|
|
1037
1060
|
test: "bun test src",
|
|
1038
1061
|
typecheck: "tsc --noEmit",
|
|
1039
1062
|
"validate:plugin-versions": "bun scripts/check-plugin-versions.js",
|
|
1040
|
-
check: "tsc --noEmit && bun test src && bun run
|
|
1063
|
+
check: "tsc --noEmit && bun test src && bun run verify:plugin-sync && bun scripts/check-plugin-versions.js"
|
|
1041
1064
|
},
|
|
1042
1065
|
repository: {
|
|
1043
1066
|
type: "git",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raysonmeng/agentbridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Bridge between Claude Code and Codex — bidirectional agent communication via MCP Channel + JSON-RPC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,13 +19,14 @@
|
|
|
19
19
|
"start": "bun run src/bridge.ts",
|
|
20
20
|
"build:cli": "mkdir -p dist && bun build src/cli.ts --outfile dist/cli.js --target bun && chmod +x dist/cli.js",
|
|
21
21
|
"build:plugin": "mkdir -p plugins/agentbridge/server && bun build src/bridge.ts --outfile plugins/agentbridge/server/bridge-server.js --target bun && bun build src/daemon.ts --outfile plugins/agentbridge/server/daemon.js --target bun",
|
|
22
|
+
"verify:plugin-sync": "node scripts/verify-plugin-sync.cjs",
|
|
22
23
|
"postinstall": "node scripts/postinstall.cjs",
|
|
23
24
|
"prepublishOnly": "bun run build:cli && bun run build:plugin",
|
|
24
25
|
"validate:plugin": "claude plugin validate plugins/agentbridge && claude plugin validate .claude-plugin/marketplace.json",
|
|
25
26
|
"test": "bun test src",
|
|
26
27
|
"typecheck": "tsc --noEmit",
|
|
27
28
|
"validate:plugin-versions": "bun scripts/check-plugin-versions.js",
|
|
28
|
-
"check": "tsc --noEmit && bun test src && bun run
|
|
29
|
+
"check": "tsc --noEmit && bun test src && bun run verify:plugin-sync && bun scripts/check-plugin-versions.js"
|
|
29
30
|
},
|
|
30
31
|
"repository": {
|
|
31
32
|
"type": "git",
|
|
@@ -13749,8 +13749,8 @@ class ClaudeAdapter extends EventEmitter {
|
|
|
13749
13749
|
this.resolvedMode = this.configuredMode;
|
|
13750
13750
|
this.log(`Delivery mode set by AGENTBRIDGE_MODE: ${this.resolvedMode}`);
|
|
13751
13751
|
} else {
|
|
13752
|
-
this.resolvedMode = "
|
|
13753
|
-
this.log("Delivery mode defaulting to
|
|
13752
|
+
this.resolvedMode = "pull";
|
|
13753
|
+
this.log("Delivery mode defaulting to pull (set AGENTBRIDGE_MODE=push to opt into channel delivery)");
|
|
13754
13754
|
}
|
|
13755
13755
|
}
|
|
13756
13756
|
async pushNotification(message) {
|
|
@@ -13781,6 +13781,7 @@ class ClaudeAdapter extends EventEmitter {
|
|
|
13781
13781
|
this.log(`Pushed notification: ${msgId}`);
|
|
13782
13782
|
} catch (e) {
|
|
13783
13783
|
this.log(`Push notification failed: ${e.message}`);
|
|
13784
|
+
this.queueForPull(message);
|
|
13784
13785
|
}
|
|
13785
13786
|
}
|
|
13786
13787
|
queueForPull(message) {
|
|
@@ -13929,6 +13930,11 @@ ${formatted}`
|
|
|
13929
13930
|
|
|
13930
13931
|
// src/daemon-client.ts
|
|
13931
13932
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
13933
|
+
|
|
13934
|
+
// src/control-protocol.ts
|
|
13935
|
+
var CLOSE_CODE_REPLACED = 4001;
|
|
13936
|
+
|
|
13937
|
+
// src/daemon-client.ts
|
|
13932
13938
|
var nextSocketId = 0;
|
|
13933
13939
|
|
|
13934
13940
|
class DaemonClient extends EventEmitter2 {
|
|
@@ -14047,7 +14053,11 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14047
14053
|
if (isCurrent) {
|
|
14048
14054
|
this.ws = null;
|
|
14049
14055
|
this.rejectPendingReplies("AgentBridge daemon disconnected.");
|
|
14050
|
-
|
|
14056
|
+
if (event.code === CLOSE_CODE_REPLACED) {
|
|
14057
|
+
this.emit("rejected");
|
|
14058
|
+
} else {
|
|
14059
|
+
this.emit("disconnect");
|
|
14060
|
+
}
|
|
14051
14061
|
}
|
|
14052
14062
|
};
|
|
14053
14063
|
ws.onerror = () => {};
|
|
@@ -14499,6 +14509,16 @@ class ConfigService {
|
|
|
14499
14509
|
}
|
|
14500
14510
|
}
|
|
14501
14511
|
|
|
14512
|
+
// src/bridge-disabled-state.ts
|
|
14513
|
+
function disabledReplyError(reason) {
|
|
14514
|
+
switch (reason) {
|
|
14515
|
+
case "rejected":
|
|
14516
|
+
return "AgentBridge rejected this session \u2014 another Claude Code session is already connected. Close the other session first, or run `agentbridge kill` to reset.";
|
|
14517
|
+
case "killed":
|
|
14518
|
+
return "AgentBridge is disabled by `agentbridge kill`. Restart Claude Code (`agentbridge claude`), switch to a new conversation, or run `/resume` to reconnect.";
|
|
14519
|
+
}
|
|
14520
|
+
}
|
|
14521
|
+
|
|
14502
14522
|
// src/bridge.ts
|
|
14503
14523
|
var stateDir = new StateDirResolver;
|
|
14504
14524
|
var configService = new ConfigService;
|
|
@@ -14510,6 +14530,7 @@ var claude = new ClaudeAdapter;
|
|
|
14510
14530
|
var daemonClient = new DaemonClient(CONTROL_WS_URL);
|
|
14511
14531
|
var shuttingDown = false;
|
|
14512
14532
|
var daemonDisabled = false;
|
|
14533
|
+
var daemonDisabledReason = null;
|
|
14513
14534
|
var RECONNECT_NOTIFY_COOLDOWN_MS = 30000;
|
|
14514
14535
|
var DISABLED_RECOVERY_INTERVAL_MS = 5000;
|
|
14515
14536
|
var lastDisconnectNotifyTs = 0;
|
|
@@ -14523,7 +14544,7 @@ claude.setReplySender(async (msg, requireReply) => {
|
|
|
14523
14544
|
if (daemonDisabled) {
|
|
14524
14545
|
return {
|
|
14525
14546
|
success: false,
|
|
14526
|
-
error:
|
|
14547
|
+
error: disabledReplyError(daemonDisabledReason ?? "killed")
|
|
14527
14548
|
};
|
|
14528
14549
|
}
|
|
14529
14550
|
return daemonClient.sendReply(msg, requireReply);
|
|
@@ -14548,6 +14569,15 @@ daemonClient.on("disconnect", () => {
|
|
|
14548
14569
|
}
|
|
14549
14570
|
reconnectToDaemon();
|
|
14550
14571
|
});
|
|
14572
|
+
daemonClient.on("rejected", async () => {
|
|
14573
|
+
if (shuttingDown || daemonDisabled)
|
|
14574
|
+
return;
|
|
14575
|
+
log("Daemon rejected this session (close code 4001) \u2014 another Claude session is already connected");
|
|
14576
|
+
daemonDisabled = true;
|
|
14577
|
+
daemonDisabledReason = "rejected";
|
|
14578
|
+
await claude.pushNotification(systemMessage("system_bridge_replaced", "\u26A0\uFE0F AgentBridge daemon rejected this session \u2014 another Claude Code session is already connected. Close the other session first, or run `agentbridge kill` to reset. AgentBridge \u5B88\u62A4\u8FDB\u7A0B\u62D2\u7EDD\u4E86\u6B64\u4F1A\u8BDD\u2014\u2014\u53E6\u4E00\u4E2A Claude Code \u4F1A\u8BDD\u5DF2\u5728\u8FDE\u63A5\u4E2D\u3002\u8BF7\u5148\u5173\u95ED\u53E6\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u6216\u8FD0\u884C `agentbridge kill` \u91CD\u7F6E\u3002"));
|
|
14579
|
+
await daemonClient.disconnect();
|
|
14580
|
+
});
|
|
14551
14581
|
claude.on("ready", async () => {
|
|
14552
14582
|
log(`MCP server ready (delivery mode: ${claude.getDeliveryMode()}) \u2014 ensuring AgentBridge daemon...`);
|
|
14553
14583
|
if (daemonLifecycle.wasKilled()) {
|
|
@@ -14565,6 +14595,7 @@ async function connectToDaemon(isReconnect = false) {
|
|
|
14565
14595
|
await daemonLifecycle.ensureRunning();
|
|
14566
14596
|
await daemonClient.connect();
|
|
14567
14597
|
daemonClient.attachClaude();
|
|
14598
|
+
daemonDisabledReason = null;
|
|
14568
14599
|
if (!isReconnect) {
|
|
14569
14600
|
claude.pushNotification(systemMessage("system_bridge_ready", "\u2705 AgentBridge bridge is ready. Daemon connected. Start Codex in another terminal with: agentbridge codex"));
|
|
14570
14601
|
}
|
|
@@ -14578,6 +14609,7 @@ async function enterDisabledState(logMessage, notificationContent) {
|
|
|
14578
14609
|
if (daemonDisabled)
|
|
14579
14610
|
return;
|
|
14580
14611
|
daemonDisabled = true;
|
|
14612
|
+
daemonDisabledReason = "killed";
|
|
14581
14613
|
log(logMessage);
|
|
14582
14614
|
await claude.pushNotification(systemMessage("system_bridge_disabled", notificationContent));
|
|
14583
14615
|
await daemonClient.disconnect();
|
|
@@ -14666,11 +14698,13 @@ async function pollDisabledRecovery() {
|
|
|
14666
14698
|
await daemonClient.connect();
|
|
14667
14699
|
daemonClient.attachClaude();
|
|
14668
14700
|
daemonDisabled = false;
|
|
14701
|
+
daemonDisabledReason = null;
|
|
14669
14702
|
stopDisabledRecoveryPoller();
|
|
14670
14703
|
claude.pushNotification(systemMessage("system_bridge_recovered", "\u2705 AgentBridge recovered after the killed sentinel was cleared. Daemon reconnected."));
|
|
14671
14704
|
} catch (err) {
|
|
14672
14705
|
log(`Disabled-state direct reconnect failed: ${err.message}`);
|
|
14673
14706
|
daemonDisabled = false;
|
|
14707
|
+
daemonDisabledReason = null;
|
|
14674
14708
|
stopDisabledRecoveryPoller();
|
|
14675
14709
|
reconnectToDaemon();
|
|
14676
14710
|
}
|
|
@@ -9,8 +9,52 @@ import { spawn, execSync } from "child_process";
|
|
|
9
9
|
import { createInterface } from "readline";
|
|
10
10
|
import { EventEmitter } from "events";
|
|
11
11
|
import { appendFileSync } from "fs";
|
|
12
|
+
|
|
13
|
+
// src/app-server-protocol.ts
|
|
14
|
+
var APP_SERVER_TRACKED_REQUEST_METHODS = [
|
|
15
|
+
"thread/start",
|
|
16
|
+
"thread/resume",
|
|
17
|
+
"turn/start"
|
|
18
|
+
];
|
|
19
|
+
var APP_SERVER_SERVER_REQUEST_METHODS = [
|
|
20
|
+
"item/permissions/requestApproval",
|
|
21
|
+
"item/fileChange/requestApproval",
|
|
22
|
+
"item/commandExecution/requestApproval"
|
|
23
|
+
];
|
|
24
|
+
var APP_SERVER_NOTIFICATION_METHODS = [
|
|
25
|
+
"turn/started",
|
|
26
|
+
"turn/completed",
|
|
27
|
+
"item/started",
|
|
28
|
+
"item/agentMessage/delta",
|
|
29
|
+
"item/completed"
|
|
30
|
+
];
|
|
31
|
+
var TRACKED_REQUEST_METHOD_SET = new Set(APP_SERVER_TRACKED_REQUEST_METHODS);
|
|
32
|
+
var SERVER_REQUEST_METHOD_SET = new Set(APP_SERVER_SERVER_REQUEST_METHODS);
|
|
33
|
+
var NOTIFICATION_METHOD_SET = new Set(APP_SERVER_NOTIFICATION_METHODS);
|
|
34
|
+
function isObjectRecord(value) {
|
|
35
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
function isTrackedAppServerRequestMethod(method) {
|
|
38
|
+
return typeof method === "string" && TRACKED_REQUEST_METHOD_SET.has(method);
|
|
39
|
+
}
|
|
40
|
+
function isAppServerRequestMessage(value) {
|
|
41
|
+
if (!isObjectRecord(value))
|
|
42
|
+
return false;
|
|
43
|
+
return (typeof value.id === "number" || typeof value.id === "string") && typeof value.method === "string";
|
|
44
|
+
}
|
|
45
|
+
function isAppServerNotification(value) {
|
|
46
|
+
if (!isObjectRecord(value))
|
|
47
|
+
return false;
|
|
48
|
+
return value.id === undefined && typeof value.method === "string" && NOTIFICATION_METHOD_SET.has(value.method);
|
|
49
|
+
}
|
|
50
|
+
function isAppServerResponseMessage(value) {
|
|
51
|
+
if (!isObjectRecord(value))
|
|
52
|
+
return false;
|
|
53
|
+
return (typeof value.id === "number" || typeof value.id === "string") && value.method === undefined && (("result" in value) || ("error" in value));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/codex-adapter.ts
|
|
12
57
|
var LOG_FILE = "/tmp/agentbridge.log";
|
|
13
|
-
var TRACKED_REQUEST_METHODS = new Set(["thread/start", "thread/resume", "turn/start"]);
|
|
14
58
|
|
|
15
59
|
class CodexAdapter extends EventEmitter {
|
|
16
60
|
static RESPONSE_TRACKING_TTL_MS = 30000;
|
|
@@ -23,6 +67,8 @@ class CodexAdapter extends EventEmitter {
|
|
|
23
67
|
appPort;
|
|
24
68
|
proxyPort;
|
|
25
69
|
tuiConnId = 0;
|
|
70
|
+
connIdCounter = 0;
|
|
71
|
+
secondaryConnections = new Map;
|
|
26
72
|
agentMessageBuffers = new Map;
|
|
27
73
|
pendingRequests = new Map;
|
|
28
74
|
activeTurnIds = new Set;
|
|
@@ -30,11 +76,15 @@ class CodexAdapter extends EventEmitter {
|
|
|
30
76
|
nextProxyId = 1e5;
|
|
31
77
|
upstreamToClient = new Map;
|
|
32
78
|
serverRequestToProxy = new Map;
|
|
33
|
-
serverRequestTtlTimers = new Map;
|
|
34
79
|
pendingServerRequests = [];
|
|
80
|
+
pendingServerResponses = new Map;
|
|
35
81
|
staleProxyIds = new Map;
|
|
36
82
|
bridgeRequestIds = new Map;
|
|
37
83
|
intentionalDisconnect = false;
|
|
84
|
+
pendingTuiMessages = [];
|
|
85
|
+
reconnectingForNewSession = false;
|
|
86
|
+
replayingBufferedMessages = false;
|
|
87
|
+
appServerGeneration = 0;
|
|
38
88
|
constructor(appPort = 4500, proxyPort = 4501) {
|
|
39
89
|
super();
|
|
40
90
|
this.appPort = appPort;
|
|
@@ -75,6 +125,12 @@ class CodexAdapter extends EventEmitter {
|
|
|
75
125
|
}
|
|
76
126
|
this.appServerWs?.close();
|
|
77
127
|
this.appServerWs = null;
|
|
128
|
+
for (const [id, sec] of this.secondaryConnections) {
|
|
129
|
+
try {
|
|
130
|
+
sec.appServerWs?.close();
|
|
131
|
+
} catch {}
|
|
132
|
+
this.secondaryConnections.delete(id);
|
|
133
|
+
}
|
|
78
134
|
this.proxyServer?.stop();
|
|
79
135
|
this.proxyServer = null;
|
|
80
136
|
this.clearResponseTrackingState();
|
|
@@ -135,16 +191,24 @@ class CodexAdapter extends EventEmitter {
|
|
|
135
191
|
throw new Error("Codex app-server failed to become healthy");
|
|
136
192
|
}
|
|
137
193
|
connectToAppServer(isReconnect = false) {
|
|
194
|
+
const generation = ++this.appServerGeneration;
|
|
138
195
|
return new Promise((resolve, reject) => {
|
|
139
196
|
const appWs = new WebSocket(this.appServerUrl);
|
|
140
197
|
appWs.onopen = () => {
|
|
198
|
+
if (this.appServerGeneration !== generation) {
|
|
199
|
+
appWs.close();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
141
202
|
this.appServerWs = appWs;
|
|
142
203
|
this.intentionalDisconnect = false;
|
|
143
204
|
this.reconnectAttempts = 0;
|
|
144
|
-
this.log(isReconnect ? "Reconnected to app-server" : "Connected to app-server
|
|
205
|
+
this.log(isReconnect ? "Reconnected to app-server" : "Connected to app-server");
|
|
206
|
+
this.flushPendingServerResponses();
|
|
145
207
|
resolve();
|
|
146
208
|
};
|
|
147
209
|
appWs.onmessage = (event) => {
|
|
210
|
+
if (this.appServerGeneration !== generation)
|
|
211
|
+
return;
|
|
148
212
|
const data = typeof event.data === "string" ? event.data : event.data.toString();
|
|
149
213
|
const forwarded = this.handleAppServerPayload(data);
|
|
150
214
|
if (forwarded === null)
|
|
@@ -160,22 +224,58 @@ class CodexAdapter extends EventEmitter {
|
|
|
160
224
|
}
|
|
161
225
|
};
|
|
162
226
|
appWs.onerror = () => {
|
|
227
|
+
if (this.appServerGeneration !== generation)
|
|
228
|
+
return;
|
|
163
229
|
this.log("App-server connection error");
|
|
164
230
|
if (!isReconnect)
|
|
165
231
|
reject(new Error("Failed to connect to app-server"));
|
|
166
232
|
};
|
|
167
233
|
appWs.onclose = () => {
|
|
168
|
-
this.
|
|
169
|
-
|
|
170
|
-
this.
|
|
171
|
-
this.activeTurnIds.clear();
|
|
172
|
-
this.turnInProgress = false;
|
|
173
|
-
if (!this.intentionalDisconnect) {
|
|
174
|
-
this.scheduleReconnect();
|
|
175
|
-
}
|
|
234
|
+
if (this.appServerGeneration !== generation)
|
|
235
|
+
return;
|
|
236
|
+
this.handleAppServerClose();
|
|
176
237
|
};
|
|
177
238
|
});
|
|
178
239
|
}
|
|
240
|
+
async reconnectAppServerForNewSession(tuiWs) {
|
|
241
|
+
this.appServerGeneration++;
|
|
242
|
+
this.intentionalDisconnect = true;
|
|
243
|
+
if (this.reconnectTimer) {
|
|
244
|
+
clearTimeout(this.reconnectTimer);
|
|
245
|
+
this.reconnectTimer = null;
|
|
246
|
+
}
|
|
247
|
+
const oldWs = this.appServerWs;
|
|
248
|
+
this.appServerWs = null;
|
|
249
|
+
if (oldWs) {
|
|
250
|
+
try {
|
|
251
|
+
oldWs.close();
|
|
252
|
+
} catch {}
|
|
253
|
+
}
|
|
254
|
+
this.clearResponseTrackingStateForAppServerReconnect();
|
|
255
|
+
this.activeTurnIds.clear();
|
|
256
|
+
this.turnInProgress = false;
|
|
257
|
+
try {
|
|
258
|
+
await this.connectToAppServer(false);
|
|
259
|
+
this.log("App-server reconnected for new TUI session \u2014 replaying buffered messages");
|
|
260
|
+
const messages = this.pendingTuiMessages;
|
|
261
|
+
this.pendingTuiMessages = [];
|
|
262
|
+
this.reconnectingForNewSession = false;
|
|
263
|
+
this.replayingBufferedMessages = true;
|
|
264
|
+
try {
|
|
265
|
+
for (const msg of messages) {
|
|
266
|
+
this.onTuiMessage(tuiWs, msg);
|
|
267
|
+
}
|
|
268
|
+
} finally {
|
|
269
|
+
this.replayingBufferedMessages = false;
|
|
270
|
+
}
|
|
271
|
+
} catch (err) {
|
|
272
|
+
this.log(`Failed to reconnect app-server for new session: ${err.message}`);
|
|
273
|
+
this.pendingTuiMessages = [];
|
|
274
|
+
this.reconnectingForNewSession = false;
|
|
275
|
+
this.intentionalDisconnect = false;
|
|
276
|
+
this.scheduleReconnect();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
179
279
|
reconnectAttempts = 0;
|
|
180
280
|
reconnectTimer = null;
|
|
181
281
|
static MAX_RECONNECT_ATTEMPTS = 10;
|
|
@@ -201,6 +301,16 @@ class CodexAdapter extends EventEmitter {
|
|
|
201
301
|
}
|
|
202
302
|
}, delay);
|
|
203
303
|
}
|
|
304
|
+
handleAppServerClose() {
|
|
305
|
+
this.log("App-server connection closed");
|
|
306
|
+
this.appServerWs = null;
|
|
307
|
+
this.clearResponseTrackingState();
|
|
308
|
+
this.activeTurnIds.clear();
|
|
309
|
+
this.turnInProgress = false;
|
|
310
|
+
if (!this.intentionalDisconnect) {
|
|
311
|
+
this.scheduleReconnect();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
204
314
|
startProxy() {
|
|
205
315
|
const self = this;
|
|
206
316
|
this.proxyServer = Bun.serve({
|
|
@@ -208,40 +318,109 @@ class CodexAdapter extends EventEmitter {
|
|
|
208
318
|
hostname: "127.0.0.1",
|
|
209
319
|
fetch(req, server) {
|
|
210
320
|
const url = new URL(req.url);
|
|
321
|
+
const isUpgrade = req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
322
|
+
self.log(`HTTP ${req.method} ${url.pathname} (upgrade=${isUpgrade})`);
|
|
211
323
|
if (url.pathname === "/healthz" || url.pathname === "/readyz") {
|
|
212
324
|
return fetch(`http://127.0.0.1:${self.appPort}${url.pathname}`);
|
|
213
325
|
}
|
|
214
326
|
if (server.upgrade(req, { data: { connId: 0 } }))
|
|
215
327
|
return;
|
|
328
|
+
self.log(`WARNING: non-upgrade HTTP request not handled: ${req.method} ${url.pathname}`);
|
|
216
329
|
return new Response("AgentBridge Codex Proxy");
|
|
217
330
|
},
|
|
218
331
|
websocket: {
|
|
219
332
|
open: (ws) => self.onTuiConnect(ws),
|
|
220
|
-
close: (ws) =>
|
|
333
|
+
close: (ws, code, reason) => {
|
|
334
|
+
self.log(`WebSocket close event: conn #${ws.data.connId}, code=${code}, reason=${reason || "none"}`);
|
|
335
|
+
self.onTuiDisconnect(ws);
|
|
336
|
+
},
|
|
221
337
|
message: (ws, msg) => self.onTuiMessage(ws, msg)
|
|
222
338
|
}
|
|
223
339
|
});
|
|
224
340
|
}
|
|
225
341
|
onTuiConnect(ws) {
|
|
226
|
-
this.
|
|
227
|
-
ws.data.connId =
|
|
342
|
+
const connId = ++this.connIdCounter;
|
|
343
|
+
ws.data.connId = connId;
|
|
344
|
+
if (this.tuiWs) {
|
|
345
|
+
this.log(`Secondary TUI connected (conn #${connId}, primary is #${this.tuiConnId})`);
|
|
346
|
+
this.setupSecondaryConnection(ws, connId);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const previousConnId = this.tuiConnId > 0 ? this.tuiConnId : null;
|
|
350
|
+
this.tuiConnId = connId;
|
|
228
351
|
this.tuiWs = ws;
|
|
352
|
+
this.threadId = null;
|
|
229
353
|
this.log(`TUI connected (conn #${this.tuiConnId})`);
|
|
230
354
|
this.emit("tuiConnected", this.tuiConnId);
|
|
355
|
+
if (previousConnId !== null) {
|
|
356
|
+
this.retireConnectionState(previousConnId);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
setupSecondaryConnection(ws, connId) {
|
|
360
|
+
const appWs = new WebSocket(this.appServerUrl);
|
|
361
|
+
const entry = { tuiWs: ws, appServerWs: appWs, buffer: [] };
|
|
362
|
+
this.secondaryConnections.set(connId, entry);
|
|
363
|
+
appWs.onopen = () => {
|
|
364
|
+
if (!this.secondaryConnections.has(connId)) {
|
|
365
|
+
appWs.close();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
this.log(`Secondary conn #${connId}: app-server WS connected, flushing ${entry.buffer.length} buffered messages`);
|
|
369
|
+
for (const msg of entry.buffer) {
|
|
370
|
+
try {
|
|
371
|
+
appWs.send(msg);
|
|
372
|
+
} catch {}
|
|
373
|
+
}
|
|
374
|
+
entry.buffer = [];
|
|
375
|
+
};
|
|
376
|
+
appWs.onmessage = (event) => {
|
|
377
|
+
if (!this.secondaryConnections.has(connId))
|
|
378
|
+
return;
|
|
379
|
+
const data = typeof event.data === "string" ? event.data : event.data.toString();
|
|
380
|
+
try {
|
|
381
|
+
ws.send(data);
|
|
382
|
+
} catch {}
|
|
383
|
+
};
|
|
384
|
+
appWs.onerror = () => {
|
|
385
|
+
this.log(`Secondary conn #${connId}: app-server WS error`);
|
|
386
|
+
};
|
|
387
|
+
appWs.onclose = () => {
|
|
388
|
+
this.log(`Secondary conn #${connId}: app-server WS closed`);
|
|
389
|
+
const sec = this.secondaryConnections.get(connId);
|
|
390
|
+
if (sec) {
|
|
391
|
+
this.secondaryConnections.delete(connId);
|
|
392
|
+
try {
|
|
393
|
+
sec.tuiWs.close();
|
|
394
|
+
} catch {}
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
replayPendingForThread(resumedThreadId, ws) {
|
|
231
399
|
const remaining = [];
|
|
232
400
|
for (const buffered of this.pendingServerRequests) {
|
|
401
|
+
const belongsToThread = buffered.threadId === null || buffered.threadId === resumedThreadId;
|
|
402
|
+
if (!belongsToThread) {
|
|
403
|
+
remaining.push(buffered);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
233
406
|
const proxyId = this.nextProxyId++;
|
|
234
407
|
try {
|
|
235
408
|
const parsed = JSON.parse(buffered.raw);
|
|
236
409
|
parsed.id = proxyId;
|
|
237
410
|
ws.send(JSON.stringify(parsed));
|
|
238
411
|
this.serverRequestToProxy.set(proxyId, {
|
|
412
|
+
raw: buffered.raw,
|
|
239
413
|
serverId: buffered.serverId,
|
|
240
414
|
connId: this.tuiConnId,
|
|
241
415
|
method: buffered.method,
|
|
242
|
-
timestamp: Date.now()
|
|
416
|
+
timestamp: Date.now(),
|
|
417
|
+
threadId: buffered.threadId
|
|
243
418
|
});
|
|
244
|
-
|
|
419
|
+
if (buffered.threadId === null) {
|
|
420
|
+
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})`);
|
|
421
|
+
} else {
|
|
422
|
+
this.log(`Replayed buffered server request on thread/resume: ${buffered.method} (server id=${buffered.serverId} \u2192 proxy id=${proxyId}, threadId=${buffered.threadId})`);
|
|
423
|
+
}
|
|
245
424
|
} catch (e) {
|
|
246
425
|
this.log(`Failed to replay buffered server request: ${buffered.method} (server id=${buffered.serverId}): ${e.message}`);
|
|
247
426
|
remaining.push(buffered);
|
|
@@ -249,11 +428,41 @@ class CodexAdapter extends EventEmitter {
|
|
|
249
428
|
}
|
|
250
429
|
this.pendingServerRequests = remaining;
|
|
251
430
|
}
|
|
431
|
+
dropOrphanPendingRequests(reason, matchThreadId = null) {
|
|
432
|
+
if (this.pendingServerRequests.length === 0)
|
|
433
|
+
return;
|
|
434
|
+
const remaining = [];
|
|
435
|
+
for (const buffered of this.pendingServerRequests) {
|
|
436
|
+
const shouldDrop = matchThreadId === null ? true : buffered.threadId !== null && buffered.threadId !== matchThreadId;
|
|
437
|
+
if (shouldDrop) {
|
|
438
|
+
this.log(`Dropped orphan pending server request: ${buffered.method} (server id=${buffered.serverId}, threadId=${buffered.threadId ?? "unknown"}, reason=${reason})`);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
remaining.push(buffered);
|
|
442
|
+
}
|
|
443
|
+
this.pendingServerRequests = remaining;
|
|
444
|
+
}
|
|
252
445
|
onTuiDisconnect(ws) {
|
|
253
446
|
const connId = ws.data.connId;
|
|
447
|
+
const secondary = this.secondaryConnections.get(connId);
|
|
448
|
+
if (secondary) {
|
|
449
|
+
this.log(`Secondary TUI disconnected (conn #${connId})`);
|
|
450
|
+
this.secondaryConnections.delete(connId);
|
|
451
|
+
if (secondary.appServerWs) {
|
|
452
|
+
try {
|
|
453
|
+
secondary.appServerWs.close();
|
|
454
|
+
} catch {}
|
|
455
|
+
}
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
254
458
|
if (this.tuiWs === ws) {
|
|
255
459
|
this.log(`TUI disconnected (conn #${connId})`);
|
|
256
460
|
this.tuiWs = null;
|
|
461
|
+
if (this.reconnectingForNewSession) {
|
|
462
|
+
this.log("Clearing pending TUI message buffer (TUI disconnected during app-server reconnect)");
|
|
463
|
+
this.pendingTuiMessages = [];
|
|
464
|
+
this.reconnectingForNewSession = false;
|
|
465
|
+
}
|
|
257
466
|
this.emit("tuiDisconnected", connId);
|
|
258
467
|
} else {
|
|
259
468
|
this.log(`Stale TUI disconnected (conn #${connId}, current is #${this.tuiConnId})`);
|
|
@@ -263,6 +472,17 @@ class CodexAdapter extends EventEmitter {
|
|
|
263
472
|
onTuiMessage(ws, msg) {
|
|
264
473
|
const data = typeof msg === "string" ? msg : msg.toString();
|
|
265
474
|
const connId = ws.data.connId;
|
|
475
|
+
const secondary = this.secondaryConnections.get(connId);
|
|
476
|
+
if (secondary) {
|
|
477
|
+
if (secondary.appServerWs && secondary.appServerWs.readyState === WebSocket.OPEN) {
|
|
478
|
+
try {
|
|
479
|
+
secondary.appServerWs.send(data);
|
|
480
|
+
} catch {}
|
|
481
|
+
} else {
|
|
482
|
+
secondary.buffer.push(data);
|
|
483
|
+
}
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
266
486
|
if (connId !== this.tuiConnId) {
|
|
267
487
|
this.log(`Dropping message from stale TUI conn #${connId} (current is #${this.tuiConnId})`);
|
|
268
488
|
return;
|
|
@@ -271,29 +491,51 @@ class CodexAdapter extends EventEmitter {
|
|
|
271
491
|
const parsed = JSON.parse(data);
|
|
272
492
|
if (parsed.id !== undefined && !parsed.method) {
|
|
273
493
|
const normalizedId = this.normalizeNumericId(parsed.id);
|
|
494
|
+
if (!isNaN(normalizedId) && this.pendingServerResponses.has(normalizedId)) {
|
|
495
|
+
this.log(`Ignoring duplicate approval response while app-server reconnect is pending (proxy id=${normalizedId})`);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
274
498
|
const pending = !isNaN(normalizedId) ? this.serverRequestToProxy.get(normalizedId) : undefined;
|
|
275
499
|
if (pending !== undefined) {
|
|
276
500
|
if (pending.connId !== connId) {
|
|
277
501
|
this.log(`Dropping stale server request response (proxy id=${normalizedId}, expected conn #${pending.connId}, got #${connId})`);
|
|
278
502
|
return;
|
|
279
503
|
}
|
|
504
|
+
parsed.id = pending.serverId;
|
|
505
|
+
const forwardedResponse = JSON.stringify(parsed);
|
|
280
506
|
if (!this.appServerWs || this.appServerWs.readyState !== WebSocket.OPEN) {
|
|
281
|
-
this.
|
|
507
|
+
this.bufferPendingServerResponse(normalizedId, pending, forwardedResponse, "app-server disconnected");
|
|
282
508
|
return;
|
|
283
509
|
}
|
|
284
|
-
parsed.id = pending.serverId;
|
|
285
510
|
try {
|
|
286
|
-
this.appServerWs.send(
|
|
511
|
+
this.appServerWs.send(forwardedResponse);
|
|
287
512
|
this.serverRequestToProxy.delete(normalizedId);
|
|
288
513
|
this.log(`TUI \u2192 app-server: ${pending.method} response (proxy id=${normalizedId} \u2192 server id=${pending.serverId})`);
|
|
289
514
|
} catch (e) {
|
|
290
|
-
|
|
291
|
-
this.log(`Failed to forward approval response (proxy id=${normalizedId}): ${e.message}`);
|
|
515
|
+
this.bufferPendingServerResponse(normalizedId, pending, forwardedResponse, `send failed: ${e.message}`);
|
|
292
516
|
}
|
|
293
517
|
return;
|
|
294
518
|
}
|
|
295
519
|
}
|
|
296
520
|
} catch {}
|
|
521
|
+
let detectedMethod;
|
|
522
|
+
try {
|
|
523
|
+
const parsed = JSON.parse(data);
|
|
524
|
+
detectedMethod = typeof parsed.method === "string" ? parsed.method : undefined;
|
|
525
|
+
} catch {}
|
|
526
|
+
if (!this.replayingBufferedMessages) {
|
|
527
|
+
if (detectedMethod === "initialize") {
|
|
528
|
+
this.log("Detected initialize \u2014 reconnecting app-server for fresh session");
|
|
529
|
+
this.reconnectingForNewSession = true;
|
|
530
|
+
this.pendingTuiMessages = [data];
|
|
531
|
+
this.reconnectAppServerForNewSession(ws);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (this.reconnectingForNewSession) {
|
|
535
|
+
this.pendingTuiMessages.push(data);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
297
539
|
let forwarded = data;
|
|
298
540
|
try {
|
|
299
541
|
const parsed = JSON.parse(data);
|
|
@@ -320,16 +562,21 @@ class CodexAdapter extends EventEmitter {
|
|
|
320
562
|
handleAppServerPayload(raw) {
|
|
321
563
|
try {
|
|
322
564
|
const parsed = JSON.parse(raw);
|
|
323
|
-
if (parsed
|
|
324
|
-
const
|
|
325
|
-
this.
|
|
565
|
+
if (isAppServerNotification(parsed) || typeof parsed === "object" && parsed !== null && !("id" in parsed)) {
|
|
566
|
+
const notificationLike = parsed;
|
|
567
|
+
const forwarded = this.patchResponse(notificationLike, raw);
|
|
568
|
+
this.interceptServerMessage(notificationLike);
|
|
326
569
|
return forwarded;
|
|
327
570
|
}
|
|
328
|
-
if (parsed
|
|
571
|
+
if (isAppServerRequestMessage(parsed)) {
|
|
329
572
|
this.handleServerRequest(parsed, raw);
|
|
330
573
|
return null;
|
|
331
574
|
}
|
|
332
|
-
|
|
575
|
+
if (isAppServerResponseMessage(parsed)) {
|
|
576
|
+
return this.handleAppServerResponse(parsed, raw);
|
|
577
|
+
}
|
|
578
|
+
this.log(`Dropping unclassifiable app-server message: ${raw.slice(0, 100)}`);
|
|
579
|
+
return null;
|
|
333
580
|
} catch {
|
|
334
581
|
return raw;
|
|
335
582
|
}
|
|
@@ -337,9 +584,10 @@ class CodexAdapter extends EventEmitter {
|
|
|
337
584
|
handleServerRequest(parsed, raw) {
|
|
338
585
|
const serverId = parsed.id;
|
|
339
586
|
const method = parsed.method;
|
|
587
|
+
const threadId = this.extractThreadIdFromParams(parsed.params);
|
|
340
588
|
if (!this.tuiWs) {
|
|
341
|
-
this.pendingServerRequests.push({ raw, serverId, method });
|
|
342
|
-
this.log(`Server request buffered (no TUI): ${method} (server id=${serverId})`);
|
|
589
|
+
this.pendingServerRequests.push({ raw, serverId, method, threadId });
|
|
590
|
+
this.log(`Server request buffered (no TUI): ${method} (server id=${serverId}, threadId=${threadId ?? "unknown"})`);
|
|
343
591
|
return;
|
|
344
592
|
}
|
|
345
593
|
const proxyId = this.nextProxyId++;
|
|
@@ -348,11 +596,24 @@ class CodexAdapter extends EventEmitter {
|
|
|
348
596
|
this.tuiWs.send(JSON.stringify(parsed));
|
|
349
597
|
} catch (e) {
|
|
350
598
|
this.log(`Server request send failed, buffering: ${method} (server id=${serverId}): ${e.message}`);
|
|
351
|
-
this.pendingServerRequests.push({ raw, serverId, method });
|
|
599
|
+
this.pendingServerRequests.push({ raw, serverId, method, threadId });
|
|
352
600
|
return;
|
|
353
601
|
}
|
|
354
|
-
this.serverRequestToProxy.set(proxyId, {
|
|
355
|
-
|
|
602
|
+
this.serverRequestToProxy.set(proxyId, {
|
|
603
|
+
raw,
|
|
604
|
+
serverId,
|
|
605
|
+
connId: this.tuiConnId,
|
|
606
|
+
method,
|
|
607
|
+
timestamp: Date.now(),
|
|
608
|
+
threadId
|
|
609
|
+
});
|
|
610
|
+
this.log(`Server request: ${method} (server id=${serverId} \u2192 proxy id=${proxyId}, conn #${this.tuiConnId}, threadId=${threadId ?? "unknown"})`);
|
|
611
|
+
}
|
|
612
|
+
extractThreadIdFromParams(params) {
|
|
613
|
+
if (typeof params !== "object" || params === null)
|
|
614
|
+
return null;
|
|
615
|
+
const tid = params.threadId;
|
|
616
|
+
return typeof tid === "string" && tid.length > 0 ? tid : null;
|
|
356
617
|
}
|
|
357
618
|
normalizeNumericId(id) {
|
|
358
619
|
if (typeof id === "number")
|
|
@@ -361,6 +622,30 @@ class CodexAdapter extends EventEmitter {
|
|
|
361
622
|
return Number(id);
|
|
362
623
|
return NaN;
|
|
363
624
|
}
|
|
625
|
+
bufferPendingServerResponse(proxyId, pending, forwardedResponse, reason) {
|
|
626
|
+
this.pendingServerResponses.set(proxyId, {
|
|
627
|
+
raw: forwardedResponse,
|
|
628
|
+
serverId: pending.serverId,
|
|
629
|
+
method: pending.method,
|
|
630
|
+
timestamp: Date.now()
|
|
631
|
+
});
|
|
632
|
+
this.serverRequestToProxy.delete(proxyId);
|
|
633
|
+
this.log(`Buffered approval response until app-server reconnect (${reason}) (proxy id=${proxyId} \u2192 server id=${pending.serverId})`);
|
|
634
|
+
}
|
|
635
|
+
flushPendingServerResponses() {
|
|
636
|
+
if (!this.appServerWs || this.appServerWs.readyState !== WebSocket.OPEN)
|
|
637
|
+
return;
|
|
638
|
+
for (const [proxyId, pending] of this.pendingServerResponses.entries()) {
|
|
639
|
+
try {
|
|
640
|
+
this.appServerWs.send(pending.raw);
|
|
641
|
+
this.pendingServerResponses.delete(proxyId);
|
|
642
|
+
this.log(`Flushed buffered approval response after app-server reconnect (proxy id=${proxyId} \u2192 server id=${pending.serverId})`);
|
|
643
|
+
} catch (e) {
|
|
644
|
+
this.log(`Failed to flush buffered approval response (proxy id=${proxyId}): ${e.message}`);
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
364
649
|
handleAppServerResponse(parsed, raw) {
|
|
365
650
|
const responseId = parsed.id;
|
|
366
651
|
const numericId = this.normalizeNumericId(responseId);
|
|
@@ -372,6 +657,7 @@ class CodexAdapter extends EventEmitter {
|
|
|
372
657
|
return null;
|
|
373
658
|
}
|
|
374
659
|
parsed.id = mapping.clientId;
|
|
660
|
+
this.log(`app-server \u2192 TUI: response (proxy id=${numericId} \u2192 client id=${String(mapping.clientId)}, conn #${mapping.connId})`);
|
|
375
661
|
const forwarded = this.patchResponse(parsed, JSON.stringify(parsed));
|
|
376
662
|
this.interceptServerMessage(parsed, mapping.connId);
|
|
377
663
|
return forwarded;
|
|
@@ -392,7 +678,7 @@ class CodexAdapter extends EventEmitter {
|
|
|
392
678
|
return null;
|
|
393
679
|
}
|
|
394
680
|
patchResponse(parsed, raw) {
|
|
395
|
-
if (parsed.error && parsed.id !== undefined) {
|
|
681
|
+
if (isAppServerResponseMessage(parsed) && parsed.error && parsed.id !== undefined) {
|
|
396
682
|
const errMsg = parsed.error.message ?? "";
|
|
397
683
|
if (errMsg.includes("rate limits") || errMsg.includes("rateLimits")) {
|
|
398
684
|
this.log(`Patching rateLimits error \u2192 mock success (id: ${parsed.id})`);
|
|
@@ -411,24 +697,14 @@ class CodexAdapter extends EventEmitter {
|
|
|
411
697
|
}
|
|
412
698
|
});
|
|
413
699
|
}
|
|
414
|
-
if (errMsg.includes("Already initialized")) {
|
|
415
|
-
this.log(`Patching "Already initialized" error (id: ${parsed.id})`);
|
|
416
|
-
return JSON.stringify({
|
|
417
|
-
id: parsed.id,
|
|
418
|
-
result: {
|
|
419
|
-
userAgent: "agent_bridge/0.1.0",
|
|
420
|
-
platformFamily: "unix",
|
|
421
|
-
platformOs: "macos"
|
|
422
|
-
}
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
700
|
}
|
|
426
701
|
return raw;
|
|
427
702
|
}
|
|
428
703
|
interceptServerMessage(msg, connId) {
|
|
429
704
|
this.handleTrackedResponse(msg, connId);
|
|
430
|
-
if (msg.method)
|
|
705
|
+
if ("method" in msg && typeof msg.method === "string" && isAppServerNotification(msg)) {
|
|
431
706
|
this.handleServerNotification(msg);
|
|
707
|
+
}
|
|
432
708
|
}
|
|
433
709
|
handleServerNotification(msg) {
|
|
434
710
|
const { method, params } = msg;
|
|
@@ -443,7 +719,10 @@ class CodexAdapter extends EventEmitter {
|
|
|
443
719
|
break;
|
|
444
720
|
}
|
|
445
721
|
case "item/agentMessage/delta": {
|
|
446
|
-
const
|
|
722
|
+
const itemId = params?.itemId;
|
|
723
|
+
if (typeof itemId !== "string")
|
|
724
|
+
break;
|
|
725
|
+
const buf = this.agentMessageBuffers.get(itemId);
|
|
447
726
|
if (buf && params?.delta)
|
|
448
727
|
buf.push(params.delta);
|
|
449
728
|
break;
|
|
@@ -488,14 +767,15 @@ class CodexAdapter extends EventEmitter {
|
|
|
488
767
|
return `${connId ?? this.tuiConnId}:${base}`;
|
|
489
768
|
}
|
|
490
769
|
trackPendingRequest(message, connId, _proxyId) {
|
|
491
|
-
const
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
if (!key || !
|
|
770
|
+
const rpcId = "id" in message ? message.id : undefined;
|
|
771
|
+
const method = "method" in message && typeof message.method === "string" ? message.method : undefined;
|
|
772
|
+
const key = this.pendingKey(rpcId, connId);
|
|
773
|
+
if (!key || !isTrackedAppServerRequestMethod(method))
|
|
495
774
|
return;
|
|
496
775
|
const pending = { method };
|
|
497
776
|
if (method === "turn/start") {
|
|
498
|
-
const
|
|
777
|
+
const params = "params" in message && typeof message.params === "object" && message.params !== null ? message.params : undefined;
|
|
778
|
+
const threadId = params?.threadId;
|
|
499
779
|
if (typeof threadId === "string" && threadId.length > 0) {
|
|
500
780
|
pending.threadId = threadId;
|
|
501
781
|
}
|
|
@@ -527,12 +807,17 @@ class CodexAdapter extends EventEmitter {
|
|
|
527
807
|
if (typeof threadId === "string" && threadId.length > 0) {
|
|
528
808
|
this.setActiveThreadId(threadId, `thread/start response ${key}`);
|
|
529
809
|
}
|
|
810
|
+
this.dropOrphanPendingRequests(`thread/start (new session)`);
|
|
530
811
|
break;
|
|
531
812
|
}
|
|
532
813
|
case "thread/resume": {
|
|
533
814
|
const threadId = message?.result?.thread?.id;
|
|
534
815
|
if (typeof threadId === "string" && threadId.length > 0) {
|
|
535
816
|
this.setActiveThreadId(threadId, `thread/resume response ${key}`);
|
|
817
|
+
if (this.tuiWs) {
|
|
818
|
+
this.replayPendingForThread(threadId, this.tuiWs);
|
|
819
|
+
}
|
|
820
|
+
this.dropOrphanPendingRequests(`thread/resume to ${threadId}`, threadId);
|
|
536
821
|
}
|
|
537
822
|
break;
|
|
538
823
|
}
|
|
@@ -592,20 +877,22 @@ class CodexAdapter extends EventEmitter {
|
|
|
592
877
|
this.upstreamToClient.delete(upId);
|
|
593
878
|
this.trackStaleProxyId(upId);
|
|
594
879
|
}
|
|
880
|
+
const requeuedServerRequests = [];
|
|
595
881
|
for (const [proxyId, pending] of this.serverRequestToProxy.entries()) {
|
|
596
882
|
if (pending.connId === connId) {
|
|
597
|
-
this.
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
},
|
|
605
|
-
timer.unref?.();
|
|
606
|
-
this.serverRequestTtlTimers.set(proxyId, timer);
|
|
883
|
+
this.serverRequestToProxy.delete(proxyId);
|
|
884
|
+
requeuedServerRequests.push({
|
|
885
|
+
raw: pending.raw,
|
|
886
|
+
serverId: pending.serverId,
|
|
887
|
+
method: pending.method,
|
|
888
|
+
threadId: pending.threadId
|
|
889
|
+
});
|
|
890
|
+
this.log(`Requeued in-flight server request after TUI disconnect (proxy id=${proxyId}, server id=${pending.serverId}, method=${pending.method}, threadId=${pending.threadId ?? "unknown"})`);
|
|
607
891
|
}
|
|
608
892
|
}
|
|
893
|
+
if (requeuedServerRequests.length === 0)
|
|
894
|
+
return;
|
|
895
|
+
this.pendingServerRequests.push(...requeuedServerRequests);
|
|
609
896
|
}
|
|
610
897
|
trackStaleProxyId(proxyId) {
|
|
611
898
|
this.clearTrackedId(this.staleProxyIds, proxyId);
|
|
@@ -640,7 +927,7 @@ class CodexAdapter extends EventEmitter {
|
|
|
640
927
|
store.delete(id);
|
|
641
928
|
return true;
|
|
642
929
|
}
|
|
643
|
-
|
|
930
|
+
clearTransientResponseTrackingState() {
|
|
644
931
|
this.pendingRequests.clear();
|
|
645
932
|
this.upstreamToClient.clear();
|
|
646
933
|
for (const timer of this.staleProxyIds.values()) {
|
|
@@ -651,12 +938,26 @@ class CodexAdapter extends EventEmitter {
|
|
|
651
938
|
clearTimeout(timer);
|
|
652
939
|
}
|
|
653
940
|
this.bridgeRequestIds.clear();
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
this.serverRequestTtlTimers.clear();
|
|
941
|
+
}
|
|
942
|
+
clearResponseTrackingState() {
|
|
943
|
+
this.clearTransientResponseTrackingState();
|
|
658
944
|
this.serverRequestToProxy.clear();
|
|
659
945
|
this.pendingServerRequests = [];
|
|
946
|
+
this.pendingServerResponses.clear();
|
|
947
|
+
}
|
|
948
|
+
clearResponseTrackingStateForAppServerReconnect() {
|
|
949
|
+
this.clearTransientResponseTrackingState();
|
|
950
|
+
for (const pending of this.serverRequestToProxy.values()) {
|
|
951
|
+
this.pendingServerRequests.push({
|
|
952
|
+
raw: pending.raw,
|
|
953
|
+
serverId: pending.serverId,
|
|
954
|
+
method: pending.method,
|
|
955
|
+
threadId: pending.threadId
|
|
956
|
+
});
|
|
957
|
+
this.log(`Requeued in-flight server request on app-server reconnect (server id=${pending.serverId}, method=${pending.method}, threadId=${pending.threadId ?? "unknown"})`);
|
|
958
|
+
}
|
|
959
|
+
this.serverRequestToProxy.clear();
|
|
960
|
+
this.pendingServerResponses.clear();
|
|
660
961
|
}
|
|
661
962
|
async checkPorts() {
|
|
662
963
|
for (const port of [this.appPort, this.proxyPort]) {
|
|
@@ -1344,6 +1645,9 @@ class ConfigService {
|
|
|
1344
1645
|
}
|
|
1345
1646
|
}
|
|
1346
1647
|
|
|
1648
|
+
// src/control-protocol.ts
|
|
1649
|
+
var CLOSE_CODE_REPLACED = 4001;
|
|
1650
|
+
|
|
1347
1651
|
// src/daemon.ts
|
|
1348
1652
|
var stateDir = new StateDirResolver;
|
|
1349
1653
|
stateDir.ensure();
|
|
@@ -1585,8 +1889,10 @@ function handleControlMessage(ws, raw) {
|
|
|
1585
1889
|
}
|
|
1586
1890
|
}
|
|
1587
1891
|
function attachClaude(ws) {
|
|
1588
|
-
if (attachedClaude && attachedClaude !== ws) {
|
|
1589
|
-
|
|
1892
|
+
if (attachedClaude && attachedClaude !== ws && attachedClaude.readyState !== WebSocket.CLOSED) {
|
|
1893
|
+
log(`Rejecting Claude frontend #${ws.data.clientId} \u2014 another session (#${attachedClaude.data.clientId}) is already attached (readyState=${attachedClaude.readyState})`);
|
|
1894
|
+
ws.close(CLOSE_CODE_REPLACED, "another Claude session is already connected");
|
|
1895
|
+
return;
|
|
1590
1896
|
}
|
|
1591
1897
|
clearPendingClaudeDisconnect("Claude frontend attached");
|
|
1592
1898
|
attachedClaude = ws;
|