@minhspark/codex-mcp-bridge 1.13.9 → 1.15.1

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"
@@ -394,26 +418,141 @@ async function readParentCommandLine(parentPid, platform) {
394
418
  return stdout.trim();
395
419
  }
396
420
 
421
+ export function nativeToolsPipeCandidatesFromWindowsSnapshot(snapshot, { parentPid, localAppData = process.env.LOCALAPPDATA } = {}) {
422
+ if (!Number.isSafeInteger(parentPid) || parentPid <= 0 || !Array.isArray(snapshot?.ancestors)
423
+ || snapshot.ancestors.length < 2 || snapshot.ancestors.length > 4 || !Array.isArray(snapshot.pipes)) return null;
424
+ const ancestors = snapshot.ancestors;
425
+ let expectedPid = parentPid;
426
+ const visited = new Set();
427
+ for (const ancestor of ancestors) {
428
+ if (!Number.isSafeInteger(ancestor?.pid) || ancestor.pid !== expectedPid || visited.has(ancestor.pid)
429
+ || typeof ancestor.executablePath !== "string" || /[\r\n\0]/.test(ancestor.executablePath)) return null;
430
+ visited.add(ancestor.pid);
431
+ expectedPid = ancestor.parentPid;
432
+ }
433
+ const desktop = ancestors.at(-1);
434
+ const appServer = ancestors.at(-2);
435
+ const desktopPath = path.win32.normalize(desktop.executablePath);
436
+ if (!/^[a-z]:\\Program Files\\WindowsApps\\OpenAI\.Codex_[0-9.]+_(?:x64|arm64)__2p2nqsd0c76g0\\app\\ChatGPT\.exe$/i.test(desktopPath)) return null;
437
+ if (ancestors.slice(0, -2).some((ancestor) => path.win32.basename(ancestor.executablePath).toLowerCase() !== "node.exe")) return null;
438
+ const serverPath = path.win32.normalize(appServer.executablePath);
439
+ const bundledServer = path.win32.join(path.win32.dirname(desktopPath), "resources", "codex.exe");
440
+ const downloadedRoot = localAppData ? path.win32.join(localAppData, "OpenAI", "Codex", "bin") : null;
441
+ const downloadedRelative = downloadedRoot ? path.win32.relative(downloadedRoot, serverPath) : "";
442
+ if (serverPath.toLowerCase() !== bundledServer.toLowerCase() && !/^[a-f0-9]{8,64}\\codex\.exe$/i.test(downloadedRelative)) return null;
443
+ if (typeof appServer.commandLine !== "string" || /[\r\n\0]/.test(appServer.commandLine)) return null;
444
+ const args = splitDesktopCommandLine(appServer.commandLine, "win32");
445
+ if (!args.length || path.win32.normalize(args[0]).toLowerCase() !== serverPath.toLowerCase()) return null;
446
+ let command = null;
447
+ for (let index = 1; index < args.length; index++) {
448
+ if (args[index] === "-c" || args[index] === "--config") index++;
449
+ else if (!args[index].startsWith("-")) { command = args[index]; break; }
450
+ }
451
+ if (command !== "app-server") return null;
452
+ const candidates = snapshot.pipes.filter((pipe) => pipe?.serverPid === desktop.pid
453
+ && typeof pipe.path === "string" && /^\\\\\.\\pipe\\codex-browser-use-[a-z0-9-]+$/i.test(pipe.path));
454
+ const paths = [...new Set(candidates.map((pipe) => pipe.path))].sort();
455
+ return paths;
456
+ }
457
+
458
+ async function readWindowsNativePipeSnapshot(parentPid) {
459
+ if (!Number.isSafeInteger(parentPid) || parentPid <= 0) return null;
460
+ const powershell = path.win32.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
461
+ const script = String.raw`
462
+ $ErrorActionPreference = 'Stop'
463
+ $ancestors = @()
464
+ $nextProcessId = ${parentPid}
465
+ for ($depth = 0; $depth -lt 4; $depth++) {
466
+ $ancestor = Get-CimInstance Win32_Process -Filter "ProcessId=$nextProcessId"
467
+ if (!$ancestor) { break }
468
+ $ancestors += @{ pid = [int]$ancestor.ProcessId; parentPid = [int]$ancestor.ParentProcessId; executablePath = $ancestor.ExecutablePath; commandLine = $ancestor.CommandLine }
469
+ if ($ancestor.Name -ieq 'ChatGPT.exe') { break }
470
+ if ($ancestor.Name -ine 'node.exe' -and $ancestor.Name -ine 'codex.exe') { break }
471
+ $nextProcessId = [int]$ancestor.ParentProcessId
472
+ }
473
+ $pipes = @()
474
+ if ($ancestors.Count -ge 2 -and $ancestor.Name -ieq 'ChatGPT.exe') {
475
+ Add-Type -TypeDefinition @'
476
+ using System;
477
+ using System.Runtime.InteropServices;
478
+ using Microsoft.Win32.SafeHandles;
479
+ public static class CodexNativePipeOwner {
480
+ [DllImport("kernel32.dll", SetLastError = true)]
481
+ public static extern bool GetNamedPipeServerProcessId(SafePipeHandle pipe, out uint serverProcessId);
482
+ }
483
+ '@
484
+ $names = @([System.IO.Directory]::GetFiles('\\.\pipe\') | Where-Object { $_ -match '^\\\\\.\\pipe\\codex-browser-use-[a-zA-Z0-9-]+$' } | Sort-Object | Select-Object -First 128)
485
+ foreach ($pipePath in $names) {
486
+ $client = $null
487
+ try {
488
+ $client = [System.IO.Pipes.NamedPipeClientStream]::new('.', $pipePath.Substring(9), [System.IO.Pipes.PipeDirection]::InOut, [System.IO.Pipes.PipeOptions]::Asynchronous)
489
+ $client.Connect(30)
490
+ $serverProcessId = [uint32]0
491
+ if ([CodexNativePipeOwner]::GetNamedPipeServerProcessId($client.SafePipeHandle, [ref]$serverProcessId) -and $serverProcessId -eq $ancestor.ProcessId) {
492
+ $pipes += @{ path = $pipePath; serverPid = [int]$serverProcessId }
493
+ }
494
+ } catch {} finally { if ($client) { $client.Dispose() } }
495
+ }
496
+ }
497
+ @{ ancestors = @($ancestors); pipes = @($pipes) } | ConvertTo-Json -Depth 4 -Compress
498
+ `;
499
+ const { stdout } = await execFileAsync(powershell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script], {
500
+ timeout: 5000, maxBuffer: 128 * 1024, windowsHide: true,
501
+ });
502
+ return stdout.trim() ? JSON.parse(stdout.trim()) : null;
503
+ }
504
+
505
+ async function probeWindowsNativeToolsPipe(socketPath, { env, timeoutMs }) {
506
+ const executorThreadId = resolveRelayThreadId(env).threadId;
507
+ const client = new NativeToolsClient({ env, socketPath, timeoutMs });
508
+ const timer = globalThis.setTimeout(() => client.close(), timeoutMs);
509
+ try {
510
+ const nativeResult = await client.dispatchDesktop({ executorThreadId, operation: "list_projects", arguments: {} });
511
+ return Array.isArray(decodeNativeToolResult(nativeResult)?.projects);
512
+ } finally {
513
+ globalThis.clearTimeout(timer);
514
+ client.close();
515
+ }
516
+ }
517
+
397
518
  export async function resolveNativeToolsPipePath({
398
519
  env = process.env,
399
520
  parentPid = process.ppid,
400
521
  platform = process.platform,
401
522
  readParentCommandLine: readParent = readParentCommandLine,
523
+ readWindowsSnapshot = readWindowsNativePipeSnapshot,
524
+ probeWindowsPipe = probeWindowsNativeToolsPipe,
525
+ windowsDiscoveryTimeoutMs = 7000,
402
526
  } = {}) {
403
527
  if (env.CODEX_APP_TOOLS_PIPE_PATH) return env.CODEX_APP_TOOLS_PIPE_PATH;
404
528
  try {
405
- return nativeToolsPipeFromCommandLine(await readParent(parentPid, platform), { platform });
406
- } catch {
529
+ const inherited = nativeToolsPipeFromCommandLine(await readParent(parentPid, platform), { platform });
530
+ if (inherited) return inherited;
531
+ } catch {}
532
+ if (platform !== "win32") return null;
533
+ const deadline = Date.now() + Math.max(1, Math.min(7000, Number(windowsDiscoveryTimeoutMs) || 7000));
534
+ try {
535
+ const candidates = nativeToolsPipeCandidatesFromWindowsSnapshot(await readWindowsSnapshot(parentPid), { parentPid, localAppData: env.LOCALAPPDATA }) ?? [];
536
+ for (const candidate of candidates) {
537
+ const remaining = deadline - Date.now();
538
+ if (remaining <= 0) break;
539
+ try {
540
+ if (await probeWindowsPipe(candidate, { env, timeoutMs: Math.min(750, remaining) })) return candidate;
541
+ } catch {}
542
+ }
407
543
  return null;
408
- }
544
+ } catch { return null; }
409
545
  }
410
546
 
411
547
  export class NativeToolsClient {
412
- constructor({ env = process.env, socketPath = env.CODEX_APP_TOOLS_PIPE_PATH, timeoutMs = DEFAULT_TIMEOUT_MS, resolveSocketPath = () => resolveNativeToolsPipePath({ env }) } = {}) {
548
+ constructor({ env = process.env, socketPath = env.CODEX_APP_TOOLS_PIPE_PATH, timeoutMs = DEFAULT_TIMEOUT_MS, resolveSocketPath = () => resolveNativeToolsPipePath({ env }), assertAccount = assertAccountIdentity } = {}) {
413
549
  this.env = env;
414
550
  this.socketPath = socketPath;
551
+ this.explicitSocketPath = socketPath || null;
552
+ this.hasDiscoveredSocket = false;
415
553
  this.timeoutMs = timeoutMs;
416
554
  this.resolveSocketPath = resolveSocketPath;
555
+ this.assertAccount = assertAccount;
417
556
  this.socket = null;
418
557
  this.connectingSocket = null;
419
558
  this.connecting = null;
@@ -432,6 +571,7 @@ export class NativeToolsClient {
432
571
  throw new NativeRelayError("Native tools client closed while discovering the Desktop pipe", "NATIVE_PIPE_UNAVAILABLE");
433
572
  }
434
573
  this.socketPath = socketPath;
574
+ if (socketPath && !this.explicitSocketPath) this.hasDiscoveredSocket = true;
435
575
  if (!this.socketPath) {
436
576
  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
577
  }
@@ -440,14 +580,17 @@ export class NativeToolsClient {
440
580
  this.connectingSocket = socket;
441
581
  let buffer = Buffer.alloc(0);
442
582
  let connected = false;
583
+ let failed = false;
443
584
  const timer = globalThis.setTimeout(() => {
444
- reject(new NativeRelayError("Timed out connecting to the Codex Desktop native tools pipe", "NATIVE_PIPE_UNAVAILABLE"));
445
- socket.destroy();
585
+ fail(new NativeRelayError("Timed out connecting to the Codex Desktop native tools pipe", "NATIVE_PIPE_UNAVAILABLE"));
446
586
  }, this.timeoutMs);
447
587
  const fail = (err) => {
588
+ if (failed) return;
589
+ failed = true;
448
590
  globalThis.clearTimeout(timer);
449
591
  if (!connected) reject(err);
450
592
  if (this.socket === socket) this.socket = null;
593
+ if (!this.explicitSocketPath && this.connectionGeneration === generation) this.socketPath = null;
451
594
  for (const pending of this.pending.values()) {
452
595
  if (pending.socket === socket) pending.reject(err);
453
596
  }
@@ -503,15 +646,17 @@ export class NativeToolsClient {
503
646
  }
504
647
  }
505
648
 
506
- async dispatch(args) {
507
- return this.#request(nativeDispatchParams(args));
649
+ async dispatch(args, options) {
650
+ return this.#request(nativeDispatchParams(args), options);
508
651
  }
509
652
 
510
- async dispatchDesktop(args) {
511
- return this.#request(nativeDesktopOperationParams(args));
653
+ async dispatchDesktop(args, options) {
654
+ return this.#request(nativeDesktopOperationParams(args), options);
512
655
  }
513
656
 
514
- async #request(params) {
657
+ async #request(params, { beforeSend, accountContext } = {}) {
658
+ validateBeforeSend(beforeSend);
659
+ const accounts = relayAccountContext(accountContext);
515
660
  const id = this.nextId++;
516
661
  const payload = Buffer.from(JSON.stringify({
517
662
  jsonrpc: "2.0",
@@ -522,31 +667,50 @@ export class NativeToolsClient {
522
667
  if (payload.length > MAX_FRAME_BYTES) {
523
668
  throw new NativeRelayError("Native dispatch exceeds the frame limit", "RELAY_MESSAGE_TOO_LARGE");
524
669
  }
525
- await this.connect();
670
+ try {
671
+ await this.connect();
672
+ } catch (error) {
673
+ throw beforeWriteError(error);
674
+ }
526
675
  const header = Buffer.alloc(4);
527
676
  header.writeUInt32LE(payload.length);
528
677
  return new Promise((resolve, reject) => {
678
+ const socket = this.socket;
679
+ let dispatched = false;
529
680
  const finish = (fn, value) => {
530
681
  if (!this.pending.delete(id)) return;
531
682
  globalThis.clearTimeout(timer);
532
683
  fn(value);
533
684
  };
534
685
  const timer = globalThis.setTimeout(() => {
535
- finish(reject, new NativeRelayError("Codex Desktop native dispatch timed out; delivery may have occurred", "NATIVE_DELIVERY_UNCONFIRMED"));
686
+ finish(reject, dispatched
687
+ ? new NativeRelayError("Codex Desktop native dispatch timed out; delivery may have occurred", "NATIVE_DELIVERY_UNCONFIRMED")
688
+ : beforeWriteError(new NativeRelayError("Codex Desktop native send deadline elapsed before dispatch", "NATIVE_PIPE_UNAVAILABLE")));
536
689
  }, this.timeoutMs);
537
690
  this.pending.set(id, {
538
- socket: this.socket,
691
+ socket,
539
692
  resolve: (value) => finish(resolve, value),
540
- reject: (err) => finish(reject, err),
693
+ reject: (err) => finish(reject, dispatched ? err : beforeWriteError(err)),
541
694
  });
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
- }
695
+ void (async () => {
696
+ try {
697
+ const checked = beforeSend?.();
698
+ if (checked && typeof checked.then === "function") await checked;
699
+ if (!this.pending.has(id)) return;
700
+ if (accounts) {
701
+ const verified = this.assertAccount(accounts);
702
+ if (verified && typeof verified.then === "function") await verified;
703
+ }
704
+ if (!this.pending.has(id)) return;
705
+ if (!socket || socket.destroyed || this.socket !== socket) throw new NativeRelayError("Native tools pipe closed before dispatch", "NATIVE_PIPE_UNAVAILABLE");
706
+ dispatched = true;
707
+ socket.write(Buffer.concat([header, payload]), (err) => {
708
+ if (err) finish(reject, new NativeRelayError(`Native dispatch write failed: ${err.message}`, "NATIVE_DELIVERY_UNCONFIRMED"));
709
+ });
710
+ } catch (err) {
711
+ finish(reject, dispatched ? new NativeRelayError(`Native dispatch write failed: ${err.message}`, "NATIVE_DELIVERY_UNCONFIRMED") : beforeWriteError(err));
712
+ }
713
+ })();
550
714
  });
551
715
  }
552
716
 
@@ -554,6 +718,7 @@ export class NativeToolsClient {
554
718
  this.connectionGeneration++;
555
719
  const socket = this.socket;
556
720
  this.socket = null;
721
+ if (!this.explicitSocketPath) this.socketPath = null;
557
722
  for (const pending of this.pending.values()) {
558
723
  pending.reject(new NativeRelayError("Native tools client closed before confirming delivery", "NATIVE_DELIVERY_UNCONFIRMED"));
559
724
  }
@@ -603,9 +768,10 @@ export function nativeRelayStatus(env = process.env) {
603
768
  * socket cannot go stale while Codex Desktop restarts underneath it.
604
769
  */
605
770
  export class NativeDesktopRelay {
606
- constructor({ env = process.env, socketPath = null, timeoutMs = DEFAULT_TIMEOUT_MS, log = () => {} } = {}) {
771
+ constructor({ env = process.env, socketPath = null, accountSocketPath = null, timeoutMs = DEFAULT_TIMEOUT_MS, log = () => {} } = {}) {
607
772
  this.env = env;
608
773
  this.explicitSocketPath = socketPath;
774
+ this.accountSocketPath = accountSocketPath ?? socketPath ?? accountRelaySocketPath(env);
609
775
  this.timeoutMs = timeoutMs;
610
776
  this.log = log;
611
777
  }
@@ -614,27 +780,31 @@ export class NativeDesktopRelay {
614
780
  return this.explicitSocketPath ?? relaySocketPath(this.env);
615
781
  }
616
782
 
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 };
783
+ status({ accountContext } = {}) {
784
+ const socketPath = accountContext ? this.accountSocketPath : this.explicitSocketPath;
785
+ if (!socketPath) return nativeRelayStatus(this.env);
786
+ const status = nativeRelayStatus({ ...this.env, CODEX_NATIVE_RELAY_SOCKET: socketPath });
787
+ return { ...status, socketPath };
621
788
  }
622
789
 
623
790
  get available() {
624
791
  return this.status().enabled;
625
792
  }
626
793
 
627
- async sendMessage(targetThreadId, message, { timeoutMs = this.timeoutMs } = {}) {
628
- const request = { v: RELAY_PROTOCOL_VERSION, targetThreadId, message };
629
- return this.#request(request, timeoutMs);
794
+ async sendMessage(targetThreadId, message, { timeoutMs = this.timeoutMs, beforeSend, accountContext } = {}) {
795
+ const accounts = relayAccountContext(accountContext);
796
+ const request = { v: accounts ? ACCOUNT_RELAY_PROTOCOL_VERSION : RELAY_PROTOCOL_VERSION, targetThreadId, message, ...(accounts ? { accountContext: accounts } : {}) };
797
+ return this.#request(request, timeoutMs, beforeSend);
630
798
  }
631
799
 
632
- async requestDesktop(operation, args, { timeoutMs = this.timeoutMs } = {}) {
800
+ async requestDesktop(operation, args, { timeoutMs = this.timeoutMs, beforeSend, accountContext } = {}) {
633
801
  validateDesktopOperation(operation, args);
634
- return this.#request({ v: RELAY_PROTOCOL_VERSION, operation, arguments: args }, timeoutMs);
802
+ const accounts = relayAccountContext(accountContext);
803
+ return this.#request({ v: accounts ? ACCOUNT_RELAY_PROTOCOL_VERSION : RELAY_PROTOCOL_VERSION, operation, arguments: args, ...(accounts ? { accountContext: accounts } : {}) }, timeoutMs, beforeSend);
635
804
  }
636
805
 
637
- async #request(request, timeoutMs) {
806
+ async #request(request, timeoutMs, beforeSend) {
807
+ validateBeforeSend(beforeSend);
638
808
  const line = `${JSON.stringify(request)}\n`;
639
809
  if (Buffer.byteLength(line, "utf8") > MAX_FRAME_BYTES) {
640
810
  throw new NativeRelayError(
@@ -643,20 +813,21 @@ export class NativeDesktopRelay {
643
813
  );
644
814
  }
645
815
 
646
- const response = await this.#roundTrip(line, timeoutMs);
647
- if (response?.ok === true && response.v === RELAY_PROTOCOL_VERSION) {
816
+ const response = await this.#roundTrip(line, timeoutMs, beforeSend, request.accountContext ? this.accountSocketPath : this.socketPath);
817
+ if (response?.ok === true && response.v === request.v) {
648
818
  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
819
  return response;
650
820
  }
651
- throw new NativeRelayError(
821
+ const error = new NativeRelayError(
652
822
  response?.error?.message ?? "the Codex Desktop relay refused the message",
653
823
  response?.error?.code ?? "NATIVE_DISPATCH_FAILED",
654
- { reachedCompanion: true },
824
+ { reachedCompanion: response?.error?.sent !== false },
655
825
  );
826
+ if (response?.error?.sent === false) error.sent = false;
827
+ throw error;
656
828
  }
657
829
 
658
- #roundTrip(line, timeoutMs) {
659
- const socketPath = this.socketPath;
830
+ #roundTrip(line, timeoutMs, beforeSend, socketPath) {
660
831
  return new Promise((resolve, reject) => {
661
832
  let buffer = Buffer.alloc(0);
662
833
  let settled = false;
@@ -675,18 +846,29 @@ export class NativeDesktopRelay {
675
846
  () =>
676
847
  finish(
677
848
  reject,
678
- new NativeRelayError(
849
+ dispatched ? new NativeRelayError(
679
850
  `The Codex Desktop relay did not answer within ${timeoutMs}ms`,
680
851
  "RELAY_TIMEOUT",
681
- { reachedCompanion: dispatched },
682
- ),
852
+ { reachedCompanion: true },
853
+ ) : beforeWriteError(new NativeRelayError("The Codex Desktop relay send deadline elapsed before dispatch", "RELAY_UNREACHABLE")),
683
854
  ),
684
855
  timeoutMs,
685
856
  );
686
857
 
687
858
  socket.on("connect", () => {
688
- dispatched = true;
689
- socket.write(line);
859
+ if (settled) return;
860
+ void (async () => {
861
+ try {
862
+ const checked = beforeSend?.();
863
+ if (checked && typeof checked.then === "function") await checked;
864
+ if (settled) return;
865
+ if (socket.destroyed) throw new NativeRelayError("The relay socket closed before dispatch", "RELAY_UNREACHABLE");
866
+ dispatched = true;
867
+ socket.write(line);
868
+ } catch (error) {
869
+ finish(reject, dispatched ? new NativeRelayError(`Relay write failed: ${error.message}`, "RELAY_DELIVERY_UNCONFIRMED", { reachedCompanion: true }) : beforeWriteError(error));
870
+ }
871
+ })();
690
872
  });
691
873
  socket.on("data", (chunk) => {
692
874
  buffer = Buffer.concat([buffer, chunk]);
@@ -717,17 +899,19 @@ export class NativeDesktopRelay {
717
899
  socket.on("error", (err) =>
718
900
  finish(
719
901
  reject,
720
- new NativeRelayError(`Codex Desktop relay connection failed at ${socketPath}: ${err.message}`, dispatched ? "RELAY_DELIVERY_UNCONFIRMED" : "RELAY_UNREACHABLE", { reachedCompanion: dispatched }),
902
+ dispatched
903
+ ? new NativeRelayError(`Codex Desktop relay connection failed at ${socketPath}: ${err.message}`, "RELAY_DELIVERY_UNCONFIRMED", { reachedCompanion: true })
904
+ : beforeWriteError(new NativeRelayError(`Codex Desktop relay connection failed at ${socketPath}: ${err.message}`, "RELAY_UNREACHABLE")),
721
905
  ),
722
906
  );
723
907
  socket.on("close", () =>
724
908
  finish(
725
909
  reject,
726
- new NativeRelayError(
910
+ dispatched ? new NativeRelayError(
727
911
  `The Codex Desktop relay at ${socketPath} closed before answering`,
728
- dispatched ? "RELAY_DELIVERY_UNCONFIRMED" : "RELAY_UNREACHABLE",
729
- { reachedCompanion: dispatched },
730
- ),
912
+ "RELAY_DELIVERY_UNCONFIRMED",
913
+ { reachedCompanion: true },
914
+ ) : beforeWriteError(new NativeRelayError(`The Codex Desktop relay at ${socketPath} closed before dispatch`, "RELAY_UNREACHABLE")),
731
915
  ),
732
916
  );
733
917
  });