@minhspark/codex-mcp-bridge 1.13.7 → 1.13.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/CHANGELOG.md +11 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/scripts/install-native-relay.mjs +1 -1
- package/src/claude-bridge.mjs +1 -1
- package/src/index.mjs +1 -1
- package/src/native-relay-companion.mjs +1 -1
- package/src/peer-protocol.mjs +25 -11
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](ht
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.13.8] - 2026-09-07
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Reject contradictory message IDs, non-peer origins, duplicate matching roots, and reply branches that cross another queued command. Preserve both measured Desktop transcript formats while preventing another command's answer from completing the wrong pending request.
|
|
12
|
+
- Ignore malformed transcript records and text blocks without crashing the reply watcher. Empty or malformed user input no longer counts as a tool-result continuation.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Document that an absorbed message is identified by `source_uuid` on Claude Code 2.1.260 and later and by `origin.msg_id` on earlier builds, which 1.13.7 already matches.
|
|
17
|
+
|
|
7
18
|
## [1.13.7] - 2026-09-07
|
|
8
19
|
|
|
9
20
|
### Fixed
|
package/README.md
CHANGED
|
@@ -402,7 +402,7 @@ Both bridge status tools report the loaded source fingerprint and process identi
|
|
|
402
402
|
|
|
403
403
|
Claude's [inbound permission controls](https://code.claude.com/docs/en/cross-session-messaging#control-inbound-messages) remain authoritative, and their default rule is symmetric: when no `crossSessionInbound` value applies, a message is delivered only when the sender's attested class matches the recipient's. A recipient that bypasses permission prompts holds every sender that does not attest `bypass`; a recipient that prompts (default, `acceptEdits`, `auto`, `dontAsk`) holds every sender that attests `bypass`. The bridge attests the sender's true class from the verified Codex turn and never adjusts it to suit a recipient. Claude Desktop does not declare the `peer_inbound_approval` dialog kind to Claude Code, so a message held by a Desktop-hosted session has no approval button there; Claude Code keeps it until `dialogExpiry` (five minutes by default) and then reports it as expired. A `held` receipt in Desktop mode therefore means the two tasks run in different permission classes, not that the sender was misread. Report that to the user: only they can run the recipient task in the matching class or set `crossSessionInbound` to `accept` in that session's own settings. Because that outcome is predictable, `send_to_claude_session` first reads the recipient session's effective `crossSessionInbound` from the same files Claude Desktop loads (managed settings, then the user's `~/.claude/settings.json`, with the recipient project's `.claude/settings.json` and `settings.local.json` able only to tighten it) and refuses at preflight with `CLAUDE_RECIPIENT_INBOUND_POLICY` when that value is `refuse` or `hold`, since nothing could reach Claude or be approved. An explicit `accept` sends without a class check, because Claude delivers regardless of class. Otherwise the parity default applies: the recipient task's permission mode from Claude Desktop metadata is compared with the verified sender class and a mismatch is refused with `CLAUDE_RECIPIENT_CLASS_MISMATCH`, naming both classes and the user-only remedies on either side; both checks repeat immediately before writing. A task whose mode is unknown is sent as before and Claude decides. Flags passed at launch such as `--settings` are invisible to the bridge, so put the value in a settings file. `list_claude_sessions` shows an explicit inbound value with its source, and receipts carry `senderApprovalPolicy`, `recipientPermissionMode`, `recipientPermissionClass`, and `recipientInboundPolicy`; a held receipt repeats both classes. Do not change `CLAUDE_BRIDGE_PERMISSION_MODE`, fabricate a sender class, or edit recipient settings on the user's behalf. Legacy CLI configuration must truthfully represent every session using that MCP entry; Desktop sends derive their class from the verified calling turn. If the Desktop UI does not expose the pending approval, keep the original receipt rather than creating a CLI session.
|
|
404
404
|
|
|
405
|
-
Claude Desktop disables the CLI-native `SendMessage` tool. For Desktop targets, the bridge requests an ordinary answer in the destination conversation and reads only a completed assistant turn descended from the transcript entry that records the injected message. Unrelated human prompts and sidechains are excluded. Late answers remain available through the inbox while the bridge process is running. This does not change Desktop tool permissions or the receiver's inbound policy. Claude Code records the injected message in one of two shapes: an idle recipient starts a new turn with a user entry whose uuid is the message id, while a busy recipient absorbs the message into its running turn as a `queued_command` attachment whose `source_uuid` is the message id, and that turn's closing text is the reply. Both shapes are recognised; before 1.13.7 only the idle one was, so a reply to an absorbed message was visible in the app yet reported as `reply_timeout` with the message still pending. A reply taken from an absorbed turn is that turn's closing text, which may not address the message, so the receipt marks it `replyAbsorbed: true`, the tool result says so, and the text forwarded to Codex carries the same note.
|
|
405
|
+
Claude Desktop disables the CLI-native `SendMessage` tool. For Desktop targets, the bridge requests an ordinary answer in the destination conversation and reads only a completed assistant turn descended from the transcript entry that records the injected message. Unrelated human prompts and sidechains are excluded. Late answers remain available through the inbox while the bridge process is running. This does not change Desktop tool permissions or the receiver's inbound policy. Claude Code records the injected message in one of two shapes: an idle recipient starts a new turn with a user entry whose uuid is the message id, while a busy recipient absorbs the message into its running turn as a `queued_command` attachment whose `source_uuid` (Claude Code 2.1.260 and later) or `origin.msg_id` (earlier builds such as 2.1.229 and 2.1.237, where `source_uuid` is unrelated) is the message id, and that turn's closing text is the reply. Both shapes are recognised; before 1.13.7 only the idle one was, so a reply to an absorbed message was visible in the app yet reported as `reply_timeout` with the message still pending. A reply taken from an absorbed turn is that turn's closing text, which may not address the message, so the receipt marks it `replyAbsorbed: true`, the tool result says so, and the text forwarded to Codex carries the same note.
|
|
406
406
|
|
|
407
407
|
In Desktop-only mode, the sender is resolved separately for every MCP call. Codex supplies `x-codex-turn-metadata` with the calling task and turn IDs; the bridge matches them to one active, local Codex Desktop rollout and reads its effective permission profile and approval settings. The diagnostic `sandbox_mode` label is never used to authorize a send. Missing or invalid host review flags, a completed or superseded turn, or an unsupported permission profile blocks sending before any message bytes are written. Status distinguishes enabled, disabled, missing, and invalid review flags. Do not supply fabricated MCP metadata or use another process to make a blocked send succeed.
|
|
408
408
|
|
package/package.json
CHANGED
|
@@ -20,7 +20,7 @@ import { exitForVersionRequest } from "../src/cli-version.mjs";
|
|
|
20
20
|
|
|
21
21
|
exitForVersionRequest(import.meta.url);
|
|
22
22
|
|
|
23
|
-
const VERSION = "1.13.
|
|
23
|
+
const VERSION = "1.13.8";
|
|
24
24
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
25
25
|
const entry = path.join(root, "src", "native-relay-companion.mjs");
|
|
26
26
|
const serverName = process.env.CODEX_NATIVE_RELAY_NAME ?? "codex-native-relay";
|
package/src/claude-bridge.mjs
CHANGED
package/src/index.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import { exitForVersionRequest } from "./cli-version.mjs";
|
|
|
23
23
|
|
|
24
24
|
exitForVersionRequest(import.meta.url);
|
|
25
25
|
|
|
26
|
-
const VERSION = "1.13.
|
|
26
|
+
const VERSION = "1.13.8";
|
|
27
27
|
const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
|
|
28
28
|
|
|
29
29
|
function errorResponse(code, message) {
|
package/src/peer-protocol.mjs
CHANGED
|
@@ -293,10 +293,15 @@ export function readTranscript(sessionId, cwd, limit = 10) {
|
|
|
293
293
|
* turn's closing text. Both were measured on Claude Code 2.1.260-2.1.263.
|
|
294
294
|
*/
|
|
295
295
|
function injectedMessageRoot(entry, msgId) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
296
|
+
if (typeof entry.uuid !== "string" || !entry.uuid.trim()) return null;
|
|
297
|
+
const matchesOrigin = (origin) => origin?.kind === "peer" && origin.msg_id === msgId;
|
|
298
|
+
if (entry.message?.role === "user") {
|
|
299
|
+
if (entry.origin !== undefined && !matchesOrigin(entry.origin)) return null;
|
|
300
|
+
if (entry.uuid === msgId || matchesOrigin(entry.origin)) return "idle";
|
|
301
|
+
}
|
|
302
|
+
const attachment = entry.type === "attachment" ? entry.attachment : null;
|
|
303
|
+
if (attachment?.type === "queued_command" && matchesOrigin(attachment.origin)
|
|
304
|
+
&& (attachment.source_uuid === undefined || attachment.source_uuid === msgId)) return "absorbed";
|
|
300
305
|
return null;
|
|
301
306
|
}
|
|
302
307
|
|
|
@@ -305,21 +310,30 @@ export function readTranscriptReply(sessionId, cwd, msgId) {
|
|
|
305
310
|
let lines;
|
|
306
311
|
try { lines = fs.readFileSync(file, "utf8").split("\n"); }
|
|
307
312
|
catch { return null; }
|
|
308
|
-
const
|
|
309
|
-
|
|
313
|
+
const entries = [];
|
|
314
|
+
const roots = [];
|
|
310
315
|
for (const line of lines) {
|
|
311
316
|
let entry;
|
|
312
317
|
try { entry = JSON.parse(line); } catch { continue; }
|
|
313
|
-
if (entry.isSidechain) continue;
|
|
318
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry) || entry.isSidechain) continue;
|
|
319
|
+
entries.push(entry);
|
|
314
320
|
const root = injectedMessageRoot(entry, msgId);
|
|
315
|
-
if (root) { absorbed
|
|
321
|
+
if (root) roots.push({ entry, absorbed: root === "absorbed" });
|
|
322
|
+
}
|
|
323
|
+
if (roots.length !== 1) return null;
|
|
324
|
+
const { entry: root, absorbed } = roots[0];
|
|
325
|
+
const descendants = new Set();
|
|
326
|
+
for (const entry of entries) {
|
|
327
|
+
if (entry === root) { descendants.add(entry.uuid); continue; }
|
|
316
328
|
if (!descendants.has(entry.parentUuid)) continue;
|
|
329
|
+
if (entry.type === "attachment" && entry.attachment?.type === "queued_command") continue;
|
|
317
330
|
const role = entry.message?.role;
|
|
318
331
|
const content = entry.message?.content;
|
|
319
|
-
if (role === "user" && !(Array.isArray(content) && content.every((part) => part
|
|
320
|
-
if (typeof entry.uuid
|
|
332
|
+
if (role === "user" && !(Array.isArray(content) && content.length > 0 && content.every((part) => part?.type === "tool_result"))) continue;
|
|
333
|
+
if (typeof entry.uuid !== "string" || !entry.uuid.trim()) continue;
|
|
334
|
+
descendants.add(entry.uuid);
|
|
321
335
|
if (role !== "assistant" || !["end_turn", "stop_sequence"].includes(entry.message.stop_reason)) continue;
|
|
322
|
-
const text = Array.isArray(content) ? content.filter((part) => part
|
|
336
|
+
const text = Array.isArray(content) ? content.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n") : typeof content === "string" ? content : "";
|
|
323
337
|
if (text.trim()) return { text: text.trim(), msgId: entry.uuid, source: "transcript", inReplyTo: msgId, absorbed };
|
|
324
338
|
}
|
|
325
339
|
return null;
|