@minhspark/codex-mcp-bridge 1.18.0 → 1.18.2

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 CHANGED
@@ -4,6 +4,20 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](ht
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.18.2] - 2026-09-26
8
+
9
+ ### Fixed
10
+
11
+ - Restore native relay compatibility with Codex Desktop 26.924 by including the required caller source in native tool requests, allowing pipe discovery and Desktop operations to recover automatically.
12
+
13
+ ## [1.18.1] - 2026-09-25
14
+
15
+ ### Fixed
16
+
17
+ - Report npm installation completion with an unchanged version instead of claiming the package is already up to date after a same-version reinstall.
18
+ - Discover Claude Desktop sessions when accumulated task configuration exceeds 32 MiB. Retain only bounded identity fields and SHA-256 fingerprints while preserving account isolation, duplicate detection, and concurrent-change checks.
19
+ - Report Desktop task verification failures separately from missing peer endpoints in session discovery and bridge status.
20
+
7
21
  ## [1.18.0] - 2026-09-23
8
22
 
9
23
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhspark/codex-mcp-bridge",
3
- "version": "1.18.0",
3
+ "version": "1.18.2",
4
4
  "description": "Two-way MCP bridge between Claude and Codex: prompts into a live Codex thread, messages into a running Claude Code session.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -22,7 +22,7 @@ import { createReleaseSnapshot, snapshotRoot } from "../src/release-snapshot.mjs
22
22
 
23
23
  exitForVersionRequest(import.meta.url);
24
24
 
25
- const VERSION = "1.18.0";
25
+ const VERSION = "1.18.2";
26
26
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
27
27
  const entry = path.join(root, "src", "mcp-supervisor.mjs");
28
28
  const entryArgs = ["native-relay-companion.mjs"];
@@ -130,7 +130,7 @@ try {
130
130
  } elseif ($before.Status -eq 'missing') {
131
131
  Write-Host "Successfully installed: @minhspark/codex-mcp-bridge v$($after.Version)"
132
132
  } elseif ($before.Version -ceq $after.Version) {
133
- Write-Host "Already up to date: @minhspark/codex-mcp-bridge v$($after.Version)"
133
+ Write-Host "Installation completed: @minhspark/codex-mcp-bridge v$($after.Version) (version unchanged)"
134
134
  } else {
135
135
  Write-Host "Successfully updated: @minhspark/codex-mcp-bridge v$($before.Version) -> v$($after.Version)"
136
136
  }
@@ -70,7 +70,7 @@ npm() {
70
70
  if [ -z "$bridge_after" ] || [ "$bridge_before_verified" -ne 1 ] || [ "$bridge_after_root" != "$bridge_root" ]; then
71
71
  printf 'Warning: npm completed, but the installed @minhspark/codex-mcp-bridge version could not be verified.\n' >&2
72
72
  elif [ "$bridge_after_root" = "$bridge_root" ] && [ "$bridge_after" = "$bridge_before" ]; then
73
- printf 'Already up to date: @minhspark/codex-mcp-bridge v%s\n' "$bridge_after"
73
+ printf 'Installation completed: @minhspark/codex-mcp-bridge v%s (version unchanged)\n' "$bridge_after"
74
74
  elif [ "$bridge_after_root" = "$bridge_root" ] && [ -n "$bridge_before" ]; then
75
75
  printf 'Successfully updated: @minhspark/codex-mcp-bridge v%s -> v%s\n' "$bridge_before" "$bridge_after"
76
76
  else
@@ -26,7 +26,7 @@ import { AGENT_PROMPT_GUIDANCE, PROMPT_FIELD_HINT } from "./prompt-guidance.mjs"
26
26
 
27
27
  exitForVersionRequest(import.meta.url);
28
28
 
29
- const VERSION = "1.18.0";
29
+ const VERSION = "1.18.2";
30
30
  void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
31
31
  const FORWARD_MIN_INTERVAL_MS = 5000;
32
32
  const FORWARD_MAX_PER_SESSION = 50;
@@ -164,6 +164,40 @@ function withDesktopContext(session, account = readClaudeAccountContext()) {
164
164
  ? { ...session, desktop: readClaudeDesktopContext(session, { account }), inbound: readClaudeInboundPolicy(session.cwd) } : session;
165
165
  }
166
166
 
167
+ function inspectDiscovery(sessions, account, expectedCwd) {
168
+ const eligible = [];
169
+ const excluded = [];
170
+ let nonDesktopCount = 0;
171
+ let candidateCount = 0;
172
+ for (const session of sessions) {
173
+ if (rootPolicy.enabled && !rootPolicy.allows(session.cwd)) continue;
174
+ if (expectedCwd !== undefined) {
175
+ try { assertClaudeSessionCwd(session, expectedCwd); }
176
+ catch { continue; }
177
+ }
178
+ if (desktopOnly && session.entrypoint !== "claude-desktop") {
179
+ nonDesktopCount += 1;
180
+ continue;
181
+ }
182
+ candidateCount += 1;
183
+ const inspected = withDesktopContext(session, account);
184
+ if (!desktopOnly || inspected.desktop?.status === "matched") {
185
+ eligible.push(inspected);
186
+ continue;
187
+ }
188
+ const status = inspected.desktop?.status ?? "missing";
189
+ const reason = inspected.desktop?.reason ?? "The Desktop task identity could not be verified.";
190
+ const previous = excluded.find((entry) => entry.status === status && entry.reason === reason);
191
+ if (previous) previous.count += 1;
192
+ else excluded.push({ status, reason, count: 1 });
193
+ }
194
+ return { sessions: eligible, discovery: { candidateCount, matchedCount: eligible.length, nonDesktopCount, excluded } };
195
+ }
196
+
197
+ function discoveryDetails(discovery) {
198
+ return discovery.excluded.map((entry) => `Desktop task verification: ${entry.count} candidate(s) ${entry.status} - ${entry.reason}`);
199
+ }
200
+
167
201
  function assertSessionRoot(session, label = "Claude session") {
168
202
  return rootPolicy.assert(session?.cwd, `${label} working directory`);
169
203
  }
@@ -340,17 +374,14 @@ registerTool(
340
374
  if (expectedCwd !== undefined) assertClaudeSessionCwd({ cwd: expectedCwd }, expectedCwd);
341
375
  const account = readClaudeAccountContext();
342
376
  if (desktopOnly && account.status !== "verified") return { ...textResult(`Claude account ${account.status}: ${account.reason} Sign in and retry discovery.`, true), structuredContent: { account: publicAccountState(account), sessions: [] } };
343
- const sessions = listClaudeSessions({ includeDead: includeDead ?? false }).filter(
344
- (s) => s.pid !== process.pid && (!desktopOnly || s.entrypoint === "claude-desktop"),
345
- ).filter((session) => !rootPolicy.enabled || rootPolicy.allows(session.cwd)
346
- ).filter((session) => {
347
- if (expectedCwd === undefined) return true;
348
- try { assertClaudeSessionCwd(session, expectedCwd); return true; }
349
- catch { return false; }
350
- }).map((session) => withDesktopContext(session, account)).filter((session) => !desktopOnly || session.desktop?.status === "matched");
351
- if (!sessions.length) return textResult(desktopOnly ? missingDesktopSession : "No live Claude Code session found.");
352
- return { ...textResult(`${sessions.length} Claude${desktopOnly ? " Desktop" : ""} session(s):\n\n${sessions.map(formatSessionRow).join("\n")}`),
353
- structuredContent: { account: publicAccountState(account), sessions: sessions.map(({ socket, bridgeSessionId, ...session }) => session) } };
377
+ const { sessions, discovery } = inspectDiscovery(listClaudeSessions({ includeDead: includeDead ?? false }).filter((s) => s.pid !== process.pid), account, expectedCwd);
378
+ const summary = sessions.length
379
+ ? `${sessions.length} Claude${desktopOnly ? " Desktop" : ""} session(s):\n\n${sessions.map(formatSessionRow).join("\n")}`
380
+ : discovery.excluded.length
381
+ ? `Found ${discovery.candidateCount} registered Claude Desktop candidate(s) with messaging endpoints, but their Desktop task identities could not be verified. Delivery remains blocked.`
382
+ : desktopOnly ? missingDesktopSession : "No live Claude Code session found.";
383
+ return { ...textResult([summary, ...discoveryDetails(discovery)].join("\n")),
384
+ structuredContent: { account: publicAccountState(account), sessions: sessions.map(({ socket, bridgeSessionId, ...session }) => session), discovery } };
354
385
  } catch (err) {
355
386
  return failure(err);
356
387
  }
@@ -646,9 +677,7 @@ registerTool(
646
677
  await peer.start();
647
678
  const sessions = listClaudeSessions().filter((s) => s.pid !== process.pid);
648
679
  const accounts = readBridgeAccounts();
649
- const eligible = sessions.filter((session) => !desktopOnly || session.entrypoint === "claude-desktop")
650
- .filter((session) => !rootPolicy.enabled || rootPolicy.allows(session.cwd))
651
- .filter((session) => !desktopOnly || withDesktopContext(session, accounts.claude).desktop?.status === "matched");
680
+ const { sessions: eligible, discovery } = inspectDiscovery(sessions, accounts.claude);
652
681
  const visibleInbox = peer.inbox.filter((record) => recordVisible(record, accounts));
653
682
  const visiblePending = [...peer.pendingMessages.keys()].filter((id) => readReceipt(id));
654
683
  const visibleForwarding = replyForwarder.status((record) => recordVisible(record, accounts));
@@ -666,7 +695,8 @@ registerTool(
666
695
  `Claude account: ${accounts.claude.status}${accounts.claude.fingerprint ? ` (${accounts.claude.fingerprint.slice(0, 12)})` : ` - ${accounts.claude.reason}`}`,
667
696
  `Codex account: ${accounts.codex.status}${accounts.codex.fingerprint ? ` (${accounts.codex.fingerprint.slice(0, 12)})` : ` - ${accounts.codex.reason}`}`,
668
697
  `live sessions: ${eligible.length}`,
669
- ...(rootPolicy.enabled ? [] : [`excluded: ${sessions.length - eligible.length} non-Desktop or inactive-account session(s)`]),
698
+ `excluded: ${discovery.nonDesktopCount} non-Desktop session(s); ${discovery.candidateCount - eligible.length} Desktop candidate(s) failed task verification`,
699
+ ...discoveryDetails(discovery),
670
700
  `relay thread: ${forwarding.threadId ?? "(none - use bind_codex_thread)"}`,
671
701
  `delivery: ${delivery.describe()}`,
672
702
  `inbox: ${visibleInbox.length} pending message(s)`,
@@ -676,7 +706,7 @@ registerTool(
676
706
  ];
677
707
  const state = runtime.status();
678
708
  lines.push(`runtime pid: ${state.pid}`, `loaded source: ${state.revision}`, `disk source: ${state.diskRevision ?? "unreadable"}`, `runtime state: ${state.current ? "current" : `STALE - ${state.reason}; reconnect this MCP server in the existing task`}`);
679
- return { ...textResult(lines.join("\n"), !state.current), structuredContent: { runtime: state, accounts: { claude: publicAccountState(accounts.claude), codex: publicAccountState(accounts.codex) }, replyForwarding: visibleForwarding, ...(sender ? { sender } : {}) } };
709
+ return { ...textResult(lines.join("\n"), !state.current), structuredContent: { runtime: state, accounts: { claude: publicAccountState(accounts.claude), codex: publicAccountState(accounts.codex) }, discovery, replyForwarding: visibleForwarding, ...(sender ? { sender } : {}) } };
680
710
  } catch (err) {
681
711
  return failure(err);
682
712
  }
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import fs from "node:fs";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
@@ -7,7 +8,7 @@ const ACCOUNT_ID = new RegExp(`^${UUID}$`, "i");
7
8
  const TASK_FILE = new RegExp(`^(local_${UUID})\\.json$`, "i");
8
9
  const MAX_ENTRIES = 8192;
9
10
  const MAX_FILE_BYTES = 4 * 1024 * 1024;
10
- const MAX_TOTAL_BYTES = 32 * 1024 * 1024;
11
+ const MAX_IDENTITY_BYTES = 32 * 1024 * 1024;
11
12
  const VERSION_FIELDS = ["size", "mtimeMs", "ctimeMs", "ino", "dev"];
12
13
 
13
14
  const sameVersion = (left, right) => VERSION_FIELDS.every((field) => left[field] === right[field]);
@@ -69,18 +70,16 @@ function readMetadata(file, budget) {
69
70
  try {
70
71
  const before = fs.fstatSync(descriptor);
71
72
  if (!before.isFile() || before.size > MAX_FILE_BYTES) throw new Error("file limit");
72
- budget.bytes += before.size;
73
- if (budget.bytes > MAX_TOTAL_BYTES) throw new Error("total limit");
74
73
  const bytes = Buffer.alloc(before.size);
75
74
  if (fs.readSync(descriptor, bytes, 0, bytes.length, 0) !== bytes.length) throw new Error("short read");
76
75
  const after = fs.fstatSync(descriptor);
77
76
  const current = fs.lstatSync(file);
78
77
  if (!sameVersion(before, after) || current.isSymbolicLink() || !sameVersion(current, after)) throw new Error("metadata changed");
79
78
  budget.versions.set(file, current);
80
- budget.contents.set(file, bytes);
79
+ budget.digests.set(file, createHash("sha256").update(bytes).digest("hex"));
81
80
  const data = JSON.parse(bytes.toString("utf8"));
82
81
  if (!data || typeof data !== "object" || Array.isArray(data)) throw new Error("invalid metadata");
83
- return {
82
+ const record = {
84
83
  taskId: data.sessionId,
85
84
  fileTaskId: TASK_FILE.exec(path.basename(file))[1],
86
85
  cliSessionId: data.cliSessionId,
@@ -90,18 +89,14 @@ function readMetadata(file, budget) {
90
89
  isArchived: data.isArchived,
91
90
  permissionMode: data.permissionMode,
92
91
  };
92
+ budget.bytes += Buffer.byteLength(JSON.stringify(record));
93
+ if (budget.bytes > MAX_IDENTITY_BYTES) throw new Error("identity limit");
94
+ return record;
93
95
  } finally {
94
96
  fs.closeSync(descriptor);
95
97
  }
96
98
  }
97
99
 
98
- /**
99
- * Stat fields cannot prove a record is unchanged: a same-size rewrite inside
100
- * one filesystem timestamp tick (about 16 ms on Windows) keeps size, times
101
- * and inode identical. The final consistency pass therefore compares the
102
- * bytes themselves; the records are small JSON files, so the extra read is
103
- * cheap and the check is deterministic on every platform.
104
- */
105
100
  function rereadMetadata(file) {
106
101
  const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
107
102
  try {
@@ -109,7 +104,7 @@ function rereadMetadata(file) {
109
104
  if (!stat.isFile() || stat.size > MAX_FILE_BYTES) throw new Error("file limit");
110
105
  const bytes = Buffer.alloc(stat.size);
111
106
  if (fs.readSync(descriptor, bytes, 0, bytes.length, 0) !== bytes.length) throw new Error("short read");
112
- return bytes;
107
+ return createHash("sha256").update(bytes).digest("hex");
113
108
  } finally {
114
109
  fs.closeSync(descriptor);
115
110
  }
@@ -138,8 +133,14 @@ export function readClaudeDesktopContext(session, { platform = process.platform,
138
133
  const directory = root ?? (account ? path.join(account.root, "claude-code-sessions") : sessionsRoot(platform, env));
139
134
  if (!fs.existsSync(directory)) return result("missing", "Claude Desktop task metadata is not available on this host.");
140
135
  records = readClaudeDesktopTasks({ platform, env, root, account });
141
- } catch {
142
- return result("mismatch", "Claude Desktop task metadata could not be read completely and consistently; no task identity is confirmed.");
136
+ } catch (error) {
137
+ const detail = {
138
+ "file limit": " A task metadata file exceeds the 4 MiB per-file limit.",
139
+ "identity limit": " Task identity fields exceed the 32 MiB retained-data limit.",
140
+ "scan limit": " The task directory exceeds the 8192-entry scan limit.",
141
+ "metadata changed": " Task metadata changed during inspection; retry discovery.",
142
+ }[error.message] ?? "";
143
+ return result("mismatch", `Claude Desktop task metadata could not be read completely and consistently; no task identity is confirmed.${detail}`);
143
144
  }
144
145
 
145
146
  const primary = records.filter((record) => record.cliSessionId === session.sessionId);
@@ -198,13 +199,13 @@ export function readClaudeDesktopTasks({ platform = process.platform, env = proc
198
199
  }
199
200
  const directory = root ?? (account ? path.join(account.root, "claude-code-sessions") : sessionsRoot(platform, env));
200
201
  const files = metadataFiles(directory, account?.accountId);
201
- const budget = { bytes: 0, versions: new Map(), contents: new Map() };
202
+ const budget = { bytes: 0, versions: new Map(), digests: new Map() };
202
203
  const records = files.map((file) => readMetadata(file, budget));
203
204
  const currentFiles = metadataFiles(directory, account?.accountId);
204
205
  if (files.length !== currentFiles.length || files.some((file, index) => file !== currentFiles[index])) throw new Error("metadata changed");
205
206
  for (const file of files) {
206
207
  const current = fs.lstatSync(file);
207
- if (!current.isFile() || !sameVersion(current, budget.versions.get(file)) || !rereadMetadata(file).equals(budget.contents.get(file))) throw new Error("metadata changed");
208
+ if (!current.isFile() || !sameVersion(current, budget.versions.get(file)) || rereadMetadata(file) !== budget.digests.get(file)) throw new Error("metadata changed");
208
209
  }
209
210
  return records;
210
211
  }
package/src/index.mjs CHANGED
@@ -34,7 +34,7 @@ import { AGENT_PROMPT_GUIDANCE, PROMPT_FIELD_HINT } from "./prompt-guidance.mjs"
34
34
 
35
35
  exitForVersionRequest(import.meta.url);
36
36
 
37
- const VERSION = "1.18.0";
37
+ const VERSION = "1.18.2";
38
38
  void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
39
39
  const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
40
40
 
@@ -34,7 +34,7 @@ import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
34
34
 
35
35
  exitForVersionRequest(import.meta.url);
36
36
 
37
- const VERSION = "1.18.0";
37
+ const VERSION = "1.18.2";
38
38
  const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
39
39
 
40
40
  function errorResponse(code, message, sent) {
@@ -168,6 +168,7 @@ export function resolveRelayThreadId(env = process.env) {
168
168
  export function nativeDispatchParams({ executorThreadId, targetThreadId, message }) {
169
169
  return {
170
170
  arguments: { threadId: targetThreadId, prompt: message },
171
+ callerSource: "codex",
171
172
  callId: `codex-native-relay-${randomUUID()}`,
172
173
  namespace: "codex_app",
173
174
  threadId: executorThreadId,
@@ -246,6 +247,7 @@ export function nativeDesktopOperationParams({ executorThreadId, operation, argu
246
247
  validateDesktopOperation(operation, args);
247
248
  return {
248
249
  arguments: args,
250
+ callerSource: "codex",
249
251
  callId: `codex-native-relay-${randomUUID()}`,
250
252
  namespace: "codex_app",
251
253
  threadId: executorThreadId,