@minhspark/codex-mcp-bridge 1.22.19 → 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.19",
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,38 @@ 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
+
23
+ ## [1.22.20] - 2026-10-24
24
+
25
+ ### Fixed
26
+
27
+ - A Claude Desktop target no longer stays blocked for good when another sender's peer message lands in a reply chain that is waiting on a background task. Reply observation used to drop that message and everything after it, so the delivery never resolved, every later send to that session failed with `PEER_REPLY_PENDING`, and reload stayed refused until Codex Desktop restarted. A peer message for a different message id now ends observation the same way human input does: the delivery is reported as interrupted, the task outcome stays unknown, and it is not marked safe to retry.
28
+ - Peer interruption requires a nonblank string message id distinct from the tracked request. Missing, blank, non-string and matching ids cannot release pending ownership. Existing transcript ancestry, ambiguity and open-branch checks remain in force.
29
+
30
+ ### Tests
31
+
32
+ - Transcript regressions cover a background wait interrupted by another peer, malformed peer ids, unrelated ancestry, incomplete or duplicate records, and unresolved sibling branches.
33
+ - Endpoint regressions verify late interruption after a timeout, pending-send and reload recovery, retained interruption receipts across reload, and a separately requested follow-up without resending the original action.
34
+
35
+ ### Limitations
36
+
37
+ - Interruption ends reply observation only. Background work may still be running; inspect the existing task before issuing a follow-up. The original action is never marked safe to retry.
38
+
7
39
  ## [1.22.19] - 2026-10-22
8
40
 
9
41
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhspark/codex-mcp-bridge",
3
- "version": "1.22.19",
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.19",
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.19";
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.19";
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.19";
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.19";
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) {
@@ -367,6 +367,12 @@ function injectedMessageRoot(entry, msgId) {
367
367
  return null;
368
368
  }
369
369
 
370
+ function isOtherPeerInput(entry, msgId) {
371
+ const origin = entry.origin;
372
+ return origin?.kind === "peer" && typeof origin.msg_id === "string"
373
+ && origin.msg_id.trim().length > 0 && origin.msg_id !== msgId;
374
+ }
375
+
370
376
  function isInterruptedDelivery(receipt) {
371
377
  return receipt?.status === "interrupted" && receipt.source === "transcript"
372
378
  && receipt.reasonCode === "CLAUDE_REPLY_INTERRUPTED"
@@ -480,6 +486,28 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
480
486
  if (roots.length !== 1) return null;
481
487
  const { entry: root, absorbed } = roots[0];
482
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
+ }
483
511
  const descendants = new Map();
484
512
  const backgroundCalls = new Map();
485
513
  const openLeaves = new Set();
@@ -491,7 +519,7 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
491
519
  openLeaves.delete(entry.parentUuid);
492
520
  interruptions.push({ entryId: entry.uuid, backgroundTaskIds: [...outstanding.keys()] });
493
521
  };
494
- for (const entry of entries) {
522
+ for (const entry of orderedEntries) {
495
523
  if (entry === root) { descendants.set(entry.uuid, new Map()); openLeaves.add(entry.uuid); continue; }
496
524
  let parentUuid = entry.parentUuid;
497
525
  if (!ambiguous && !interruptions.length && descendants.has(entry.logicalParentUuid)
@@ -503,6 +531,7 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
503
531
  }
504
532
  }
505
533
  if (!descendants.has(parentUuid)) continue;
534
+ let taskNotification = null;
506
535
  if (entry.type === "attachment" && entry.attachment?.type === "queued_command") {
507
536
  // Only a native command record is a boundary. Its text is never used as
508
537
  // proof of authorization, cancellation, or successful task completion.
@@ -510,12 +539,12 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
510
539
  interrupt(entry, descendants.get(entry.parentUuid));
511
540
  continue;
512
541
  }
513
- // An inline native task notification is not a new prompt. Preserve the
514
- // active reply chain even when the notification belongs to another task;
515
- // 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.
516
544
  if (entry.attachment.commandMode !== "task-notification"
517
545
  || entry.attachment.origin?.kind !== "task-notification"
518
546
  || entry.attachment.origin?.producer !== "session-task") continue;
547
+ taskNotification = entry.attachment.prompt;
519
548
  }
520
549
  const role = entry.message?.role;
521
550
  const content = entry.message?.content;
@@ -532,7 +561,7 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
532
561
  && (typeof content === "string" && content.trim()
533
562
  || Array.isArray(content) && content.length > 0 && content.every((part) => part?.type === "text" && typeof part.text === "string")
534
563
  && content.some((part) => part.text.trim()));
535
- if (ordinaryInput) {
564
+ if (ordinaryInput || isOtherPeerInput(entry, msgId)) {
536
565
  interrupt(entry, outstanding);
537
566
  continue;
538
567
  }
@@ -542,14 +571,18 @@ export function readTranscriptOutcome(sessionId, cwd, msgId) {
542
571
  if (entry.origin?.kind !== "task-notification" || entry.origin.producer !== "session-task"
543
572
  || entry.promptSource !== "system" || entry.turnOrigin !== "task_notification"
544
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())) {
545
577
  const field = (name) => {
546
- const matches = [...content.matchAll(new RegExp(`<${name}>([^<>]+)</${name}>`, "g"))];
578
+ const matches = [...taskNotification.matchAll(new RegExp(`<${name}>([^<>]+)</${name}>`, "g"))];
547
579
  return matches.length === 1 ? matches[0][1] : null;
548
580
  };
549
581
  const taskId = field("task-id");
550
582
  if (!outstanding.has(taskId) || outstanding.get(taskId) !== field("tool-use-id")
551
- || !["completed", "failed", "killed"].includes(field("status"))) continue;
552
- outstanding.delete(taskId);
583
+ || !["completed", "failed", "killed"].includes(field("status"))) {
584
+ if (role === "user") continue;
585
+ } else outstanding.delete(taskId);
553
586
  }
554
587
  if (typeof entry.uuid !== "string" || !entry.uuid.trim()) continue;
555
588
  if (role === "assistant" && Array.isArray(content)) {