@raysonmeng/agentbridge 0.1.7 → 0.1.9
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 +110 -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 +110 -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.9",
|
|
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.9",
|
|
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.9", "0.0.0-source"),
|
|
1169
|
+
commit: defineString("10dfd58", "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.9", "0.0.0-source"),
|
|
21
|
+
commit: defineString("10dfd58", "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,45 @@ 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
|
+
const expectedTurnId = this.currentSteerableTurnId();
|
|
815
|
+
if (!expectedTurnId) {
|
|
816
|
+
this.log("Cannot steer: no addressable active turn id (turn/started carried no id)");
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
this.log(`Steering message into active Codex turn ${expectedTurnId} (${text.length} chars)`);
|
|
820
|
+
const requestId = this.nextInjectionId--;
|
|
821
|
+
this.trackBridgeRequestId(requestId, "steer");
|
|
822
|
+
const params = {
|
|
823
|
+
threadId: this.threadId,
|
|
824
|
+
expectedTurnId,
|
|
825
|
+
input: [{ type: "text", text }]
|
|
826
|
+
};
|
|
827
|
+
try {
|
|
828
|
+
this.appServerWs.send(JSON.stringify({
|
|
829
|
+
method: "turn/steer",
|
|
830
|
+
id: requestId,
|
|
831
|
+
params
|
|
832
|
+
}));
|
|
833
|
+
return true;
|
|
834
|
+
} catch (err) {
|
|
835
|
+
this.untrackBridgeRequestId(requestId);
|
|
836
|
+
this.log(`Steer send failed: ${err.message}`);
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
800
840
|
async waitForHealthy(maxRetries = 20, delayMs = 500) {
|
|
801
841
|
for (let i = 0;i < maxRetries; i++) {
|
|
802
842
|
try {
|
|
@@ -1519,14 +1559,22 @@ class CodexAdapter extends EventEmitter {
|
|
|
1519
1559
|
this.interceptServerMessage(parsed, mapping.connId);
|
|
1520
1560
|
return forwarded;
|
|
1521
1561
|
}
|
|
1522
|
-
|
|
1562
|
+
const bridgeKind = !isNaN(numericId) ? this.consumeBridgeRequestId(numericId) : null;
|
|
1563
|
+
if (bridgeKind) {
|
|
1523
1564
|
if (parsed.error) {
|
|
1524
|
-
this.log(`Bridge-originated request failed (id ${responseId}): ${parsed.error.message ?? "unknown error"}`);
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1565
|
+
this.log(`Bridge-originated ${bridgeKind} request failed (id ${responseId}): ${parsed.error.message ?? "unknown error"}`);
|
|
1566
|
+
if (bridgeKind === "steer") {
|
|
1567
|
+
this.emit("steerFailed", parsed.error.message ?? "unknown error");
|
|
1568
|
+
} else {
|
|
1569
|
+
this.lastTurnEndedAbnormally = true;
|
|
1570
|
+
this.emit("turnAborted", `injected turn/start rejected: ${parsed.error.message ?? "unknown error"}`);
|
|
1571
|
+
this.notifyPhaseIfChanged();
|
|
1572
|
+
}
|
|
1528
1573
|
} else {
|
|
1529
|
-
this.log(`Bridge-originated request completed (id ${responseId})`);
|
|
1574
|
+
this.log(`Bridge-originated ${bridgeKind} request completed (id ${responseId})`);
|
|
1575
|
+
if (bridgeKind === "steer") {
|
|
1576
|
+
this.emit("steerAccepted");
|
|
1577
|
+
}
|
|
1530
1578
|
}
|
|
1531
1579
|
return null;
|
|
1532
1580
|
}
|
|
@@ -1719,6 +1767,14 @@ class CodexAdapter extends EventEmitter {
|
|
|
1719
1767
|
this.log(`Thread detected: ${threadId} (${reason})`);
|
|
1720
1768
|
this.emit("ready", threadId);
|
|
1721
1769
|
}
|
|
1770
|
+
currentSteerableTurnId() {
|
|
1771
|
+
let newest = null;
|
|
1772
|
+
for (const id of this.activeTurnIds) {
|
|
1773
|
+
if (!id.startsWith("unknown:"))
|
|
1774
|
+
newest = id;
|
|
1775
|
+
}
|
|
1776
|
+
return newest;
|
|
1777
|
+
}
|
|
1722
1778
|
get turnPhase() {
|
|
1723
1779
|
if (this.activeTurnIds.size > 0) {
|
|
1724
1780
|
const allStalled = [...this.activeTurnIds].every((id) => this.currentlyStalledTurnIds.has(id));
|
|
@@ -1737,6 +1793,7 @@ class CodexAdapter extends EventEmitter {
|
|
|
1737
1793
|
markTurnStarted(turnId) {
|
|
1738
1794
|
const wasInProgress = this.turnInProgress;
|
|
1739
1795
|
const turnKey = typeof turnId === "string" && turnId.length > 0 ? turnId : `unknown:${Date.now()}`;
|
|
1796
|
+
this.activeTurnIds.delete(turnKey);
|
|
1740
1797
|
this.activeTurnIds.add(turnKey);
|
|
1741
1798
|
this.stalledTurnIds.delete(turnKey);
|
|
1742
1799
|
this.currentlyStalledTurnIds.delete(turnKey);
|
|
@@ -1877,18 +1934,23 @@ class CodexAdapter extends EventEmitter {
|
|
|
1877
1934
|
consumeStaleProxyId(proxyId) {
|
|
1878
1935
|
return this.clearTrackedId(this.staleProxyIds, proxyId);
|
|
1879
1936
|
}
|
|
1880
|
-
trackBridgeRequestId(requestId) {
|
|
1937
|
+
trackBridgeRequestId(requestId, kind = "turn-start") {
|
|
1881
1938
|
this.clearTrackedId(this.bridgeRequestIds, requestId);
|
|
1882
1939
|
const timer = setTimeout(() => {
|
|
1883
1940
|
this.bridgeRequestIds.delete(requestId);
|
|
1941
|
+
this.bridgeRequestKinds.delete(requestId);
|
|
1884
1942
|
}, CodexAdapter.RESPONSE_TRACKING_TTL_MS);
|
|
1885
1943
|
timer.unref?.();
|
|
1886
1944
|
this.bridgeRequestIds.set(requestId, timer);
|
|
1945
|
+
this.bridgeRequestKinds.set(requestId, kind);
|
|
1887
1946
|
}
|
|
1888
1947
|
consumeBridgeRequestId(requestId) {
|
|
1889
|
-
|
|
1948
|
+
const kind = this.bridgeRequestKinds.get(requestId) ?? "turn-start";
|
|
1949
|
+
this.bridgeRequestKinds.delete(requestId);
|
|
1950
|
+
return this.clearTrackedId(this.bridgeRequestIds, requestId) ? kind : null;
|
|
1890
1951
|
}
|
|
1891
1952
|
untrackBridgeRequestId(requestId) {
|
|
1953
|
+
this.bridgeRequestKinds.delete(requestId);
|
|
1892
1954
|
this.clearTrackedId(this.bridgeRequestIds, requestId);
|
|
1893
1955
|
}
|
|
1894
1956
|
clearTrackedId(store, id) {
|
|
@@ -1910,6 +1972,7 @@ class CodexAdapter extends EventEmitter {
|
|
|
1910
1972
|
clearTimeout(timer);
|
|
1911
1973
|
}
|
|
1912
1974
|
this.bridgeRequestIds.clear();
|
|
1975
|
+
this.bridgeRequestKinds.clear();
|
|
1913
1976
|
}
|
|
1914
1977
|
clearResponseTrackingState() {
|
|
1915
1978
|
this.clearTransientResponseTrackingState();
|
|
@@ -3947,6 +4010,14 @@ codex.on("turnPhaseChanged", ({ phase, previous }) => {
|
|
|
3947
4010
|
tryWriteStatusFile(`turnPhase:${phase}`);
|
|
3948
4011
|
broadcastStatus();
|
|
3949
4012
|
});
|
|
4013
|
+
codex.on("steerFailed", (reason) => {
|
|
4014
|
+
log(`Steer rejected by app-server: ${reason}`);
|
|
4015
|
+
const advice = codex.turnInProgress ? "wait for it to finish (\u2705), then send normally" : "the turn has ended \u2014 resend as a normal reply";
|
|
4016
|
+
emitToClaude(systemMessage("system_steer_failed", `\u26A0\uFE0F Your steer message did NOT reach Codex (${reason}). The original turn continues unaffected \u2014 ${advice}.`));
|
|
4017
|
+
});
|
|
4018
|
+
codex.on("steerAccepted", () => {
|
|
4019
|
+
log("Steer accepted by app-server");
|
|
4020
|
+
});
|
|
3950
4021
|
codex.on("turnStarted", () => {
|
|
3951
4022
|
log("Codex turn started");
|
|
3952
4023
|
emitToClaude(systemMessage("system_turn_started", "\u23F3 Codex is working on the current task. Wait for completion before sending a reply."));
|
|
@@ -4184,9 +4255,37 @@ function handleControlMessage(ws, raw) {
|
|
|
4184
4255
|
}
|
|
4185
4256
|
log(`Forwarding Claude \u2192 Codex (${message.message.content.length} chars, requireReply=${requireReply})`);
|
|
4186
4257
|
const tierOverrides = BUDGET_CONFIG.codexTierControl ? budgetCoordinator?.getCodexTurnOverrides() ?? undefined : undefined;
|
|
4258
|
+
if (codex.turnInProgress && message.onBusy === "steer") {
|
|
4259
|
+
if (requireReply) {
|
|
4260
|
+
sendProtocolMessage(ws, {
|
|
4261
|
+
type: "claude_to_codex_result",
|
|
4262
|
+
requestId: message.requestId,
|
|
4263
|
+
success: false,
|
|
4264
|
+
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.'
|
|
4265
|
+
});
|
|
4266
|
+
return;
|
|
4267
|
+
}
|
|
4268
|
+
const steerContent = `[STEER from Claude]
|
|
4269
|
+
` + `Mid-turn update for the current Codex turn. Integrate if relevant; do not restart work unless explicitly requested.
|
|
4270
|
+
|
|
4271
|
+
` + message.message.content;
|
|
4272
|
+
const steered = codex.steerMessage(steerContent);
|
|
4273
|
+
log(`Steer ${steered ? "transport-accepted" : "failed"} (${message.message.content.length} chars)`);
|
|
4274
|
+
if (steered) {
|
|
4275
|
+
clearAttentionWindow();
|
|
4276
|
+
}
|
|
4277
|
+
const steerFailureAdvice = codex.turnInProgress ? "Steer failed: the running turn cannot be steered right now \u2014 wait for it to finish (\u2705), then send normally." : "Steer failed: the turn may have just ended or the connection dropped \u2014 retry as a normal reply.";
|
|
4278
|
+
sendProtocolMessage(ws, {
|
|
4279
|
+
type: "claude_to_codex_result",
|
|
4280
|
+
requestId: message.requestId,
|
|
4281
|
+
success: steered,
|
|
4282
|
+
error: steered ? undefined : steerFailureAdvice
|
|
4283
|
+
});
|
|
4284
|
+
return;
|
|
4285
|
+
}
|
|
4187
4286
|
const injected = codex.injectMessage(contentToSend, tierOverrides);
|
|
4188
4287
|
if (!injected) {
|
|
4189
|
-
const reason = codex.turnInProgress ?
|
|
4288
|
+
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
4289
|
log(`Injection rejected: ${reason}`);
|
|
4191
4290
|
sendProtocolMessage(ws, {
|
|
4192
4291
|
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.9", "0.0.0-source"),
|
|
14231
|
+
commit: defineString("10dfd58", "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.9", "0.0.0-source"),
|
|
21
|
+
commit: defineString("10dfd58", "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,45 @@ 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
|
+
const expectedTurnId = this.currentSteerableTurnId();
|
|
815
|
+
if (!expectedTurnId) {
|
|
816
|
+
this.log("Cannot steer: no addressable active turn id (turn/started carried no id)");
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
this.log(`Steering message into active Codex turn ${expectedTurnId} (${text.length} chars)`);
|
|
820
|
+
const requestId = this.nextInjectionId--;
|
|
821
|
+
this.trackBridgeRequestId(requestId, "steer");
|
|
822
|
+
const params = {
|
|
823
|
+
threadId: this.threadId,
|
|
824
|
+
expectedTurnId,
|
|
825
|
+
input: [{ type: "text", text }]
|
|
826
|
+
};
|
|
827
|
+
try {
|
|
828
|
+
this.appServerWs.send(JSON.stringify({
|
|
829
|
+
method: "turn/steer",
|
|
830
|
+
id: requestId,
|
|
831
|
+
params
|
|
832
|
+
}));
|
|
833
|
+
return true;
|
|
834
|
+
} catch (err) {
|
|
835
|
+
this.untrackBridgeRequestId(requestId);
|
|
836
|
+
this.log(`Steer send failed: ${err.message}`);
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
800
840
|
async waitForHealthy(maxRetries = 20, delayMs = 500) {
|
|
801
841
|
for (let i = 0;i < maxRetries; i++) {
|
|
802
842
|
try {
|
|
@@ -1519,14 +1559,22 @@ class CodexAdapter extends EventEmitter {
|
|
|
1519
1559
|
this.interceptServerMessage(parsed, mapping.connId);
|
|
1520
1560
|
return forwarded;
|
|
1521
1561
|
}
|
|
1522
|
-
|
|
1562
|
+
const bridgeKind = !isNaN(numericId) ? this.consumeBridgeRequestId(numericId) : null;
|
|
1563
|
+
if (bridgeKind) {
|
|
1523
1564
|
if (parsed.error) {
|
|
1524
|
-
this.log(`Bridge-originated request failed (id ${responseId}): ${parsed.error.message ?? "unknown error"}`);
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1565
|
+
this.log(`Bridge-originated ${bridgeKind} request failed (id ${responseId}): ${parsed.error.message ?? "unknown error"}`);
|
|
1566
|
+
if (bridgeKind === "steer") {
|
|
1567
|
+
this.emit("steerFailed", parsed.error.message ?? "unknown error");
|
|
1568
|
+
} else {
|
|
1569
|
+
this.lastTurnEndedAbnormally = true;
|
|
1570
|
+
this.emit("turnAborted", `injected turn/start rejected: ${parsed.error.message ?? "unknown error"}`);
|
|
1571
|
+
this.notifyPhaseIfChanged();
|
|
1572
|
+
}
|
|
1528
1573
|
} else {
|
|
1529
|
-
this.log(`Bridge-originated request completed (id ${responseId})`);
|
|
1574
|
+
this.log(`Bridge-originated ${bridgeKind} request completed (id ${responseId})`);
|
|
1575
|
+
if (bridgeKind === "steer") {
|
|
1576
|
+
this.emit("steerAccepted");
|
|
1577
|
+
}
|
|
1530
1578
|
}
|
|
1531
1579
|
return null;
|
|
1532
1580
|
}
|
|
@@ -1719,6 +1767,14 @@ class CodexAdapter extends EventEmitter {
|
|
|
1719
1767
|
this.log(`Thread detected: ${threadId} (${reason})`);
|
|
1720
1768
|
this.emit("ready", threadId);
|
|
1721
1769
|
}
|
|
1770
|
+
currentSteerableTurnId() {
|
|
1771
|
+
let newest = null;
|
|
1772
|
+
for (const id of this.activeTurnIds) {
|
|
1773
|
+
if (!id.startsWith("unknown:"))
|
|
1774
|
+
newest = id;
|
|
1775
|
+
}
|
|
1776
|
+
return newest;
|
|
1777
|
+
}
|
|
1722
1778
|
get turnPhase() {
|
|
1723
1779
|
if (this.activeTurnIds.size > 0) {
|
|
1724
1780
|
const allStalled = [...this.activeTurnIds].every((id) => this.currentlyStalledTurnIds.has(id));
|
|
@@ -1737,6 +1793,7 @@ class CodexAdapter extends EventEmitter {
|
|
|
1737
1793
|
markTurnStarted(turnId) {
|
|
1738
1794
|
const wasInProgress = this.turnInProgress;
|
|
1739
1795
|
const turnKey = typeof turnId === "string" && turnId.length > 0 ? turnId : `unknown:${Date.now()}`;
|
|
1796
|
+
this.activeTurnIds.delete(turnKey);
|
|
1740
1797
|
this.activeTurnIds.add(turnKey);
|
|
1741
1798
|
this.stalledTurnIds.delete(turnKey);
|
|
1742
1799
|
this.currentlyStalledTurnIds.delete(turnKey);
|
|
@@ -1877,18 +1934,23 @@ class CodexAdapter extends EventEmitter {
|
|
|
1877
1934
|
consumeStaleProxyId(proxyId) {
|
|
1878
1935
|
return this.clearTrackedId(this.staleProxyIds, proxyId);
|
|
1879
1936
|
}
|
|
1880
|
-
trackBridgeRequestId(requestId) {
|
|
1937
|
+
trackBridgeRequestId(requestId, kind = "turn-start") {
|
|
1881
1938
|
this.clearTrackedId(this.bridgeRequestIds, requestId);
|
|
1882
1939
|
const timer = setTimeout(() => {
|
|
1883
1940
|
this.bridgeRequestIds.delete(requestId);
|
|
1941
|
+
this.bridgeRequestKinds.delete(requestId);
|
|
1884
1942
|
}, CodexAdapter.RESPONSE_TRACKING_TTL_MS);
|
|
1885
1943
|
timer.unref?.();
|
|
1886
1944
|
this.bridgeRequestIds.set(requestId, timer);
|
|
1945
|
+
this.bridgeRequestKinds.set(requestId, kind);
|
|
1887
1946
|
}
|
|
1888
1947
|
consumeBridgeRequestId(requestId) {
|
|
1889
|
-
|
|
1948
|
+
const kind = this.bridgeRequestKinds.get(requestId) ?? "turn-start";
|
|
1949
|
+
this.bridgeRequestKinds.delete(requestId);
|
|
1950
|
+
return this.clearTrackedId(this.bridgeRequestIds, requestId) ? kind : null;
|
|
1890
1951
|
}
|
|
1891
1952
|
untrackBridgeRequestId(requestId) {
|
|
1953
|
+
this.bridgeRequestKinds.delete(requestId);
|
|
1892
1954
|
this.clearTrackedId(this.bridgeRequestIds, requestId);
|
|
1893
1955
|
}
|
|
1894
1956
|
clearTrackedId(store, id) {
|
|
@@ -1910,6 +1972,7 @@ class CodexAdapter extends EventEmitter {
|
|
|
1910
1972
|
clearTimeout(timer);
|
|
1911
1973
|
}
|
|
1912
1974
|
this.bridgeRequestIds.clear();
|
|
1975
|
+
this.bridgeRequestKinds.clear();
|
|
1913
1976
|
}
|
|
1914
1977
|
clearResponseTrackingState() {
|
|
1915
1978
|
this.clearTransientResponseTrackingState();
|
|
@@ -3947,6 +4010,14 @@ codex.on("turnPhaseChanged", ({ phase, previous }) => {
|
|
|
3947
4010
|
tryWriteStatusFile(`turnPhase:${phase}`);
|
|
3948
4011
|
broadcastStatus();
|
|
3949
4012
|
});
|
|
4013
|
+
codex.on("steerFailed", (reason) => {
|
|
4014
|
+
log(`Steer rejected by app-server: ${reason}`);
|
|
4015
|
+
const advice = codex.turnInProgress ? "wait for it to finish (\u2705), then send normally" : "the turn has ended \u2014 resend as a normal reply";
|
|
4016
|
+
emitToClaude(systemMessage("system_steer_failed", `\u26A0\uFE0F Your steer message did NOT reach Codex (${reason}). The original turn continues unaffected \u2014 ${advice}.`));
|
|
4017
|
+
});
|
|
4018
|
+
codex.on("steerAccepted", () => {
|
|
4019
|
+
log("Steer accepted by app-server");
|
|
4020
|
+
});
|
|
3950
4021
|
codex.on("turnStarted", () => {
|
|
3951
4022
|
log("Codex turn started");
|
|
3952
4023
|
emitToClaude(systemMessage("system_turn_started", "\u23F3 Codex is working on the current task. Wait for completion before sending a reply."));
|
|
@@ -4184,9 +4255,37 @@ function handleControlMessage(ws, raw) {
|
|
|
4184
4255
|
}
|
|
4185
4256
|
log(`Forwarding Claude \u2192 Codex (${message.message.content.length} chars, requireReply=${requireReply})`);
|
|
4186
4257
|
const tierOverrides = BUDGET_CONFIG.codexTierControl ? budgetCoordinator?.getCodexTurnOverrides() ?? undefined : undefined;
|
|
4258
|
+
if (codex.turnInProgress && message.onBusy === "steer") {
|
|
4259
|
+
if (requireReply) {
|
|
4260
|
+
sendProtocolMessage(ws, {
|
|
4261
|
+
type: "claude_to_codex_result",
|
|
4262
|
+
requestId: message.requestId,
|
|
4263
|
+
success: false,
|
|
4264
|
+
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.'
|
|
4265
|
+
});
|
|
4266
|
+
return;
|
|
4267
|
+
}
|
|
4268
|
+
const steerContent = `[STEER from Claude]
|
|
4269
|
+
` + `Mid-turn update for the current Codex turn. Integrate if relevant; do not restart work unless explicitly requested.
|
|
4270
|
+
|
|
4271
|
+
` + message.message.content;
|
|
4272
|
+
const steered = codex.steerMessage(steerContent);
|
|
4273
|
+
log(`Steer ${steered ? "transport-accepted" : "failed"} (${message.message.content.length} chars)`);
|
|
4274
|
+
if (steered) {
|
|
4275
|
+
clearAttentionWindow();
|
|
4276
|
+
}
|
|
4277
|
+
const steerFailureAdvice = codex.turnInProgress ? "Steer failed: the running turn cannot be steered right now \u2014 wait for it to finish (\u2705), then send normally." : "Steer failed: the turn may have just ended or the connection dropped \u2014 retry as a normal reply.";
|
|
4278
|
+
sendProtocolMessage(ws, {
|
|
4279
|
+
type: "claude_to_codex_result",
|
|
4280
|
+
requestId: message.requestId,
|
|
4281
|
+
success: steered,
|
|
4282
|
+
error: steered ? undefined : steerFailureAdvice
|
|
4283
|
+
});
|
|
4284
|
+
return;
|
|
4285
|
+
}
|
|
4187
4286
|
const injected = codex.injectMessage(contentToSend, tierOverrides);
|
|
4188
4287
|
if (!injected) {
|
|
4189
|
-
const reason = codex.turnInProgress ?
|
|
4288
|
+
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
4289
|
log(`Injection rejected: ${reason}`);
|
|
4191
4290
|
sendProtocolMessage(ws, {
|
|
4192
4291
|
type: "claude_to_codex_result",
|