@minhspark/codex-mcp-bridge 1.22.20 → 1.22.21

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-mcp-bridge",
3
- "version": "1.22.20",
3
+ "version": "1.22.21",
4
4
  "description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
5
5
  "author": {
6
6
  "name": "Bui Dang Minh"
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](ht
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.22.21] - 2026-10-26
8
+
9
+ ### Fixed
10
+
11
+ - Recognize native Claude background-task completion notifications stored as inline attachments. A completed command could otherwise remain outstanding, hiding the final reply and blocking later messages with `PEER_REPLY_PENDING`. Both notification formats retain native-origin, exact task/tool-call identity and transcript ancestry checks; observing the original reply never resends its action.
12
+ - Resolve Claude reply ancestry when a final response is persisted before its parent attachment. Process unambiguous records with ancestors first, reject cycles, and preserve input boundaries and pending ownership until the required parent arrives.
13
+
14
+ ### Verification
15
+
16
+ - Three focused regressions fail against 1.22.20 and pass with this fix: inline completion, child-before-parent ancestry, and pending ownership recovery without repeating the original action.
17
+ - Independent parser checks cover 1,320 record-order and invalid-notification combinations. Endpoint tests verify that missing parents keep ownership pending and that a distinct follow-up is allowed only after the original reply resolves.
18
+
19
+ ### Limitations
20
+
21
+ - Missing or ambiguous transcript ancestry still cannot establish a reply. Inspect the existing task instead of resending its original action. An already blocked older runtime may need a client restart after upgrading to load this fix.
22
+
7
23
  ## [1.22.20] - 2026-10-24
8
24
 
9
25
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhspark/codex-mcp-bridge",
3
- "version": "1.22.20",
3
+ "version": "1.22.21",
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",
@@ -83,6 +83,6 @@
83
83
  },
84
84
  "dependencies": {
85
85
  "@modelcontextprotocol/sdk": "^1.32.1",
86
- "zod": "^3.25.76"
86
+ "zod": "^4.6.7"
87
87
  }
88
88
  }
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "codex-mcp-bridge",
4
- "version": "1.22.20",
4
+ "version": "1.22.21",
5
5
  "description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
6
6
  "author": {
7
7
  "name": "Bui Dang Minh",
@@ -25,7 +25,7 @@ import { removeRelayFile } from "../src/native-relay-files.mjs";
25
25
 
26
26
  exitForVersionRequest(import.meta.url);
27
27
 
28
- const VERSION = "1.22.20";
28
+ const VERSION = "1.22.21";
29
29
  const root = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
30
30
  const entry = path.join(root, "src", "mcp-supervisor.mjs");
31
31
  const entryArgs = ["native-relay-companion.mjs"];
@@ -30,7 +30,7 @@ import { discoverClaudeProject } from "./claude-project-discovery.mjs";
30
30
 
31
31
  exitForVersionRequest(import.meta.url);
32
32
 
33
- const VERSION = "1.22.20";
33
+ const VERSION = "1.22.21";
34
34
  void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
35
35
  const FORWARD_MIN_INTERVAL_MS = 5000;
36
36
  const FORWARD_MAX_PER_SESSION = 50;
package/src/index.mjs CHANGED
@@ -41,7 +41,7 @@ import { createRelayWake, relayKeeperEnabled, relayWakeStateFile, startRelayKeep
41
41
 
42
42
  exitForVersionRequest(import.meta.url);
43
43
 
44
- const VERSION = "1.22.20";
44
+ const VERSION = "1.22.21";
45
45
  void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
46
46
  const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
47
47
 
@@ -40,7 +40,7 @@ import { NativeCreationReceipts, creationIdValid } from "./native-creation-recei
40
40
 
41
41
  exitForVersionRequest(import.meta.url);
42
42
 
43
- const VERSION = "1.22.20";
43
+ const VERSION = "1.22.21";
44
44
  const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
45
45
 
46
46
  function errorResponse(code, message, sent, details) {
@@ -486,6 +486,28 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
486
486
  if (roots.length !== 1) return null;
487
487
  const { entry: root, absorbed } = roots[0];
488
488
  const entriesById = new Map(entries.map((entry) => [entry.uuid, entry]));
489
+ const orderedEntries = ambiguous ? entries : [];
490
+ if (!ambiguous) {
491
+ // Claude can persist a reply before its parent attachment; visit late
492
+ // ancestors before their children instead of assuming append order.
493
+ const visited = new Set();
494
+ for (const entry of entries) {
495
+ const ancestors = [];
496
+ const visiting = new Set();
497
+ let ancestor = entry;
498
+ while (ancestor && !visited.has(ancestor)) {
499
+ if (visiting.has(ancestor)) return null;
500
+ visiting.add(ancestor);
501
+ ancestors.push(ancestor);
502
+ ancestor = typeof ancestor.parentUuid === "string" ? entriesById.get(ancestor.parentUuid) : null;
503
+ }
504
+ while (ancestors.length) {
505
+ const ancestor = ancestors.pop();
506
+ visited.add(ancestor);
507
+ orderedEntries.push(ancestor);
508
+ }
509
+ }
510
+ }
489
511
  const descendants = new Map();
490
512
  const backgroundCalls = new Map();
491
513
  const openLeaves = new Set();
@@ -497,7 +519,7 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
497
519
  openLeaves.delete(entry.parentUuid);
498
520
  interruptions.push({ entryId: entry.uuid, backgroundTaskIds: [...outstanding.keys()] });
499
521
  };
500
- for (const entry of entries) {
522
+ for (const entry of orderedEntries) {
501
523
  if (entry === root) { descendants.set(entry.uuid, new Map()); openLeaves.add(entry.uuid); continue; }
502
524
  let parentUuid = entry.parentUuid;
503
525
  if (!ambiguous && !interruptions.length && descendants.has(entry.logicalParentUuid)
@@ -509,6 +531,7 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
509
531
  }
510
532
  }
511
533
  if (!descendants.has(parentUuid)) continue;
534
+ let taskNotification = null;
512
535
  if (entry.type === "attachment" && entry.attachment?.type === "queued_command") {
513
536
  // Only a native command record is a boundary. Its text is never used as
514
537
  // proof of authorization, cancellation, or successful task completion.
@@ -516,12 +539,12 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
516
539
  interrupt(entry, descendants.get(entry.parentUuid));
517
540
  continue;
518
541
  }
519
- // An inline native task notification is not a new prompt. Preserve the
520
- // active reply chain even when the notification belongs to another task;
521
- // its text alone never completes any outstanding background command.
542
+ // Inline native notifications preserve the chain; only matching task and
543
+ // tool-call identities below can complete an outstanding command.
522
544
  if (entry.attachment.commandMode !== "task-notification"
523
545
  || entry.attachment.origin?.kind !== "task-notification"
524
546
  || entry.attachment.origin?.producer !== "session-task") continue;
547
+ taskNotification = entry.attachment.prompt;
525
548
  }
526
549
  const role = entry.message?.role;
527
550
  const content = entry.message?.content;
@@ -548,14 +571,18 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
548
571
  if (entry.origin?.kind !== "task-notification" || entry.origin.producer !== "session-task"
549
572
  || entry.promptSource !== "system" || entry.turnOrigin !== "task_notification"
550
573
  || typeof content !== "string" || !/^<task-notification>\s[\s\S]*<\/task-notification>$/.test(content.trim())) continue;
574
+ taskNotification = content;
575
+ }
576
+ if (typeof taskNotification === "string" && /^<task-notification>\s[\s\S]*<\/task-notification>$/.test(taskNotification.trim())) {
551
577
  const field = (name) => {
552
- const matches = [...content.matchAll(new RegExp(`<${name}>([^<>]+)</${name}>`, "g"))];
578
+ const matches = [...taskNotification.matchAll(new RegExp(`<${name}>([^<>]+)</${name}>`, "g"))];
553
579
  return matches.length === 1 ? matches[0][1] : null;
554
580
  };
555
581
  const taskId = field("task-id");
556
582
  if (!outstanding.has(taskId) || outstanding.get(taskId) !== field("tool-use-id")
557
- || !["completed", "failed", "killed"].includes(field("status"))) continue;
558
- outstanding.delete(taskId);
583
+ || !["completed", "failed", "killed"].includes(field("status"))) {
584
+ if (role === "user") continue;
585
+ } else outstanding.delete(taskId);
559
586
  }
560
587
  if (typeof entry.uuid !== "string" || !entry.uuid.trim()) continue;
561
588
  if (role === "assistant" && Array.isArray(content)) {