@raysonmeng/agentbridge 0.1.4 → 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
|
}
|
|
@@ -67,6 +67,8 @@ class CodexAdapter extends EventEmitter {
|
|
|
67
67
|
appPort;
|
|
68
68
|
proxyPort;
|
|
69
69
|
tuiConnId = 0;
|
|
70
|
+
connIdCounter = 0;
|
|
71
|
+
secondaryConnections = new Map;
|
|
70
72
|
agentMessageBuffers = new Map;
|
|
71
73
|
pendingRequests = new Map;
|
|
72
74
|
activeTurnIds = new Set;
|
|
@@ -74,11 +76,15 @@ class CodexAdapter extends EventEmitter {
|
|
|
74
76
|
nextProxyId = 1e5;
|
|
75
77
|
upstreamToClient = new Map;
|
|
76
78
|
serverRequestToProxy = new Map;
|
|
77
|
-
serverRequestTtlTimers = new Map;
|
|
78
79
|
pendingServerRequests = [];
|
|
80
|
+
pendingServerResponses = new Map;
|
|
79
81
|
staleProxyIds = new Map;
|
|
80
82
|
bridgeRequestIds = new Map;
|
|
81
83
|
intentionalDisconnect = false;
|
|
84
|
+
pendingTuiMessages = [];
|
|
85
|
+
reconnectingForNewSession = false;
|
|
86
|
+
replayingBufferedMessages = false;
|
|
87
|
+
appServerGeneration = 0;
|
|
82
88
|
constructor(appPort = 4500, proxyPort = 4501) {
|
|
83
89
|
super();
|
|
84
90
|
this.appPort = appPort;
|
|
@@ -119,6 +125,12 @@ class CodexAdapter extends EventEmitter {
|
|
|
119
125
|
}
|
|
120
126
|
this.appServerWs?.close();
|
|
121
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
|
+
}
|
|
122
134
|
this.proxyServer?.stop();
|
|
123
135
|
this.proxyServer = null;
|
|
124
136
|
this.clearResponseTrackingState();
|
|
@@ -179,16 +191,24 @@ class CodexAdapter extends EventEmitter {
|
|
|
179
191
|
throw new Error("Codex app-server failed to become healthy");
|
|
180
192
|
}
|
|
181
193
|
connectToAppServer(isReconnect = false) {
|
|
194
|
+
const generation = ++this.appServerGeneration;
|
|
182
195
|
return new Promise((resolve, reject) => {
|
|
183
196
|
const appWs = new WebSocket(this.appServerUrl);
|
|
184
197
|
appWs.onopen = () => {
|
|
198
|
+
if (this.appServerGeneration !== generation) {
|
|
199
|
+
appWs.close();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
185
202
|
this.appServerWs = appWs;
|
|
186
203
|
this.intentionalDisconnect = false;
|
|
187
204
|
this.reconnectAttempts = 0;
|
|
188
|
-
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();
|
|
189
207
|
resolve();
|
|
190
208
|
};
|
|
191
209
|
appWs.onmessage = (event) => {
|
|
210
|
+
if (this.appServerGeneration !== generation)
|
|
211
|
+
return;
|
|
192
212
|
const data = typeof event.data === "string" ? event.data : event.data.toString();
|
|
193
213
|
const forwarded = this.handleAppServerPayload(data);
|
|
194
214
|
if (forwarded === null)
|
|
@@ -204,22 +224,58 @@ class CodexAdapter extends EventEmitter {
|
|
|
204
224
|
}
|
|
205
225
|
};
|
|
206
226
|
appWs.onerror = () => {
|
|
227
|
+
if (this.appServerGeneration !== generation)
|
|
228
|
+
return;
|
|
207
229
|
this.log("App-server connection error");
|
|
208
230
|
if (!isReconnect)
|
|
209
231
|
reject(new Error("Failed to connect to app-server"));
|
|
210
232
|
};
|
|
211
233
|
appWs.onclose = () => {
|
|
212
|
-
this.
|
|
213
|
-
|
|
214
|
-
this.
|
|
215
|
-
this.activeTurnIds.clear();
|
|
216
|
-
this.turnInProgress = false;
|
|
217
|
-
if (!this.intentionalDisconnect) {
|
|
218
|
-
this.scheduleReconnect();
|
|
219
|
-
}
|
|
234
|
+
if (this.appServerGeneration !== generation)
|
|
235
|
+
return;
|
|
236
|
+
this.handleAppServerClose();
|
|
220
237
|
};
|
|
221
238
|
});
|
|
222
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
|
+
}
|
|
223
279
|
reconnectAttempts = 0;
|
|
224
280
|
reconnectTimer = null;
|
|
225
281
|
static MAX_RECONNECT_ATTEMPTS = 10;
|
|
@@ -245,6 +301,16 @@ class CodexAdapter extends EventEmitter {
|
|
|
245
301
|
}
|
|
246
302
|
}, delay);
|
|
247
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
|
+
}
|
|
248
314
|
startProxy() {
|
|
249
315
|
const self = this;
|
|
250
316
|
this.proxyServer = Bun.serve({
|
|
@@ -252,40 +318,109 @@ class CodexAdapter extends EventEmitter {
|
|
|
252
318
|
hostname: "127.0.0.1",
|
|
253
319
|
fetch(req, server) {
|
|
254
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})`);
|
|
255
323
|
if (url.pathname === "/healthz" || url.pathname === "/readyz") {
|
|
256
324
|
return fetch(`http://127.0.0.1:${self.appPort}${url.pathname}`);
|
|
257
325
|
}
|
|
258
326
|
if (server.upgrade(req, { data: { connId: 0 } }))
|
|
259
327
|
return;
|
|
328
|
+
self.log(`WARNING: non-upgrade HTTP request not handled: ${req.method} ${url.pathname}`);
|
|
260
329
|
return new Response("AgentBridge Codex Proxy");
|
|
261
330
|
},
|
|
262
331
|
websocket: {
|
|
263
332
|
open: (ws) => self.onTuiConnect(ws),
|
|
264
|
-
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
|
+
},
|
|
265
337
|
message: (ws, msg) => self.onTuiMessage(ws, msg)
|
|
266
338
|
}
|
|
267
339
|
});
|
|
268
340
|
}
|
|
269
341
|
onTuiConnect(ws) {
|
|
270
|
-
this.
|
|
271
|
-
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;
|
|
272
351
|
this.tuiWs = ws;
|
|
352
|
+
this.threadId = null;
|
|
273
353
|
this.log(`TUI connected (conn #${this.tuiConnId})`);
|
|
274
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) {
|
|
275
399
|
const remaining = [];
|
|
276
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
|
+
}
|
|
277
406
|
const proxyId = this.nextProxyId++;
|
|
278
407
|
try {
|
|
279
408
|
const parsed = JSON.parse(buffered.raw);
|
|
280
409
|
parsed.id = proxyId;
|
|
281
410
|
ws.send(JSON.stringify(parsed));
|
|
282
411
|
this.serverRequestToProxy.set(proxyId, {
|
|
412
|
+
raw: buffered.raw,
|
|
283
413
|
serverId: buffered.serverId,
|
|
284
414
|
connId: this.tuiConnId,
|
|
285
415
|
method: buffered.method,
|
|
286
|
-
timestamp: Date.now()
|
|
416
|
+
timestamp: Date.now(),
|
|
417
|
+
threadId: buffered.threadId
|
|
287
418
|
});
|
|
288
|
-
|
|
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
|
+
}
|
|
289
424
|
} catch (e) {
|
|
290
425
|
this.log(`Failed to replay buffered server request: ${buffered.method} (server id=${buffered.serverId}): ${e.message}`);
|
|
291
426
|
remaining.push(buffered);
|
|
@@ -293,11 +428,41 @@ class CodexAdapter extends EventEmitter {
|
|
|
293
428
|
}
|
|
294
429
|
this.pendingServerRequests = remaining;
|
|
295
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
|
+
}
|
|
296
445
|
onTuiDisconnect(ws) {
|
|
297
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
|
+
}
|
|
298
458
|
if (this.tuiWs === ws) {
|
|
299
459
|
this.log(`TUI disconnected (conn #${connId})`);
|
|
300
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
|
+
}
|
|
301
466
|
this.emit("tuiDisconnected", connId);
|
|
302
467
|
} else {
|
|
303
468
|
this.log(`Stale TUI disconnected (conn #${connId}, current is #${this.tuiConnId})`);
|
|
@@ -307,6 +472,17 @@ class CodexAdapter extends EventEmitter {
|
|
|
307
472
|
onTuiMessage(ws, msg) {
|
|
308
473
|
const data = typeof msg === "string" ? msg : msg.toString();
|
|
309
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
|
+
}
|
|
310
486
|
if (connId !== this.tuiConnId) {
|
|
311
487
|
this.log(`Dropping message from stale TUI conn #${connId} (current is #${this.tuiConnId})`);
|
|
312
488
|
return;
|
|
@@ -315,29 +491,51 @@ class CodexAdapter extends EventEmitter {
|
|
|
315
491
|
const parsed = JSON.parse(data);
|
|
316
492
|
if (parsed.id !== undefined && !parsed.method) {
|
|
317
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
|
+
}
|
|
318
498
|
const pending = !isNaN(normalizedId) ? this.serverRequestToProxy.get(normalizedId) : undefined;
|
|
319
499
|
if (pending !== undefined) {
|
|
320
500
|
if (pending.connId !== connId) {
|
|
321
501
|
this.log(`Dropping stale server request response (proxy id=${normalizedId}, expected conn #${pending.connId}, got #${connId})`);
|
|
322
502
|
return;
|
|
323
503
|
}
|
|
504
|
+
parsed.id = pending.serverId;
|
|
505
|
+
const forwardedResponse = JSON.stringify(parsed);
|
|
324
506
|
if (!this.appServerWs || this.appServerWs.readyState !== WebSocket.OPEN) {
|
|
325
|
-
this.
|
|
507
|
+
this.bufferPendingServerResponse(normalizedId, pending, forwardedResponse, "app-server disconnected");
|
|
326
508
|
return;
|
|
327
509
|
}
|
|
328
|
-
parsed.id = pending.serverId;
|
|
329
510
|
try {
|
|
330
|
-
this.appServerWs.send(
|
|
511
|
+
this.appServerWs.send(forwardedResponse);
|
|
331
512
|
this.serverRequestToProxy.delete(normalizedId);
|
|
332
513
|
this.log(`TUI \u2192 app-server: ${pending.method} response (proxy id=${normalizedId} \u2192 server id=${pending.serverId})`);
|
|
333
514
|
} catch (e) {
|
|
334
|
-
|
|
335
|
-
this.log(`Failed to forward approval response (proxy id=${normalizedId}): ${e.message}`);
|
|
515
|
+
this.bufferPendingServerResponse(normalizedId, pending, forwardedResponse, `send failed: ${e.message}`);
|
|
336
516
|
}
|
|
337
517
|
return;
|
|
338
518
|
}
|
|
339
519
|
}
|
|
340
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
|
+
}
|
|
341
539
|
let forwarded = data;
|
|
342
540
|
try {
|
|
343
541
|
const parsed = JSON.parse(data);
|
|
@@ -386,9 +584,10 @@ class CodexAdapter extends EventEmitter {
|
|
|
386
584
|
handleServerRequest(parsed, raw) {
|
|
387
585
|
const serverId = parsed.id;
|
|
388
586
|
const method = parsed.method;
|
|
587
|
+
const threadId = this.extractThreadIdFromParams(parsed.params);
|
|
389
588
|
if (!this.tuiWs) {
|
|
390
|
-
this.pendingServerRequests.push({ raw, serverId, method });
|
|
391
|
-
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"})`);
|
|
392
591
|
return;
|
|
393
592
|
}
|
|
394
593
|
const proxyId = this.nextProxyId++;
|
|
@@ -397,11 +596,24 @@ class CodexAdapter extends EventEmitter {
|
|
|
397
596
|
this.tuiWs.send(JSON.stringify(parsed));
|
|
398
597
|
} catch (e) {
|
|
399
598
|
this.log(`Server request send failed, buffering: ${method} (server id=${serverId}): ${e.message}`);
|
|
400
|
-
this.pendingServerRequests.push({ raw, serverId, method });
|
|
599
|
+
this.pendingServerRequests.push({ raw, serverId, method, threadId });
|
|
401
600
|
return;
|
|
402
601
|
}
|
|
403
|
-
this.serverRequestToProxy.set(proxyId, {
|
|
404
|
-
|
|
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;
|
|
405
617
|
}
|
|
406
618
|
normalizeNumericId(id) {
|
|
407
619
|
if (typeof id === "number")
|
|
@@ -410,6 +622,30 @@ class CodexAdapter extends EventEmitter {
|
|
|
410
622
|
return Number(id);
|
|
411
623
|
return NaN;
|
|
412
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
|
+
}
|
|
413
649
|
handleAppServerResponse(parsed, raw) {
|
|
414
650
|
const responseId = parsed.id;
|
|
415
651
|
const numericId = this.normalizeNumericId(responseId);
|
|
@@ -421,6 +657,7 @@ class CodexAdapter extends EventEmitter {
|
|
|
421
657
|
return null;
|
|
422
658
|
}
|
|
423
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})`);
|
|
424
661
|
const forwarded = this.patchResponse(parsed, JSON.stringify(parsed));
|
|
425
662
|
this.interceptServerMessage(parsed, mapping.connId);
|
|
426
663
|
return forwarded;
|
|
@@ -460,17 +697,6 @@ class CodexAdapter extends EventEmitter {
|
|
|
460
697
|
}
|
|
461
698
|
});
|
|
462
699
|
}
|
|
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
700
|
}
|
|
475
701
|
return raw;
|
|
476
702
|
}
|
|
@@ -544,7 +770,6 @@ class CodexAdapter extends EventEmitter {
|
|
|
544
770
|
const rpcId = "id" in message ? message.id : undefined;
|
|
545
771
|
const method = "method" in message && typeof message.method === "string" ? message.method : undefined;
|
|
546
772
|
const key = this.pendingKey(rpcId, connId);
|
|
547
|
-
this.log(`[track] method=${method} id=${rpcId} (type=${typeof rpcId}) key=${key}`);
|
|
548
773
|
if (!key || !isTrackedAppServerRequestMethod(method))
|
|
549
774
|
return;
|
|
550
775
|
const pending = { method };
|
|
@@ -582,12 +807,17 @@ class CodexAdapter extends EventEmitter {
|
|
|
582
807
|
if (typeof threadId === "string" && threadId.length > 0) {
|
|
583
808
|
this.setActiveThreadId(threadId, `thread/start response ${key}`);
|
|
584
809
|
}
|
|
810
|
+
this.dropOrphanPendingRequests(`thread/start (new session)`);
|
|
585
811
|
break;
|
|
586
812
|
}
|
|
587
813
|
case "thread/resume": {
|
|
588
814
|
const threadId = message?.result?.thread?.id;
|
|
589
815
|
if (typeof threadId === "string" && threadId.length > 0) {
|
|
590
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);
|
|
591
821
|
}
|
|
592
822
|
break;
|
|
593
823
|
}
|
|
@@ -647,20 +877,22 @@ class CodexAdapter extends EventEmitter {
|
|
|
647
877
|
this.upstreamToClient.delete(upId);
|
|
648
878
|
this.trackStaleProxyId(upId);
|
|
649
879
|
}
|
|
880
|
+
const requeuedServerRequests = [];
|
|
650
881
|
for (const [proxyId, pending] of this.serverRequestToProxy.entries()) {
|
|
651
882
|
if (pending.connId === connId) {
|
|
652
|
-
this.
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
},
|
|
660
|
-
timer.unref?.();
|
|
661
|
-
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"})`);
|
|
662
891
|
}
|
|
663
892
|
}
|
|
893
|
+
if (requeuedServerRequests.length === 0)
|
|
894
|
+
return;
|
|
895
|
+
this.pendingServerRequests.push(...requeuedServerRequests);
|
|
664
896
|
}
|
|
665
897
|
trackStaleProxyId(proxyId) {
|
|
666
898
|
this.clearTrackedId(this.staleProxyIds, proxyId);
|
|
@@ -695,7 +927,7 @@ class CodexAdapter extends EventEmitter {
|
|
|
695
927
|
store.delete(id);
|
|
696
928
|
return true;
|
|
697
929
|
}
|
|
698
|
-
|
|
930
|
+
clearTransientResponseTrackingState() {
|
|
699
931
|
this.pendingRequests.clear();
|
|
700
932
|
this.upstreamToClient.clear();
|
|
701
933
|
for (const timer of this.staleProxyIds.values()) {
|
|
@@ -706,12 +938,26 @@ class CodexAdapter extends EventEmitter {
|
|
|
706
938
|
clearTimeout(timer);
|
|
707
939
|
}
|
|
708
940
|
this.bridgeRequestIds.clear();
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
this.serverRequestTtlTimers.clear();
|
|
941
|
+
}
|
|
942
|
+
clearResponseTrackingState() {
|
|
943
|
+
this.clearTransientResponseTrackingState();
|
|
713
944
|
this.serverRequestToProxy.clear();
|
|
714
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();
|
|
715
961
|
}
|
|
716
962
|
async checkPorts() {
|
|
717
963
|
for (const port of [this.appPort, this.proxyPort]) {
|
|
@@ -1399,6 +1645,9 @@ class ConfigService {
|
|
|
1399
1645
|
}
|
|
1400
1646
|
}
|
|
1401
1647
|
|
|
1648
|
+
// src/control-protocol.ts
|
|
1649
|
+
var CLOSE_CODE_REPLACED = 4001;
|
|
1650
|
+
|
|
1402
1651
|
// src/daemon.ts
|
|
1403
1652
|
var stateDir = new StateDirResolver;
|
|
1404
1653
|
stateDir.ensure();
|
|
@@ -1640,8 +1889,10 @@ function handleControlMessage(ws, raw) {
|
|
|
1640
1889
|
}
|
|
1641
1890
|
}
|
|
1642
1891
|
function attachClaude(ws) {
|
|
1643
|
-
if (attachedClaude && attachedClaude !== ws) {
|
|
1644
|
-
|
|
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;
|
|
1645
1896
|
}
|
|
1646
1897
|
clearPendingClaudeDisconnect("Claude frontend attached");
|
|
1647
1898
|
attachedClaude = ws;
|