@raysonmeng/agentbridge 0.1.7 → 0.1.8
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/dist/cli.js +6 -5
- package/dist/daemon.js +90 -11
- package/package.json +1 -1
- package/plugins/agentbridge/.claude-plugin/plugin.json +1 -1
- package/plugins/agentbridge/server/bridge-server.js +29 -9
- package/plugins/agentbridge/server/daemon.js +90 -11
|
@@ -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.8",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "AgentBridge Contributors",
|
|
18
18
|
"email": "raysonmeng@qq.com"
|
package/dist/cli.js
CHANGED
|
@@ -120,7 +120,7 @@ function parsePositiveIntEnv(name, fallback, log = () => {}, env = process.env)
|
|
|
120
120
|
var require_package = __commonJS((exports, module) => {
|
|
121
121
|
module.exports = {
|
|
122
122
|
name: "@raysonmeng/agentbridge",
|
|
123
|
-
version: "0.1.
|
|
123
|
+
version: "0.1.8",
|
|
124
124
|
description: "Bridge between Claude Code and Codex \u2014 bidirectional agent communication via MCP Channel + JSON-RPC",
|
|
125
125
|
type: "module",
|
|
126
126
|
packageManager: "bun@1.3.11",
|
|
@@ -1047,7 +1047,7 @@ var init_daemon_client = __esm(() => {
|
|
|
1047
1047
|
this.ws = null;
|
|
1048
1048
|
this.rejectPendingReplies("Daemon connection closed");
|
|
1049
1049
|
}
|
|
1050
|
-
async sendReply(message, requireReply) {
|
|
1050
|
+
async sendReply(message, requireReply, onBusy) {
|
|
1051
1051
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1052
1052
|
return { success: false, error: "AgentBridge daemon is not connected." };
|
|
1053
1053
|
}
|
|
@@ -1062,7 +1062,8 @@ var init_daemon_client = __esm(() => {
|
|
|
1062
1062
|
type: "claude_to_codex",
|
|
1063
1063
|
requestId,
|
|
1064
1064
|
message,
|
|
1065
|
-
...requireReply ? { requireReply: true } : {}
|
|
1065
|
+
...requireReply ? { requireReply: true } : {},
|
|
1066
|
+
...onBusy && onBusy !== "reject" ? { onBusy } : {}
|
|
1066
1067
|
});
|
|
1067
1068
|
});
|
|
1068
1069
|
}
|
|
@@ -1164,8 +1165,8 @@ function formatBuildInfo(build) {
|
|
|
1164
1165
|
var BUILD_INFO;
|
|
1165
1166
|
var init_build_info = __esm(() => {
|
|
1166
1167
|
BUILD_INFO = Object.freeze({
|
|
1167
|
-
version: defineString("0.1.
|
|
1168
|
-
commit: defineString("
|
|
1168
|
+
version: defineString("0.1.8", "0.0.0-source"),
|
|
1169
|
+
commit: defineString("c80a7fd", "source"),
|
|
1169
1170
|
bundle: defineBundle("dist"),
|
|
1170
1171
|
contractVersion: defineNumber(1, CONTRACT_VERSION)
|
|
1171
1172
|
});
|
package/dist/daemon.js
CHANGED
|
@@ -17,8 +17,8 @@ function defineNumber(value, fallback) {
|
|
|
17
17
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
18
18
|
}
|
|
19
19
|
var BUILD_INFO = Object.freeze({
|
|
20
|
-
version: defineString("0.1.
|
|
21
|
-
commit: defineString("
|
|
20
|
+
version: defineString("0.1.8", "0.0.0-source"),
|
|
21
|
+
commit: defineString("c80a7fd", "source"),
|
|
22
22
|
bundle: defineBundle("dist"),
|
|
23
23
|
contractVersion: defineNumber(1, CONTRACT_VERSION)
|
|
24
24
|
});
|
|
@@ -639,6 +639,7 @@ class CodexAdapter extends EventEmitter {
|
|
|
639
639
|
pendingServerResponses = new Map;
|
|
640
640
|
staleProxyIds = new Map;
|
|
641
641
|
bridgeRequestIds = new Map;
|
|
642
|
+
bridgeRequestKinds = new Map;
|
|
642
643
|
intentionalDisconnect = false;
|
|
643
644
|
pendingTuiMessages = [];
|
|
644
645
|
reconnectingForNewSession = false;
|
|
@@ -797,6 +798,36 @@ class CodexAdapter extends EventEmitter {
|
|
|
797
798
|
return false;
|
|
798
799
|
}
|
|
799
800
|
}
|
|
801
|
+
steerMessage(text) {
|
|
802
|
+
if (!this.threadId) {
|
|
803
|
+
this.log("Cannot steer: no active thread");
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
if (!this.appServerWs || this.appServerWs.readyState !== WebSocket.OPEN) {
|
|
807
|
+
this.log("Cannot steer: app-server WebSocket not connected");
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
if (!this.turnInProgress) {
|
|
811
|
+
this.log("Cannot steer: no turn in progress (use injectMessage)");
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
this.log(`Steering message into active Codex turn (${text.length} chars)`);
|
|
815
|
+
const requestId = this.nextInjectionId--;
|
|
816
|
+
this.trackBridgeRequestId(requestId, "steer");
|
|
817
|
+
const params = { threadId: this.threadId, input: [{ type: "text", text }] };
|
|
818
|
+
try {
|
|
819
|
+
this.appServerWs.send(JSON.stringify({
|
|
820
|
+
method: "turn/steer",
|
|
821
|
+
id: requestId,
|
|
822
|
+
params
|
|
823
|
+
}));
|
|
824
|
+
return true;
|
|
825
|
+
} catch (err) {
|
|
826
|
+
this.untrackBridgeRequestId(requestId);
|
|
827
|
+
this.log(`Steer send failed: ${err.message}`);
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
800
831
|
async waitForHealthy(maxRetries = 20, delayMs = 500) {
|
|
801
832
|
for (let i = 0;i < maxRetries; i++) {
|
|
802
833
|
try {
|
|
@@ -1519,14 +1550,22 @@ class CodexAdapter extends EventEmitter {
|
|
|
1519
1550
|
this.interceptServerMessage(parsed, mapping.connId);
|
|
1520
1551
|
return forwarded;
|
|
1521
1552
|
}
|
|
1522
|
-
|
|
1553
|
+
const bridgeKind = !isNaN(numericId) ? this.consumeBridgeRequestId(numericId) : null;
|
|
1554
|
+
if (bridgeKind) {
|
|
1523
1555
|
if (parsed.error) {
|
|
1524
|
-
this.log(`Bridge-originated request failed (id ${responseId}): ${parsed.error.message ?? "unknown error"}`);
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1556
|
+
this.log(`Bridge-originated ${bridgeKind} request failed (id ${responseId}): ${parsed.error.message ?? "unknown error"}`);
|
|
1557
|
+
if (bridgeKind === "steer") {
|
|
1558
|
+
this.emit("steerFailed", parsed.error.message ?? "unknown error");
|
|
1559
|
+
} else {
|
|
1560
|
+
this.lastTurnEndedAbnormally = true;
|
|
1561
|
+
this.emit("turnAborted", `injected turn/start rejected: ${parsed.error.message ?? "unknown error"}`);
|
|
1562
|
+
this.notifyPhaseIfChanged();
|
|
1563
|
+
}
|
|
1528
1564
|
} else {
|
|
1529
|
-
this.log(`Bridge-originated request completed (id ${responseId})`);
|
|
1565
|
+
this.log(`Bridge-originated ${bridgeKind} request completed (id ${responseId})`);
|
|
1566
|
+
if (bridgeKind === "steer") {
|
|
1567
|
+
this.emit("steerAccepted");
|
|
1568
|
+
}
|
|
1530
1569
|
}
|
|
1531
1570
|
return null;
|
|
1532
1571
|
}
|
|
@@ -1877,18 +1916,23 @@ class CodexAdapter extends EventEmitter {
|
|
|
1877
1916
|
consumeStaleProxyId(proxyId) {
|
|
1878
1917
|
return this.clearTrackedId(this.staleProxyIds, proxyId);
|
|
1879
1918
|
}
|
|
1880
|
-
trackBridgeRequestId(requestId) {
|
|
1919
|
+
trackBridgeRequestId(requestId, kind = "turn-start") {
|
|
1881
1920
|
this.clearTrackedId(this.bridgeRequestIds, requestId);
|
|
1882
1921
|
const timer = setTimeout(() => {
|
|
1883
1922
|
this.bridgeRequestIds.delete(requestId);
|
|
1923
|
+
this.bridgeRequestKinds.delete(requestId);
|
|
1884
1924
|
}, CodexAdapter.RESPONSE_TRACKING_TTL_MS);
|
|
1885
1925
|
timer.unref?.();
|
|
1886
1926
|
this.bridgeRequestIds.set(requestId, timer);
|
|
1927
|
+
this.bridgeRequestKinds.set(requestId, kind);
|
|
1887
1928
|
}
|
|
1888
1929
|
consumeBridgeRequestId(requestId) {
|
|
1889
|
-
|
|
1930
|
+
const kind = this.bridgeRequestKinds.get(requestId) ?? "turn-start";
|
|
1931
|
+
this.bridgeRequestKinds.delete(requestId);
|
|
1932
|
+
return this.clearTrackedId(this.bridgeRequestIds, requestId) ? kind : null;
|
|
1890
1933
|
}
|
|
1891
1934
|
untrackBridgeRequestId(requestId) {
|
|
1935
|
+
this.bridgeRequestKinds.delete(requestId);
|
|
1892
1936
|
this.clearTrackedId(this.bridgeRequestIds, requestId);
|
|
1893
1937
|
}
|
|
1894
1938
|
clearTrackedId(store, id) {
|
|
@@ -1910,6 +1954,7 @@ class CodexAdapter extends EventEmitter {
|
|
|
1910
1954
|
clearTimeout(timer);
|
|
1911
1955
|
}
|
|
1912
1956
|
this.bridgeRequestIds.clear();
|
|
1957
|
+
this.bridgeRequestKinds.clear();
|
|
1913
1958
|
}
|
|
1914
1959
|
clearResponseTrackingState() {
|
|
1915
1960
|
this.clearTransientResponseTrackingState();
|
|
@@ -3947,6 +3992,13 @@ codex.on("turnPhaseChanged", ({ phase, previous }) => {
|
|
|
3947
3992
|
tryWriteStatusFile(`turnPhase:${phase}`);
|
|
3948
3993
|
broadcastStatus();
|
|
3949
3994
|
});
|
|
3995
|
+
codex.on("steerFailed", (reason) => {
|
|
3996
|
+
log(`Steer rejected by app-server: ${reason}`);
|
|
3997
|
+
emitToClaude(systemMessage("system_steer_failed", `\u26A0\uFE0F Your steer message did NOT reach Codex (${reason}). The original turn continues unaffected \u2014 wait for it to finish, or resend as a normal reply.`));
|
|
3998
|
+
});
|
|
3999
|
+
codex.on("steerAccepted", () => {
|
|
4000
|
+
log("Steer accepted by app-server");
|
|
4001
|
+
});
|
|
3950
4002
|
codex.on("turnStarted", () => {
|
|
3951
4003
|
log("Codex turn started");
|
|
3952
4004
|
emitToClaude(systemMessage("system_turn_started", "\u23F3 Codex is working on the current task. Wait for completion before sending a reply."));
|
|
@@ -4184,9 +4236,36 @@ function handleControlMessage(ws, raw) {
|
|
|
4184
4236
|
}
|
|
4185
4237
|
log(`Forwarding Claude \u2192 Codex (${message.message.content.length} chars, requireReply=${requireReply})`);
|
|
4186
4238
|
const tierOverrides = BUDGET_CONFIG.codexTierControl ? budgetCoordinator?.getCodexTurnOverrides() ?? undefined : undefined;
|
|
4239
|
+
if (codex.turnInProgress && message.onBusy === "steer") {
|
|
4240
|
+
if (requireReply) {
|
|
4241
|
+
sendProtocolMessage(ws, {
|
|
4242
|
+
type: "claude_to_codex_result",
|
|
4243
|
+
requestId: message.requestId,
|
|
4244
|
+
success: false,
|
|
4245
|
+
error: 'require_reply is not supported together with on_busy="steer" yet. Send the steer without require_reply, or wait for the turn to finish.'
|
|
4246
|
+
});
|
|
4247
|
+
return;
|
|
4248
|
+
}
|
|
4249
|
+
const steerContent = `[STEER from Claude]
|
|
4250
|
+
` + `Mid-turn update for the current Codex turn. Integrate if relevant; do not restart work unless explicitly requested.
|
|
4251
|
+
|
|
4252
|
+
` + message.message.content;
|
|
4253
|
+
const steered = codex.steerMessage(steerContent);
|
|
4254
|
+
log(`Steer ${steered ? "transport-accepted" : "failed"} (${message.message.content.length} chars)`);
|
|
4255
|
+
if (steered) {
|
|
4256
|
+
clearAttentionWindow();
|
|
4257
|
+
}
|
|
4258
|
+
sendProtocolMessage(ws, {
|
|
4259
|
+
type: "claude_to_codex_result",
|
|
4260
|
+
requestId: message.requestId,
|
|
4261
|
+
success: steered,
|
|
4262
|
+
error: steered ? undefined : "Steer failed: the turn may have just ended or the connection dropped \u2014 retry as a normal reply."
|
|
4263
|
+
});
|
|
4264
|
+
return;
|
|
4265
|
+
}
|
|
4187
4266
|
const injected = codex.injectMessage(contentToSend, tierOverrides);
|
|
4188
4267
|
if (!injected) {
|
|
4189
|
-
const reason = codex.turnInProgress ?
|
|
4268
|
+
const reason = codex.turnInProgress ? 'Codex is busy executing a turn. Options: wait for it to finish, or retry with on_busy="steer" to feed this message into the running turn without interrupting it.' : "Injection failed: no active thread or WebSocket not connected.";
|
|
4190
4269
|
log(`Injection rejected: ${reason}`);
|
|
4191
4270
|
sendProtocolMessage(ws, {
|
|
4192
4271
|
type: "claude_to_codex_result",
|
package/package.json
CHANGED
|
@@ -13947,7 +13947,7 @@ var CLAUDE_INSTRUCTIONS = [
|
|
|
13947
13947
|
"## Turn coordination",
|
|
13948
13948
|
"- When you see '\u23F3 Codex is working', do NOT call the reply tool \u2014 wait for '\u2705 Codex finished'.",
|
|
13949
13949
|
"- After Codex finishes a turn, you have an attention window to review and respond before new messages arrive.",
|
|
13950
|
-
|
|
13950
|
+
'- If the reply tool returns a busy error, Codex is still executing. You decide: wait and retry later, or resend with on_busy="steer" to feed the message INTO the running turn (good for mid-course corrections; it does not interrupt or restart the work).',
|
|
13951
13951
|
"",
|
|
13952
13952
|
"## Budget awareness",
|
|
13953
13953
|
"- Use the get_budget tool to check both agents' subscription quota (5h/weekly windows, drift, pause state).",
|
|
@@ -14099,6 +14099,11 @@ ${formatted}`
|
|
|
14099
14099
|
require_reply: {
|
|
14100
14100
|
type: "boolean",
|
|
14101
14101
|
description: "When true, Codex is required to send a reply. All Codex messages from this turn will be forwarded immediately (bypassing STATUS buffering). Use this when you need a direct answer from Codex."
|
|
14102
|
+
},
|
|
14103
|
+
on_busy: {
|
|
14104
|
+
type: "string",
|
|
14105
|
+
enum: ["reject", "steer"],
|
|
14106
|
+
description: `What to do when Codex is mid-turn. "reject" (default): fail with a busy error \u2014 wait and retry. "steer": feed this message INTO the running turn \u2014 Codex sees it immediately and integrates it without losing work. Use steer for mid-course corrections, added constraints, or updated acceptance criteria; it does NOT start a new turn, so don't combine it with require_reply. If you need Codex to STOP and do something else, wait for the turn to finish (interrupt support is coming separately).`
|
|
14102
14107
|
}
|
|
14103
14108
|
},
|
|
14104
14109
|
required: ["text"]
|
|
@@ -14157,6 +14162,20 @@ ${formatted}`
|
|
|
14157
14162
|
};
|
|
14158
14163
|
}
|
|
14159
14164
|
const requireReply = args?.require_reply === true;
|
|
14165
|
+
const onBusyRaw = args?.on_busy;
|
|
14166
|
+
const onBusy = onBusyRaw === "steer" ? "steer" : "reject";
|
|
14167
|
+
if (onBusyRaw !== undefined && onBusyRaw !== "reject" && onBusyRaw !== "steer") {
|
|
14168
|
+
return {
|
|
14169
|
+
content: [{ type: "text", text: `Error: invalid on_busy value ${JSON.stringify(onBusyRaw)} \u2014 use "reject" or "steer".` }],
|
|
14170
|
+
isError: true
|
|
14171
|
+
};
|
|
14172
|
+
}
|
|
14173
|
+
if (onBusy === "steer" && requireReply) {
|
|
14174
|
+
return {
|
|
14175
|
+
content: [{ type: "text", text: 'Error: require_reply cannot be combined with on_busy="steer" yet \u2014 a steer joins the RUNNING turn instead of starting a new one, so reply tracking would mis-arm. Send the steer without require_reply.' }],
|
|
14176
|
+
isError: true
|
|
14177
|
+
};
|
|
14178
|
+
}
|
|
14160
14179
|
const bridgeMsg = {
|
|
14161
14180
|
id: args?.chat_id ?? `reply_${Date.now()}`,
|
|
14162
14181
|
source: "claude",
|
|
@@ -14170,7 +14189,7 @@ ${formatted}`
|
|
|
14170
14189
|
isError: true
|
|
14171
14190
|
};
|
|
14172
14191
|
}
|
|
14173
|
-
const result = await this.replySender(bridgeMsg, requireReply);
|
|
14192
|
+
const result = await this.replySender(bridgeMsg, requireReply, onBusy);
|
|
14174
14193
|
if (!result.success) {
|
|
14175
14194
|
this.log(`Reply delivery failed: ${result.error}`);
|
|
14176
14195
|
return {
|
|
@@ -14179,7 +14198,7 @@ ${formatted}`
|
|
|
14179
14198
|
};
|
|
14180
14199
|
}
|
|
14181
14200
|
const pending = this.pendingMessages.length;
|
|
14182
|
-
let responseText = "Reply sent to Codex.";
|
|
14201
|
+
let responseText = onBusy === "steer" ? "Reply sent to Codex (will be steered into the running turn if one is active; watch for a system_steer_failed notice if the app-server rejects it)." : "Reply sent to Codex.";
|
|
14183
14202
|
if (pending > 0) {
|
|
14184
14203
|
responseText += ` Note: ${pending} unread Codex message${pending > 1 ? "s" : ""} already waiting \u2014 call get_messages to read them.`;
|
|
14185
14204
|
}
|
|
@@ -14208,8 +14227,8 @@ function defineNumber(value, fallback) {
|
|
|
14208
14227
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
14209
14228
|
}
|
|
14210
14229
|
var BUILD_INFO = Object.freeze({
|
|
14211
|
-
version: defineString("0.1.
|
|
14212
|
-
commit: defineString("
|
|
14230
|
+
version: defineString("0.1.8", "0.0.0-source"),
|
|
14231
|
+
commit: defineString("c80a7fd", "source"),
|
|
14213
14232
|
bundle: defineBundle("plugin"),
|
|
14214
14233
|
contractVersion: defineNumber(1, CONTRACT_VERSION)
|
|
14215
14234
|
});
|
|
@@ -14381,7 +14400,7 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14381
14400
|
this.ws = null;
|
|
14382
14401
|
this.rejectPendingReplies("Daemon connection closed");
|
|
14383
14402
|
}
|
|
14384
|
-
async sendReply(message, requireReply) {
|
|
14403
|
+
async sendReply(message, requireReply, onBusy) {
|
|
14385
14404
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
14386
14405
|
return { success: false, error: "AgentBridge daemon is not connected." };
|
|
14387
14406
|
}
|
|
@@ -14396,7 +14415,8 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14396
14415
|
type: "claude_to_codex",
|
|
14397
14416
|
requestId,
|
|
14398
14417
|
message,
|
|
14399
|
-
...requireReply ? { requireReply: true } : {}
|
|
14418
|
+
...requireReply ? { requireReply: true } : {},
|
|
14419
|
+
...onBusy && onBusy !== "reject" ? { onBusy } : {}
|
|
14400
14420
|
});
|
|
14401
14421
|
});
|
|
14402
14422
|
}
|
|
@@ -15416,7 +15436,7 @@ if (process.env.AGENTBRIDGE_TRACE === "1") {
|
|
|
15416
15436
|
});
|
|
15417
15437
|
} catch {}
|
|
15418
15438
|
}
|
|
15419
|
-
claude.setReplySender(async (msg, requireReply) => {
|
|
15439
|
+
claude.setReplySender(async (msg, requireReply, onBusy) => {
|
|
15420
15440
|
if (msg.source !== "claude") {
|
|
15421
15441
|
return { success: false, error: "Invalid message source" };
|
|
15422
15442
|
}
|
|
@@ -15426,7 +15446,7 @@ claude.setReplySender(async (msg, requireReply) => {
|
|
|
15426
15446
|
error: disabledReplyError(daemonDisabledReason ?? "killed")
|
|
15427
15447
|
};
|
|
15428
15448
|
}
|
|
15429
|
-
return daemonClient.sendReply(msg, requireReply);
|
|
15449
|
+
return daemonClient.sendReply(msg, requireReply, onBusy);
|
|
15430
15450
|
});
|
|
15431
15451
|
daemonClient.on("codexMessage", (message) => {
|
|
15432
15452
|
log(`Forwarding daemon \u2192 Claude (${message.content.length} chars)`);
|
|
@@ -17,8 +17,8 @@ function defineNumber(value, fallback) {
|
|
|
17
17
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
18
18
|
}
|
|
19
19
|
var BUILD_INFO = Object.freeze({
|
|
20
|
-
version: defineString("0.1.
|
|
21
|
-
commit: defineString("
|
|
20
|
+
version: defineString("0.1.8", "0.0.0-source"),
|
|
21
|
+
commit: defineString("c80a7fd", "source"),
|
|
22
22
|
bundle: defineBundle("plugin"),
|
|
23
23
|
contractVersion: defineNumber(1, CONTRACT_VERSION)
|
|
24
24
|
});
|
|
@@ -639,6 +639,7 @@ class CodexAdapter extends EventEmitter {
|
|
|
639
639
|
pendingServerResponses = new Map;
|
|
640
640
|
staleProxyIds = new Map;
|
|
641
641
|
bridgeRequestIds = new Map;
|
|
642
|
+
bridgeRequestKinds = new Map;
|
|
642
643
|
intentionalDisconnect = false;
|
|
643
644
|
pendingTuiMessages = [];
|
|
644
645
|
reconnectingForNewSession = false;
|
|
@@ -797,6 +798,36 @@ class CodexAdapter extends EventEmitter {
|
|
|
797
798
|
return false;
|
|
798
799
|
}
|
|
799
800
|
}
|
|
801
|
+
steerMessage(text) {
|
|
802
|
+
if (!this.threadId) {
|
|
803
|
+
this.log("Cannot steer: no active thread");
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
if (!this.appServerWs || this.appServerWs.readyState !== WebSocket.OPEN) {
|
|
807
|
+
this.log("Cannot steer: app-server WebSocket not connected");
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
if (!this.turnInProgress) {
|
|
811
|
+
this.log("Cannot steer: no turn in progress (use injectMessage)");
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
this.log(`Steering message into active Codex turn (${text.length} chars)`);
|
|
815
|
+
const requestId = this.nextInjectionId--;
|
|
816
|
+
this.trackBridgeRequestId(requestId, "steer");
|
|
817
|
+
const params = { threadId: this.threadId, input: [{ type: "text", text }] };
|
|
818
|
+
try {
|
|
819
|
+
this.appServerWs.send(JSON.stringify({
|
|
820
|
+
method: "turn/steer",
|
|
821
|
+
id: requestId,
|
|
822
|
+
params
|
|
823
|
+
}));
|
|
824
|
+
return true;
|
|
825
|
+
} catch (err) {
|
|
826
|
+
this.untrackBridgeRequestId(requestId);
|
|
827
|
+
this.log(`Steer send failed: ${err.message}`);
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
800
831
|
async waitForHealthy(maxRetries = 20, delayMs = 500) {
|
|
801
832
|
for (let i = 0;i < maxRetries; i++) {
|
|
802
833
|
try {
|
|
@@ -1519,14 +1550,22 @@ class CodexAdapter extends EventEmitter {
|
|
|
1519
1550
|
this.interceptServerMessage(parsed, mapping.connId);
|
|
1520
1551
|
return forwarded;
|
|
1521
1552
|
}
|
|
1522
|
-
|
|
1553
|
+
const bridgeKind = !isNaN(numericId) ? this.consumeBridgeRequestId(numericId) : null;
|
|
1554
|
+
if (bridgeKind) {
|
|
1523
1555
|
if (parsed.error) {
|
|
1524
|
-
this.log(`Bridge-originated request failed (id ${responseId}): ${parsed.error.message ?? "unknown error"}`);
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1556
|
+
this.log(`Bridge-originated ${bridgeKind} request failed (id ${responseId}): ${parsed.error.message ?? "unknown error"}`);
|
|
1557
|
+
if (bridgeKind === "steer") {
|
|
1558
|
+
this.emit("steerFailed", parsed.error.message ?? "unknown error");
|
|
1559
|
+
} else {
|
|
1560
|
+
this.lastTurnEndedAbnormally = true;
|
|
1561
|
+
this.emit("turnAborted", `injected turn/start rejected: ${parsed.error.message ?? "unknown error"}`);
|
|
1562
|
+
this.notifyPhaseIfChanged();
|
|
1563
|
+
}
|
|
1528
1564
|
} else {
|
|
1529
|
-
this.log(`Bridge-originated request completed (id ${responseId})`);
|
|
1565
|
+
this.log(`Bridge-originated ${bridgeKind} request completed (id ${responseId})`);
|
|
1566
|
+
if (bridgeKind === "steer") {
|
|
1567
|
+
this.emit("steerAccepted");
|
|
1568
|
+
}
|
|
1530
1569
|
}
|
|
1531
1570
|
return null;
|
|
1532
1571
|
}
|
|
@@ -1877,18 +1916,23 @@ class CodexAdapter extends EventEmitter {
|
|
|
1877
1916
|
consumeStaleProxyId(proxyId) {
|
|
1878
1917
|
return this.clearTrackedId(this.staleProxyIds, proxyId);
|
|
1879
1918
|
}
|
|
1880
|
-
trackBridgeRequestId(requestId) {
|
|
1919
|
+
trackBridgeRequestId(requestId, kind = "turn-start") {
|
|
1881
1920
|
this.clearTrackedId(this.bridgeRequestIds, requestId);
|
|
1882
1921
|
const timer = setTimeout(() => {
|
|
1883
1922
|
this.bridgeRequestIds.delete(requestId);
|
|
1923
|
+
this.bridgeRequestKinds.delete(requestId);
|
|
1884
1924
|
}, CodexAdapter.RESPONSE_TRACKING_TTL_MS);
|
|
1885
1925
|
timer.unref?.();
|
|
1886
1926
|
this.bridgeRequestIds.set(requestId, timer);
|
|
1927
|
+
this.bridgeRequestKinds.set(requestId, kind);
|
|
1887
1928
|
}
|
|
1888
1929
|
consumeBridgeRequestId(requestId) {
|
|
1889
|
-
|
|
1930
|
+
const kind = this.bridgeRequestKinds.get(requestId) ?? "turn-start";
|
|
1931
|
+
this.bridgeRequestKinds.delete(requestId);
|
|
1932
|
+
return this.clearTrackedId(this.bridgeRequestIds, requestId) ? kind : null;
|
|
1890
1933
|
}
|
|
1891
1934
|
untrackBridgeRequestId(requestId) {
|
|
1935
|
+
this.bridgeRequestKinds.delete(requestId);
|
|
1892
1936
|
this.clearTrackedId(this.bridgeRequestIds, requestId);
|
|
1893
1937
|
}
|
|
1894
1938
|
clearTrackedId(store, id) {
|
|
@@ -1910,6 +1954,7 @@ class CodexAdapter extends EventEmitter {
|
|
|
1910
1954
|
clearTimeout(timer);
|
|
1911
1955
|
}
|
|
1912
1956
|
this.bridgeRequestIds.clear();
|
|
1957
|
+
this.bridgeRequestKinds.clear();
|
|
1913
1958
|
}
|
|
1914
1959
|
clearResponseTrackingState() {
|
|
1915
1960
|
this.clearTransientResponseTrackingState();
|
|
@@ -3947,6 +3992,13 @@ codex.on("turnPhaseChanged", ({ phase, previous }) => {
|
|
|
3947
3992
|
tryWriteStatusFile(`turnPhase:${phase}`);
|
|
3948
3993
|
broadcastStatus();
|
|
3949
3994
|
});
|
|
3995
|
+
codex.on("steerFailed", (reason) => {
|
|
3996
|
+
log(`Steer rejected by app-server: ${reason}`);
|
|
3997
|
+
emitToClaude(systemMessage("system_steer_failed", `\u26A0\uFE0F Your steer message did NOT reach Codex (${reason}). The original turn continues unaffected \u2014 wait for it to finish, or resend as a normal reply.`));
|
|
3998
|
+
});
|
|
3999
|
+
codex.on("steerAccepted", () => {
|
|
4000
|
+
log("Steer accepted by app-server");
|
|
4001
|
+
});
|
|
3950
4002
|
codex.on("turnStarted", () => {
|
|
3951
4003
|
log("Codex turn started");
|
|
3952
4004
|
emitToClaude(systemMessage("system_turn_started", "\u23F3 Codex is working on the current task. Wait for completion before sending a reply."));
|
|
@@ -4184,9 +4236,36 @@ function handleControlMessage(ws, raw) {
|
|
|
4184
4236
|
}
|
|
4185
4237
|
log(`Forwarding Claude \u2192 Codex (${message.message.content.length} chars, requireReply=${requireReply})`);
|
|
4186
4238
|
const tierOverrides = BUDGET_CONFIG.codexTierControl ? budgetCoordinator?.getCodexTurnOverrides() ?? undefined : undefined;
|
|
4239
|
+
if (codex.turnInProgress && message.onBusy === "steer") {
|
|
4240
|
+
if (requireReply) {
|
|
4241
|
+
sendProtocolMessage(ws, {
|
|
4242
|
+
type: "claude_to_codex_result",
|
|
4243
|
+
requestId: message.requestId,
|
|
4244
|
+
success: false,
|
|
4245
|
+
error: 'require_reply is not supported together with on_busy="steer" yet. Send the steer without require_reply, or wait for the turn to finish.'
|
|
4246
|
+
});
|
|
4247
|
+
return;
|
|
4248
|
+
}
|
|
4249
|
+
const steerContent = `[STEER from Claude]
|
|
4250
|
+
` + `Mid-turn update for the current Codex turn. Integrate if relevant; do not restart work unless explicitly requested.
|
|
4251
|
+
|
|
4252
|
+
` + message.message.content;
|
|
4253
|
+
const steered = codex.steerMessage(steerContent);
|
|
4254
|
+
log(`Steer ${steered ? "transport-accepted" : "failed"} (${message.message.content.length} chars)`);
|
|
4255
|
+
if (steered) {
|
|
4256
|
+
clearAttentionWindow();
|
|
4257
|
+
}
|
|
4258
|
+
sendProtocolMessage(ws, {
|
|
4259
|
+
type: "claude_to_codex_result",
|
|
4260
|
+
requestId: message.requestId,
|
|
4261
|
+
success: steered,
|
|
4262
|
+
error: steered ? undefined : "Steer failed: the turn may have just ended or the connection dropped \u2014 retry as a normal reply."
|
|
4263
|
+
});
|
|
4264
|
+
return;
|
|
4265
|
+
}
|
|
4187
4266
|
const injected = codex.injectMessage(contentToSend, tierOverrides);
|
|
4188
4267
|
if (!injected) {
|
|
4189
|
-
const reason = codex.turnInProgress ?
|
|
4268
|
+
const reason = codex.turnInProgress ? 'Codex is busy executing a turn. Options: wait for it to finish, or retry with on_busy="steer" to feed this message into the running turn without interrupting it.' : "Injection failed: no active thread or WebSocket not connected.";
|
|
4190
4269
|
log(`Injection rejected: ${reason}`);
|
|
4191
4270
|
sendProtocolMessage(ws, {
|
|
4192
4271
|
type: "claude_to_codex_result",
|