@minhspark/codex-mcp-bridge 1.13.9 → 1.15.0

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.
@@ -6,6 +6,7 @@ import { execFile } from "node:child_process";
6
6
  import { promisify } from "node:util";
7
7
 
8
8
  import { IS_MACOS, IS_WINDOWS, PLATFORM_LABEL, homeDir } from "./platform.mjs";
9
+ import { assertAccountIdentity } from "./bridge-account-context.mjs";
9
10
 
10
11
  /**
11
12
  * The Codex Desktop app owns the per-thread writer lock of every thread it has
@@ -35,6 +36,7 @@ import { IS_MACOS, IS_WINDOWS, PLATFORM_LABEL, homeDir } from "./platform.mjs";
35
36
  export const NATIVE_DISPATCH_METHOD = "tools/call";
36
37
 
37
38
  export const RELAY_PROTOCOL_VERSION = 1;
39
+ export const ACCOUNT_RELAY_PROTOCOL_VERSION = 2;
38
40
 
39
41
  /**
40
42
  * A relay frame carries one chat message, so a megabyte-scale line is either a
@@ -70,6 +72,28 @@ export function desktopTaskSocketPath(env = process.env) {
70
72
  return env.CODEX_NATIVE_RELAY_SOCKET ?? `${relaySocketPath(env)}-desktop-tasks`;
71
73
  }
72
74
 
75
+ export function accountRelaySocketPath(env = process.env) {
76
+ return `${relaySocketPath(env)}-accounts-v2`;
77
+ }
78
+
79
+ export function relayAccountContext(value) {
80
+ if (value === undefined) return undefined;
81
+ if (!exactObject(value, ["claude", "codex"]) || Object.keys(value).length !== 2 || [value.claude, value.codex].some((item) => typeof item !== "string" || !/^[a-f0-9]{64}$/.test(item))) {
82
+ throw beforeWriteError(new NativeRelayError("A bound relay request requires exact Claude and Codex account fingerprints", "RELAY_BAD_REQUEST"));
83
+ }
84
+ return Object.freeze({ claude: value.claude, codex: value.codex });
85
+ }
86
+
87
+ function beforeWriteError(error) {
88
+ const failure = new NativeRelayError(error?.message ?? "Relay preflight failed before dispatch", typeof error?.code === "string" ? error.code : "RELAY_PREFLIGHT_FAILED", { reachedCompanion: false });
89
+ failure.sent = false;
90
+ return failure;
91
+ }
92
+
93
+ function validateBeforeSend(beforeSend) {
94
+ if (beforeSend !== undefined && typeof beforeSend !== "function") throw beforeWriteError(new NativeRelayError("Relay beforeSend must be a function", "RELAY_BAD_REQUEST"));
95
+ }
96
+
73
97
  export function desktopTasksConfigured(env = process.env) {
74
98
  return env.CODEX_BRIDGE_DESKTOP_TASKS !== undefined
75
99
  ? env.CODEX_BRIDGE_DESKTOP_TASKS === "1"
@@ -409,11 +433,14 @@ export async function resolveNativeToolsPipePath({
409
433
  }
410
434
 
411
435
  export class NativeToolsClient {
412
- constructor({ env = process.env, socketPath = env.CODEX_APP_TOOLS_PIPE_PATH, timeoutMs = DEFAULT_TIMEOUT_MS, resolveSocketPath = () => resolveNativeToolsPipePath({ env }) } = {}) {
436
+ constructor({ env = process.env, socketPath = env.CODEX_APP_TOOLS_PIPE_PATH, timeoutMs = DEFAULT_TIMEOUT_MS, resolveSocketPath = () => resolveNativeToolsPipePath({ env }), assertAccount = assertAccountIdentity } = {}) {
413
437
  this.env = env;
414
438
  this.socketPath = socketPath;
439
+ this.explicitSocketPath = socketPath || null;
440
+ this.hasDiscoveredSocket = false;
415
441
  this.timeoutMs = timeoutMs;
416
442
  this.resolveSocketPath = resolveSocketPath;
443
+ this.assertAccount = assertAccount;
417
444
  this.socket = null;
418
445
  this.connectingSocket = null;
419
446
  this.connecting = null;
@@ -432,6 +459,7 @@ export class NativeToolsClient {
432
459
  throw new NativeRelayError("Native tools client closed while discovering the Desktop pipe", "NATIVE_PIPE_UNAVAILABLE");
433
460
  }
434
461
  this.socketPath = socketPath;
462
+ if (socketPath && !this.explicitSocketPath) this.hasDiscoveredSocket = true;
435
463
  if (!this.socketPath) {
436
464
  throw new NativeRelayError("CODEX_APP_TOOLS_PIPE_PATH is missing from the environment and parent Desktop app-server configuration; launch the companion from Codex Desktop", "NATIVE_PIPE_UNAVAILABLE");
437
465
  }
@@ -440,14 +468,17 @@ export class NativeToolsClient {
440
468
  this.connectingSocket = socket;
441
469
  let buffer = Buffer.alloc(0);
442
470
  let connected = false;
471
+ let failed = false;
443
472
  const timer = globalThis.setTimeout(() => {
444
- reject(new NativeRelayError("Timed out connecting to the Codex Desktop native tools pipe", "NATIVE_PIPE_UNAVAILABLE"));
445
- socket.destroy();
473
+ fail(new NativeRelayError("Timed out connecting to the Codex Desktop native tools pipe", "NATIVE_PIPE_UNAVAILABLE"));
446
474
  }, this.timeoutMs);
447
475
  const fail = (err) => {
476
+ if (failed) return;
477
+ failed = true;
448
478
  globalThis.clearTimeout(timer);
449
479
  if (!connected) reject(err);
450
480
  if (this.socket === socket) this.socket = null;
481
+ if (!this.explicitSocketPath && this.connectionGeneration === generation) this.socketPath = null;
451
482
  for (const pending of this.pending.values()) {
452
483
  if (pending.socket === socket) pending.reject(err);
453
484
  }
@@ -503,15 +534,17 @@ export class NativeToolsClient {
503
534
  }
504
535
  }
505
536
 
506
- async dispatch(args) {
507
- return this.#request(nativeDispatchParams(args));
537
+ async dispatch(args, options) {
538
+ return this.#request(nativeDispatchParams(args), options);
508
539
  }
509
540
 
510
- async dispatchDesktop(args) {
511
- return this.#request(nativeDesktopOperationParams(args));
541
+ async dispatchDesktop(args, options) {
542
+ return this.#request(nativeDesktopOperationParams(args), options);
512
543
  }
513
544
 
514
- async #request(params) {
545
+ async #request(params, { beforeSend, accountContext } = {}) {
546
+ validateBeforeSend(beforeSend);
547
+ const accounts = relayAccountContext(accountContext);
515
548
  const id = this.nextId++;
516
549
  const payload = Buffer.from(JSON.stringify({
517
550
  jsonrpc: "2.0",
@@ -522,31 +555,50 @@ export class NativeToolsClient {
522
555
  if (payload.length > MAX_FRAME_BYTES) {
523
556
  throw new NativeRelayError("Native dispatch exceeds the frame limit", "RELAY_MESSAGE_TOO_LARGE");
524
557
  }
525
- await this.connect();
558
+ try {
559
+ await this.connect();
560
+ } catch (error) {
561
+ throw beforeWriteError(error);
562
+ }
526
563
  const header = Buffer.alloc(4);
527
564
  header.writeUInt32LE(payload.length);
528
565
  return new Promise((resolve, reject) => {
566
+ const socket = this.socket;
567
+ let dispatched = false;
529
568
  const finish = (fn, value) => {
530
569
  if (!this.pending.delete(id)) return;
531
570
  globalThis.clearTimeout(timer);
532
571
  fn(value);
533
572
  };
534
573
  const timer = globalThis.setTimeout(() => {
535
- finish(reject, new NativeRelayError("Codex Desktop native dispatch timed out; delivery may have occurred", "NATIVE_DELIVERY_UNCONFIRMED"));
574
+ finish(reject, dispatched
575
+ ? new NativeRelayError("Codex Desktop native dispatch timed out; delivery may have occurred", "NATIVE_DELIVERY_UNCONFIRMED")
576
+ : beforeWriteError(new NativeRelayError("Codex Desktop native send deadline elapsed before dispatch", "NATIVE_PIPE_UNAVAILABLE")));
536
577
  }, this.timeoutMs);
537
578
  this.pending.set(id, {
538
- socket: this.socket,
579
+ socket,
539
580
  resolve: (value) => finish(resolve, value),
540
- reject: (err) => finish(reject, err),
581
+ reject: (err) => finish(reject, dispatched ? err : beforeWriteError(err)),
541
582
  });
542
- try {
543
- if (!this.socket || this.socket.destroyed) throw new Error("native tools pipe is closed");
544
- this.socket.write(Buffer.concat([header, payload]), (err) => {
545
- if (err) finish(reject, new NativeRelayError(`Native dispatch write failed: ${err.message}`, "NATIVE_DELIVERY_UNCONFIRMED"));
546
- });
547
- } catch (err) {
548
- finish(reject, new NativeRelayError(`Native dispatch write failed: ${err.message}`, "NATIVE_DELIVERY_UNCONFIRMED"));
549
- }
583
+ void (async () => {
584
+ try {
585
+ const checked = beforeSend?.();
586
+ if (checked && typeof checked.then === "function") await checked;
587
+ if (!this.pending.has(id)) return;
588
+ if (accounts) {
589
+ const verified = this.assertAccount(accounts);
590
+ if (verified && typeof verified.then === "function") await verified;
591
+ }
592
+ if (!this.pending.has(id)) return;
593
+ if (!socket || socket.destroyed || this.socket !== socket) throw new NativeRelayError("Native tools pipe closed before dispatch", "NATIVE_PIPE_UNAVAILABLE");
594
+ dispatched = true;
595
+ socket.write(Buffer.concat([header, payload]), (err) => {
596
+ if (err) finish(reject, new NativeRelayError(`Native dispatch write failed: ${err.message}`, "NATIVE_DELIVERY_UNCONFIRMED"));
597
+ });
598
+ } catch (err) {
599
+ finish(reject, dispatched ? new NativeRelayError(`Native dispatch write failed: ${err.message}`, "NATIVE_DELIVERY_UNCONFIRMED") : beforeWriteError(err));
600
+ }
601
+ })();
550
602
  });
551
603
  }
552
604
 
@@ -554,6 +606,7 @@ export class NativeToolsClient {
554
606
  this.connectionGeneration++;
555
607
  const socket = this.socket;
556
608
  this.socket = null;
609
+ if (!this.explicitSocketPath) this.socketPath = null;
557
610
  for (const pending of this.pending.values()) {
558
611
  pending.reject(new NativeRelayError("Native tools client closed before confirming delivery", "NATIVE_DELIVERY_UNCONFIRMED"));
559
612
  }
@@ -603,9 +656,10 @@ export function nativeRelayStatus(env = process.env) {
603
656
  * socket cannot go stale while Codex Desktop restarts underneath it.
604
657
  */
605
658
  export class NativeDesktopRelay {
606
- constructor({ env = process.env, socketPath = null, timeoutMs = DEFAULT_TIMEOUT_MS, log = () => {} } = {}) {
659
+ constructor({ env = process.env, socketPath = null, accountSocketPath = null, timeoutMs = DEFAULT_TIMEOUT_MS, log = () => {} } = {}) {
607
660
  this.env = env;
608
661
  this.explicitSocketPath = socketPath;
662
+ this.accountSocketPath = accountSocketPath ?? socketPath ?? accountRelaySocketPath(env);
609
663
  this.timeoutMs = timeoutMs;
610
664
  this.log = log;
611
665
  }
@@ -614,27 +668,31 @@ export class NativeDesktopRelay {
614
668
  return this.explicitSocketPath ?? relaySocketPath(this.env);
615
669
  }
616
670
 
617
- status() {
618
- if (!this.explicitSocketPath) return nativeRelayStatus(this.env);
619
- const status = nativeRelayStatus({ ...this.env, CODEX_NATIVE_RELAY_SOCKET: this.explicitSocketPath });
620
- return { ...status, socketPath: this.explicitSocketPath };
671
+ status({ accountContext } = {}) {
672
+ const socketPath = accountContext ? this.accountSocketPath : this.explicitSocketPath;
673
+ if (!socketPath) return nativeRelayStatus(this.env);
674
+ const status = nativeRelayStatus({ ...this.env, CODEX_NATIVE_RELAY_SOCKET: socketPath });
675
+ return { ...status, socketPath };
621
676
  }
622
677
 
623
678
  get available() {
624
679
  return this.status().enabled;
625
680
  }
626
681
 
627
- async sendMessage(targetThreadId, message, { timeoutMs = this.timeoutMs } = {}) {
628
- const request = { v: RELAY_PROTOCOL_VERSION, targetThreadId, message };
629
- return this.#request(request, timeoutMs);
682
+ async sendMessage(targetThreadId, message, { timeoutMs = this.timeoutMs, beforeSend, accountContext } = {}) {
683
+ const accounts = relayAccountContext(accountContext);
684
+ const request = { v: accounts ? ACCOUNT_RELAY_PROTOCOL_VERSION : RELAY_PROTOCOL_VERSION, targetThreadId, message, ...(accounts ? { accountContext: accounts } : {}) };
685
+ return this.#request(request, timeoutMs, beforeSend);
630
686
  }
631
687
 
632
- async requestDesktop(operation, args, { timeoutMs = this.timeoutMs } = {}) {
688
+ async requestDesktop(operation, args, { timeoutMs = this.timeoutMs, beforeSend, accountContext } = {}) {
633
689
  validateDesktopOperation(operation, args);
634
- return this.#request({ v: RELAY_PROTOCOL_VERSION, operation, arguments: args }, timeoutMs);
690
+ const accounts = relayAccountContext(accountContext);
691
+ return this.#request({ v: accounts ? ACCOUNT_RELAY_PROTOCOL_VERSION : RELAY_PROTOCOL_VERSION, operation, arguments: args, ...(accounts ? { accountContext: accounts } : {}) }, timeoutMs, beforeSend);
635
692
  }
636
693
 
637
- async #request(request, timeoutMs) {
694
+ async #request(request, timeoutMs, beforeSend) {
695
+ validateBeforeSend(beforeSend);
638
696
  const line = `${JSON.stringify(request)}\n`;
639
697
  if (Buffer.byteLength(line, "utf8") > MAX_FRAME_BYTES) {
640
698
  throw new NativeRelayError(
@@ -643,20 +701,21 @@ export class NativeDesktopRelay {
643
701
  );
644
702
  }
645
703
 
646
- const response = await this.#roundTrip(line, timeoutMs);
647
- if (response?.ok === true && response.v === RELAY_PROTOCOL_VERSION) {
704
+ const response = await this.#roundTrip(line, timeoutMs, beforeSend, request.accountContext ? this.accountSocketPath : this.socketPath);
705
+ if (response?.ok === true && response.v === request.v) {
648
706
  if (request.operation && response.operation !== request.operation) throw new NativeRelayError("The relay answered a different Desktop operation; do not retry this request", "RELAY_BAD_RESPONSE", { reachedCompanion: true });
649
707
  return response;
650
708
  }
651
- throw new NativeRelayError(
709
+ const error = new NativeRelayError(
652
710
  response?.error?.message ?? "the Codex Desktop relay refused the message",
653
711
  response?.error?.code ?? "NATIVE_DISPATCH_FAILED",
654
- { reachedCompanion: true },
712
+ { reachedCompanion: response?.error?.sent !== false },
655
713
  );
714
+ if (response?.error?.sent === false) error.sent = false;
715
+ throw error;
656
716
  }
657
717
 
658
- #roundTrip(line, timeoutMs) {
659
- const socketPath = this.socketPath;
718
+ #roundTrip(line, timeoutMs, beforeSend, socketPath) {
660
719
  return new Promise((resolve, reject) => {
661
720
  let buffer = Buffer.alloc(0);
662
721
  let settled = false;
@@ -675,18 +734,29 @@ export class NativeDesktopRelay {
675
734
  () =>
676
735
  finish(
677
736
  reject,
678
- new NativeRelayError(
737
+ dispatched ? new NativeRelayError(
679
738
  `The Codex Desktop relay did not answer within ${timeoutMs}ms`,
680
739
  "RELAY_TIMEOUT",
681
- { reachedCompanion: dispatched },
682
- ),
740
+ { reachedCompanion: true },
741
+ ) : beforeWriteError(new NativeRelayError("The Codex Desktop relay send deadline elapsed before dispatch", "RELAY_UNREACHABLE")),
683
742
  ),
684
743
  timeoutMs,
685
744
  );
686
745
 
687
746
  socket.on("connect", () => {
688
- dispatched = true;
689
- socket.write(line);
747
+ if (settled) return;
748
+ void (async () => {
749
+ try {
750
+ const checked = beforeSend?.();
751
+ if (checked && typeof checked.then === "function") await checked;
752
+ if (settled) return;
753
+ if (socket.destroyed) throw new NativeRelayError("The relay socket closed before dispatch", "RELAY_UNREACHABLE");
754
+ dispatched = true;
755
+ socket.write(line);
756
+ } catch (error) {
757
+ finish(reject, dispatched ? new NativeRelayError(`Relay write failed: ${error.message}`, "RELAY_DELIVERY_UNCONFIRMED", { reachedCompanion: true }) : beforeWriteError(error));
758
+ }
759
+ })();
690
760
  });
691
761
  socket.on("data", (chunk) => {
692
762
  buffer = Buffer.concat([buffer, chunk]);
@@ -717,17 +787,19 @@ export class NativeDesktopRelay {
717
787
  socket.on("error", (err) =>
718
788
  finish(
719
789
  reject,
720
- new NativeRelayError(`Codex Desktop relay connection failed at ${socketPath}: ${err.message}`, dispatched ? "RELAY_DELIVERY_UNCONFIRMED" : "RELAY_UNREACHABLE", { reachedCompanion: dispatched }),
790
+ dispatched
791
+ ? new NativeRelayError(`Codex Desktop relay connection failed at ${socketPath}: ${err.message}`, "RELAY_DELIVERY_UNCONFIRMED", { reachedCompanion: true })
792
+ : beforeWriteError(new NativeRelayError(`Codex Desktop relay connection failed at ${socketPath}: ${err.message}`, "RELAY_UNREACHABLE")),
721
793
  ),
722
794
  );
723
795
  socket.on("close", () =>
724
796
  finish(
725
797
  reject,
726
- new NativeRelayError(
798
+ dispatched ? new NativeRelayError(
727
799
  `The Codex Desktop relay at ${socketPath} closed before answering`,
728
- dispatched ? "RELAY_DELIVERY_UNCONFIRMED" : "RELAY_UNREACHABLE",
729
- { reachedCompanion: dispatched },
730
- ),
800
+ "RELAY_DELIVERY_UNCONFIRMED",
801
+ { reachedCompanion: true },
802
+ ) : beforeWriteError(new NativeRelayError(`The Codex Desktop relay at ${socketPath} closed before dispatch`, "RELAY_UNREACHABLE")),
731
803
  ),
732
804
  );
733
805
  });
@@ -6,6 +6,7 @@ import os from "node:os";
6
6
  import path from "node:path";
7
7
 
8
8
  import { homeDir, IS_WINDOWS } from "./platform.mjs";
9
+ import { cloneReloadState } from "./reload-control.mjs";
9
10
 
10
11
  const SOCKET_DIR = "/tmp/cc-socks";
11
12
 
@@ -366,6 +367,10 @@ export class PeerEndpoint {
366
367
  this.sentMessages = new Map();
367
368
  this.pendingMessages = new Map();
368
369
  this.responsePoll = null;
370
+ this.connections = new Set();
371
+ this.activityRevision = 0;
372
+ this.reloadPaused = false;
373
+ this.processHandlersRegistered = false;
369
374
  }
370
375
 
371
376
  /**
@@ -396,6 +401,7 @@ export class PeerEndpoint {
396
401
  }
397
402
 
398
403
  async start() {
404
+ if (this.reloadPaused) throw new Error("The peer endpoint is quiesced for reload");
399
405
  if (this.started) return;
400
406
  this.#sweepDeadBridges();
401
407
  const procStart = readProcessStart(this.pid);
@@ -432,24 +438,33 @@ export class PeerEndpoint {
432
438
  fs.writeFileSync(this.registryPath, JSON.stringify(this.registry), { mode: 0o600 });
433
439
  fs.writeFileSync(this.keyPath, JSON.stringify({ peerToken, ...processIdentity }), { mode: 0o600 });
434
440
 
435
- for (const signal of ["SIGINT", "SIGTERM"]) {
436
- process.on(signal, () => {
437
- this.stop();
438
- process.exit(0);
439
- });
441
+ if (!this.processHandlersRegistered) {
442
+ for (const signal of ["SIGINT", "SIGTERM"]) {
443
+ process.on(signal, () => {
444
+ this.stop();
445
+ process.exit(0);
446
+ });
447
+ }
448
+ process.on("exit", () => this.stop());
449
+ this.processHandlersRegistered = true;
440
450
  }
441
- process.on("exit", () => this.stop());
442
451
 
443
452
  this.started = true;
444
453
  this.log(`peer "${this.name}" listening on ${this.socketPath}`);
445
454
  }
446
455
 
447
456
  #handleConnection(socket) {
457
+ this.activityRevision += 1;
458
+ this.connections.add(socket);
459
+ socket.once("close", () => this.connections.delete(socket));
460
+ if (this.reloadPaused) { socket.destroy(); return; }
448
461
  let buffer = "";
449
462
  let authenticated = false;
450
463
  let firstLine = true;
451
464
  socket.setEncoding("utf8");
452
465
  socket.on("data", (chunk) => {
466
+ this.activityRevision += 1;
467
+ if (this.reloadPaused) { socket.destroy(); return; }
453
468
  buffer += chunk;
454
469
  if (buffer.length > 1048576) { socket.destroy(); return; }
455
470
  let index;
@@ -502,6 +517,7 @@ export class PeerEndpoint {
502
517
  }
503
518
 
504
519
  #receiveMessage(message) {
520
+ this.activityRevision += 1;
505
521
  const record = { ...message, receivedAt: Date.now(), sequence: ++this.messageSequence };
506
522
  this.inbox.push(record);
507
523
  const key = record.inReplyTo ?? [...this.pendingMessages].find(([, entry]) => entry.targetSocket === record.fromSocket)?.[0];
@@ -512,11 +528,12 @@ export class PeerEndpoint {
512
528
  if (sent) {
513
529
  record.inReplyTo = key;
514
530
  record.replyThreadId = sent.replyThreadId ?? null;
531
+ record.accountContext = sent.accountContext ?? null;
515
532
  sent.reply = record;
516
533
  }
517
534
  this.#removePendingReply(record.fromSocket, key);
518
535
  }
519
- this.log(`inbox <- ${record.fromSocket ?? "?"}: ${record.text.slice(0, 120)}`);
536
+ this.log(`inbox <- ${record.fromSocket ?? "?"}: ${record.text.length} characters`);
520
537
  for (const listener of [...this.listeners]) {
521
538
  try { listener(record); }
522
539
  catch (err) { this.log(`peer listener error: ${err.message}`); }
@@ -564,6 +581,7 @@ export class PeerEndpoint {
564
581
  }
565
582
 
566
583
  async send(targetSocket, text, { priority = "next", msgId, permissionMode = this.permissionMode, beforeSend } = {}) {
584
+ if (this.reloadPaused) throw new Error("The peer endpoint is quiesced; no message was sent");
567
585
  const frame = buildFrame({ text, fromSocket: this.socketPath, priority, permissionMode });
568
586
  if (msgId) frame.uuid = frame.msg_id = msgId;
569
587
  const token = readPeerToken(targetSocket);
@@ -573,6 +591,8 @@ export class PeerEndpoint {
573
591
  try {
574
592
  await new Promise((resolve, reject) => {
575
593
  const client = net.connect({ path: targetSocket });
594
+ this.connections.add(client);
595
+ client.once("close", () => this.connections.delete(client));
576
596
  let connected = false;
577
597
  let writeStarted = false;
578
598
  let settled = false;
@@ -626,7 +646,7 @@ export class PeerEndpoint {
626
646
  return frame.msg_id;
627
647
  }
628
648
 
629
- async sendAndWait(targetSocket, text, { timeoutMs = 120000, priority = "next", transcriptSession, beforeSend, permissionMode = this.permissionMode, replyThreadId, senderReview, senderApprovalPolicy, recipient } = {}) {
649
+ async sendAndWait(targetSocket, text, { timeoutMs = 120000, priority = "next", transcriptSession, beforeSend, permissionMode = this.permissionMode, replyThreadId, senderReview, senderApprovalPolicy, recipient, accountContext } = {}) {
630
650
  const previous = this.requestQueues.get(targetSocket) ?? Promise.resolve();
631
651
  const pending = previous.catch(() => {}).then(async () => {
632
652
  await beforeSend?.();
@@ -644,7 +664,7 @@ export class PeerEndpoint {
644
664
  this.unconfirmedReplies.set(targetSocket, unconfirmed + 1);
645
665
  let msgId = crypto.randomUUID();
646
666
  this.pendingMessages.set(msgId, { targetSocket, transcriptSession });
647
- this.sentMessages.set(msgId, { targetSocket, transcriptSession, sentAt: since, replyThreadId, permissionMode, senderApprovalPolicy, ...(senderReview ? { senderReview: { ...senderReview } } : {}), ...(recipient ? { recipient: { ...recipient } } : {}) });
667
+ this.sentMessages.set(msgId, { targetSocket, transcriptSession, sentAt: since, replyThreadId, permissionMode, senderApprovalPolicy, ...(senderReview ? { senderReview: { ...senderReview } } : {}), ...(recipient ? { recipient: { ...recipient } } : {}), ...(accountContext ? { accountContext: Object.freeze({ ...accountContext }) } : {}) });
648
668
  if (transcriptSession && !this.responsePoll) {
649
669
  this.responsePoll = globalThis.setInterval(() => this.#refreshTranscriptReplies(), 250);
650
670
  this.responsePoll.unref();
@@ -726,9 +746,15 @@ export class PeerEndpoint {
726
746
  });
727
747
  }
728
748
 
729
- drainInbox(limit = 20) {
749
+ drainInbox(limit = 20, accepts = () => true) {
730
750
  if (!Number.isSafeInteger(limit) || limit < 1) throw new Error("Inbox limit must be a positive integer");
731
- return this.inbox.splice(0, limit);
751
+ const selected = [];
752
+ this.inbox = this.inbox.filter((record) => {
753
+ if (selected.length >= limit || !accepts(record)) return true;
754
+ selected.push(record);
755
+ return false;
756
+ });
757
+ return selected;
732
758
  }
733
759
 
734
760
  readDelivery(msgId) {
@@ -751,11 +777,91 @@ export class PeerEndpoint {
751
777
  ...(sent.senderReview ? { senderReview: { ...sent.senderReview } } : {}),
752
778
  ...(sent.recipient ? { recipientPermissionMode: sent.recipient.permissionMode ?? null, recipientPermissionClass: sent.recipient.permissionClass ?? null, recipientInboundPolicy: sent.recipient.inboundPolicy ?? null } : {}),
753
779
  replyThreadId: sent.replyThreadId ?? null,
780
+ ...(sent.accountContext ? { accountContext: { ...sent.accountContext } } : {}),
754
781
  pending: this.pendingMessages.has(msgId),
755
782
  ...(sent.reply ? { reply: sent.reply.text, source: sent.reply.source ?? "peer", ...(sent.reply.absorbed ? { replyAbsorbed: true } : {}), ...(sent.reply.forwardingError ? { forwardingError: { ...sent.reply.forwardingError } } : {}) } : {}),
756
783
  };
757
784
  }
758
785
 
786
+ reloadReason() {
787
+ if (this.connections.size) return "Peer socket connections are still open";
788
+ if (this.requestQueues.size || this.pendingMessages.size || this.unconfirmedReplies.size) return "Claude messages still have pending or unconfirmed delivery";
789
+ for (const [id, sent] of this.sentMessages) {
790
+ if (!sent.reply && !sent.failed && !["refused", "denied", "expired", "dropped"].includes(this.deliveryReceipts.get(id)?.status)) {
791
+ return "A Claude delivery outcome remains unconfirmed";
792
+ }
793
+ }
794
+ return null;
795
+ }
796
+
797
+ async quiesce() {
798
+ const reason = this.reloadReason();
799
+ if (reason) throw new Error(reason);
800
+ const revision = this.activityRevision;
801
+ this.reloadPaused = true;
802
+ globalThis.clearInterval(this.responsePoll);
803
+ this.responsePoll = null;
804
+ if (this.server?.listening) {
805
+ await new Promise((resolve, reject) => this.server.close((error) => error ? reject(error) : resolve()));
806
+ }
807
+ this.started = false;
808
+ this.#removeRegistration();
809
+ if (revision !== this.activityRevision || this.reloadReason()) throw new Error("Peer activity raced with reload; the endpoint remains quiesced");
810
+ }
811
+
812
+ async resume() {
813
+ this.reloadPaused = false;
814
+ await this.start();
815
+ }
816
+
817
+ exportReloadState() {
818
+ const reason = this.reloadReason();
819
+ if (reason || this.started && !this.reloadPaused) throw new Error(reason ?? "Quiesce the peer endpoint before exporting state");
820
+ return cloneReloadState({ name: this.name, messageSequence: this.messageSequence, inbox: this.inbox,
821
+ sentMessages: [...this.sentMessages], deliveryReceipts: [...this.deliveryReceipts] });
822
+ }
823
+
824
+ restoreReloadState(input) {
825
+ if (this.started || this.connections.size || this.sentMessages.size || this.inbox.length) throw new Error("Peer reload state requires an empty dormant endpoint");
826
+ const state = cloneReloadState(input);
827
+ if (!state || typeof state.name !== "string" || !state.name || !Number.isSafeInteger(state.messageSequence) || state.messageSequence < 0
828
+ || !Array.isArray(state.inbox) || !Array.isArray(state.sentMessages) || !Array.isArray(state.deliveryReceipts)) throw new Error("Invalid peer reload state");
829
+ const restoreMap = (entries) => {
830
+ const restored = new Map();
831
+ for (const pair of entries) {
832
+ if (!Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== "string" || !pair[0] || restored.has(pair[0])
833
+ || !pair[1] || typeof pair[1] !== "object" || Array.isArray(pair[1])) throw new Error("Invalid peer reload record");
834
+ restored.set(pair[0], pair[1]);
835
+ }
836
+ return restored;
837
+ };
838
+ const sentMessages = restoreMap(state.sentMessages);
839
+ const deliveryReceipts = restoreMap(state.deliveryReceipts);
840
+ for (const record of state.inbox) {
841
+ if (!record || typeof record.text !== "string" || !Number.isSafeInteger(record.sequence) || record.sequence < 1
842
+ || record.sequence > state.messageSequence || !Number.isFinite(record.receivedAt)) throw new Error("Invalid inbox reload record");
843
+ }
844
+ for (const [id, sent] of sentMessages) {
845
+ if (typeof sent.targetSocket !== "string" || !sent.targetSocket || !Number.isFinite(sent.sentAt)
846
+ || sent.reply && (typeof sent.reply.text !== "string" || sent.reply.inReplyTo !== id)
847
+ || !sent.reply && !sent.failed && !["refused", "denied", "expired", "dropped"].includes(deliveryReceipts.get(id)?.status)) {
848
+ throw new Error("Invalid or unconfirmed Claude delivery reload record");
849
+ }
850
+ }
851
+ this.name = state.name;
852
+ this.messageSequence = state.messageSequence;
853
+ this.inbox = state.inbox;
854
+ this.sentMessages = sentMessages;
855
+ this.deliveryReceipts = deliveryReceipts;
856
+ }
857
+
858
+ #removeRegistration() {
859
+ for (const file of [this.socketPath, this.registryPath, this.keyPath]) {
860
+ if (!file) continue;
861
+ try { fs.rmSync(file, { force: true }); } catch {}
862
+ }
863
+ }
864
+
759
865
  stop() {
760
866
  globalThis.clearInterval(this.responsePoll);
761
867
  this.responsePoll = null;
@@ -763,12 +869,9 @@ export class PeerEndpoint {
763
869
  try {
764
870
  this.server?.close();
765
871
  } catch {}
766
- for (const file of [this.socketPath, this.registryPath, this.keyPath]) {
767
- if (!file) continue;
768
- try {
769
- fs.rmSync(file, { force: true });
770
- } catch {}
771
- }
872
+ for (const socket of this.connections) socket.destroy();
873
+ this.connections.clear();
874
+ this.#removeRegistration();
772
875
  this.started = false;
773
876
  }
774
877
  }
package/src/platform.mjs CHANGED
@@ -61,7 +61,10 @@ const FOREIGN_ROOT_PATTERNS = [
61
61
  * directory layout out of the source.
62
62
  */
63
63
  function bridgeParentDir() {
64
- const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
64
+ const sourceRoot = process.env.CODEX_BRIDGE_WORKER === "1" && process.env.CODEX_BRIDGE_SOURCE_ROOT;
65
+ const repoRoot = sourceRoot && path.isAbsolute(sourceRoot)
66
+ ? sourceRoot
67
+ : path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
65
68
  return path.dirname(repoRoot);
66
69
  }
67
70