agent-relay-codex 0.4.24 → 0.4.25

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/README.md CHANGED
@@ -25,6 +25,9 @@ bunx agent-relay-server@latest
25
25
 
26
26
  # start Codex with Agent Relay (after restarting your shell)
27
27
  codex-relay
28
+
29
+ # start a relay-only Codex agent without opening a TUI
30
+ codex-relay --headless
28
31
  ```
29
32
 
30
33
  Without restarting your shell:
@@ -76,6 +79,7 @@ Run with `AGENT_RELAY_PROFILE=frontend-developer agent-relay-codex start`.
76
79
  | `CODEX_THREAD_MODE` | `start` | Thread attach: `start`, `resume`, `auto` |
77
80
  | `CODEX_THREAD_ID` | — | Pin to a specific thread |
78
81
  | `CODEX_APP_SERVER_URL` | `ws://127.0.0.1:4501` | App-server WebSocket URL |
82
+ | `AGENT_RELAY_CODEX_HEADLESS` | — | Set to `1` to run without opening a TUI |
79
83
 
80
84
  ### Advanced tuning
81
85
 
@@ -87,7 +91,6 @@ Run with `AGENT_RELAY_PROFILE=frontend-developer agent-relay-codex start`.
87
91
  | `AGENT_RELAY_CODEX_COALESCE_WINDOW_MS` | `600` | Message batch window |
88
92
  | `AGENT_RELAY_CODEX_RELAY_BACKOFF_INITIAL_MS` | `2000` | Relay API retry backoff |
89
93
  | `AGENT_RELAY_CODEX_RELAY_BACKOFF_MAX_MS` | `60000` | Relay API retry backoff cap |
90
- | `AGENT_RELAY_CODEX_HOOK_TIMEOUT_MS` | `5000` | SessionStart handshake timeout |
91
94
 
92
95
  ## Approval mode
93
96
 
@@ -123,7 +126,13 @@ agent-relay-codex uninstall --purge # also remove runtime state and PATH entrie
123
126
 
124
127
  ## How it works
125
128
 
126
- `codex-relay` launches `codex app-server`, opens Codex through `codex --remote`, and spawns a live sidecar that:
129
+ `codex-relay` launches `codex app-server`, starts the live sidecar as the long-lived App Server client, waits for it to resolve one canonical Codex thread, then opens the TUI with `codex resume <thread-id> --remote <app-server-url>`. Agent Relay messages and direct TUI messages therefore share the same Codex context.
130
+
131
+ Use `codex-relay --headless` on servers where Agent Relay is the only interaction surface. It starts the same app-server and sidecar session but does not open a TUI; the launcher prints the attach command if you later want to connect one.
132
+
133
+ The installed SessionStart hook remains for plain `codex` launches. In that mode, the hook registers the visible Codex session with Agent Relay without requiring the managed `codex-relay` launcher.
134
+
135
+ The live sidecar:
127
136
 
128
137
  - Registers the session as an Agent Relay agent
129
138
  - Polls the relay inbox and delivers messages into the active Codex thread
@@ -16,24 +16,6 @@ type RelayStats = {
16
16
  version?: string;
17
17
  };
18
18
 
19
- type HookHandshake = {
20
- status: "ok" | "error";
21
- code: string;
22
- message?: string;
23
- pid?: number;
24
- threadId?: string;
25
- timestamp?: string;
26
- };
27
-
28
- type HookWaitResult = {
29
- ok: boolean;
30
- code: string;
31
- message?: string;
32
- pid?: number;
33
- };
34
-
35
- const DEFAULT_HOOK_HANDSHAKE_TIMEOUT_MS = 5000;
36
-
37
19
  const __dirname = dirname(fileURLToPath(import.meta.url));
38
20
  const packageRoot = resolve(__dirname, "..");
39
21
  const home = process.env.HOME || process.env.USERPROFILE || homedir();
@@ -55,14 +37,14 @@ function usage(exitCode = 0): never {
55
37
  console.log(`agent-relay-codex
56
38
 
57
39
  Usage:
58
- agent-relay-codex [--relay-url URL] [--listen ws://127.0.0.1:PORT] [-- <codex args...>]
40
+ agent-relay-codex [--headless] [--relay-url URL] [--listen ws://127.0.0.1:PORT] [-- <codex args...>]
59
41
  agent-relay-codex install [--alias|--no-alias]
60
42
  agent-relay-codex uninstall [--purge]
61
43
  agent-relay-codex alias install
62
44
  agent-relay-codex alias remove
63
45
  agent-relay-codex doctor
64
- agent-relay-codex start [--relay-url URL] [--listen ws://127.0.0.1:PORT] [--thread-mode auto|resume|start] [-- <codex args...>]
65
- codex-relay [--relay-url URL] [--listen ws://127.0.0.1:PORT] [--thread-mode auto|resume|start] [-- <codex args...>]
46
+ agent-relay-codex start [--headless] [--relay-url URL] [--listen ws://127.0.0.1:PORT] [--thread-mode auto|resume|start] [--thread-id ID] [-- <codex args...>]
47
+ codex-relay [--headless] [--relay-url URL] [--listen ws://127.0.0.1:PORT] [--thread-mode auto|resume|start] [--thread-id ID] [-- <codex args...>]
66
48
 
67
49
  With no subcommand, this launches Codex with live Agent Relay support.`);
68
50
  process.exit(exitCode);
@@ -99,21 +81,14 @@ function readJsonFile<T>(path: string, fallback: T): T {
99
81
  return JSON.parse(readFileSync(path, "utf8")) as T;
100
82
  }
101
83
 
102
- function errorMessage(error: unknown): string {
103
- return error instanceof Error ? error.message : String(error);
104
- }
105
-
106
- function envPositiveInt(name: string, fallback: number): number {
107
- const raw = process.env[name];
108
- if (!raw) return fallback;
109
- const parsed = Number.parseInt(raw, 10);
110
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
111
- }
112
-
113
84
  function appendLauncherLog(runDir: string, line: string): void {
114
85
  appendFileSync(join(runDir, "launcher.log"), `${new Date().toISOString()} ${line}\n`);
115
86
  }
116
87
 
88
+ function sanitizeSessionKey(value: string): string {
89
+ return value.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 96) || "thread";
90
+ }
91
+
117
92
  function isAgentRelaySessionStartCommand(command: string): boolean {
118
93
  return /agent-relay.*hooks\/session-start\.ts/.test(command);
119
94
  }
@@ -424,67 +399,43 @@ async function waitForPort(url: string, child: ReturnType<typeof Bun.spawn>): Pr
424
399
  throw new Error(`timed out waiting for ${url}`);
425
400
  }
426
401
 
427
- async function waitForHookHandshake(runDir: string, timeoutMs: number): Promise<HookWaitResult> {
428
- const handshakePath = join(runDir, "session-start-handshake.json");
429
- const deadline = Date.now() + timeoutMs;
430
-
431
- while (Date.now() < deadline) {
432
- if (existsSync(handshakePath)) {
433
- try {
434
- const parsed = JSON.parse(readFileSync(handshakePath, "utf8")) as HookHandshake;
435
- const code = typeof parsed.code === "string" && parsed.code.trim() ? parsed.code.trim() : "HOOK_HANDSHAKE_INVALID";
436
- const message = typeof parsed.message === "string" ? parsed.message : undefined;
437
- const pid = typeof parsed.pid === "number" && Number.isFinite(parsed.pid) && parsed.pid > 0
438
- ? parsed.pid
439
- : undefined;
440
-
441
- if (parsed.status === "ok") return { ok: true, code, message, pid };
442
- if (parsed.status === "error") return { ok: false, code, message, pid };
443
- return { ok: false, code: "HOOK_HANDSHAKE_INVALID", message: `unexpected status ${String((parsed as any).status)}` };
444
- } catch (error) {
445
- return { ok: false, code: "HOOK_HANDSHAKE_INVALID", message: errorMessage(error) };
446
- }
447
- }
448
- await Bun.sleep(100);
449
- }
450
-
451
- return {
452
- ok: false,
453
- code: "HOOK_HANDSHAKE_TIMEOUT",
454
- message: `no hook handshake observed within ${timeoutMs}ms`,
455
- };
456
- }
402
+ type SessionPermissions = {
403
+ approvalPolicy?: string;
404
+ sandbox?: string;
405
+ };
457
406
 
458
- function spawnFallbackSidecar(runDir: string, env: Record<string, string | undefined>): number {
459
- const autoDir = join(runDir, "auto");
460
- mkdirSync(autoDir, { recursive: true });
407
+ function spawnManagedSidecar(params: {
408
+ runDir: string;
409
+ env: Record<string, string | undefined>;
410
+ sessionDir: string;
411
+ statePath: string;
412
+ threadMode: string;
413
+ threadId?: string;
414
+ }): number {
415
+ const { runDir, env, sessionDir, statePath, threadMode, threadId } = params;
416
+ mkdirSync(sessionDir, { recursive: true });
461
417
 
462
418
  const sidecarEnv: Record<string, string | undefined> = {
463
419
  ...env,
464
- CODEX_THREAD_MODE: env.AGENT_RELAY_CODEX_FALLBACK_THREAD_MODE || env.CODEX_THREAD_MODE || "start",
420
+ CODEX_THREAD_MODE: threadMode,
421
+ CODEX_THREAD_ID: threadId || undefined,
465
422
  AGENT_RELAY_CODEX_CWD: process.cwd(),
466
- AGENT_RELAY_CODEX_STATE_PATH: join(autoDir, "live-state.json"),
423
+ AGENT_RELAY_CODEX_STATE_PATH: statePath,
467
424
  };
468
- delete sidecarEnv.CODEX_THREAD_ID;
469
425
 
470
426
  const sidecar = Bun.spawn(["bun", "run", join(activePackageRoot(), "live-sidecar.ts")], {
471
427
  env: sidecarEnv,
472
- stdout: Bun.file(join(autoDir, "sidecar.log")),
473
- stderr: Bun.file(join(autoDir, "sidecar.log")),
428
+ stdout: Bun.file(join(sessionDir, "sidecar.log")),
429
+ stderr: Bun.file(join(sessionDir, "sidecar.log")),
474
430
  });
475
431
  sidecar.unref();
476
432
 
477
- writeFileSync(join(autoDir, "sidecar.pid"), String(sidecar.pid));
433
+ writeFileSync(join(sessionDir, "sidecar.pid"), String(sidecar.pid));
478
434
  appendFileSync(join(runDir, "sidecar-pids.txt"), `${sidecar.pid}\n`);
479
435
 
480
436
  return sidecar.pid;
481
437
  }
482
438
 
483
- type SessionPermissions = {
484
- approvalPolicy?: string;
485
- sandbox?: string;
486
- };
487
-
488
439
  function hasCodexPermissionMode(codexArgs: string[]): boolean {
489
440
  for (const arg of codexArgs) {
490
441
  if (
@@ -549,6 +500,42 @@ function resolveSessionPermissions(codexArgs: string[]): SessionPermissions {
549
500
  return { approvalPolicy, sandbox };
550
501
  }
551
502
 
503
+ function codexModelFromArgs(codexArgs: string[]): string | undefined {
504
+ for (let index = 0; index < codexArgs.length; index += 1) {
505
+ const arg = codexArgs[index]!;
506
+ if (arg === "--model" || arg === "-m") {
507
+ const next = codexArgs[index + 1];
508
+ if (next && !next.startsWith("-")) return next;
509
+ continue;
510
+ }
511
+ if (arg.startsWith("--model=")) return arg.slice("--model=".length);
512
+ }
513
+ return undefined;
514
+ }
515
+
516
+ async function waitForManagedAgent(statePath: string, sidecarPid: number, timeoutMs = 30000): Promise<{ threadId: string; agentId: string }> {
517
+ const deadline = Date.now() + timeoutMs;
518
+ let threadId = "";
519
+ while (Date.now() < deadline) {
520
+ if (!isAlive(sidecarPid)) {
521
+ throw new Error(`managed sidecar exited before registering with Agent Relay; inspect ${dirname(statePath)}/sidecar.log`);
522
+ }
523
+ if (existsSync(statePath)) {
524
+ try {
525
+ const parsed = JSON.parse(readFileSync(statePath, "utf8")) as { threadId?: unknown; agentId?: unknown };
526
+ if (typeof parsed.threadId === "string" && parsed.threadId.trim()) threadId = parsed.threadId.trim();
527
+ if (threadId && typeof parsed.agentId === "string" && parsed.agentId.trim()) {
528
+ return { threadId, agentId: parsed.agentId.trim() };
529
+ }
530
+ } catch {
531
+ // The sidecar may be writing the state file; retry until timeout.
532
+ }
533
+ }
534
+ await Bun.sleep(100);
535
+ }
536
+ throw new Error(`timed out waiting for managed sidecar to register with Agent Relay; inspect ${dirname(statePath)}/sidecar.log`);
537
+ }
538
+
552
539
  function cleanupRun(runDir: string, appServer: ReturnType<typeof Bun.spawn> | null): void {
553
540
  if (existsSync(runDir)) {
554
541
  const pidsPath = join(runDir, "sidecar-pids.txt");
@@ -792,6 +779,8 @@ async function start(args: string[]): Promise<void> {
792
779
  let relayUrl = process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850";
793
780
  let listenUrl = process.env.CODEX_APP_SERVER_URL || "";
794
781
  let threadMode = process.env.CODEX_THREAD_MODE || "start";
782
+ let headless = process.env.AGENT_RELAY_CODEX_HEADLESS === "1";
783
+ let threadId = process.env.CODEX_THREAD_ID || "";
795
784
  const cwd = process.cwd();
796
785
  const rig = process.env.AGENT_RELAY_CODEX_RIG || "codex-live";
797
786
  const project = cwd.split("/").filter(Boolean).at(-1) || "unknown";
@@ -814,6 +803,14 @@ async function start(args: string[]): Promise<void> {
814
803
  listenUrl = args[++index] || listenUrl;
815
804
  continue;
816
805
  }
806
+ if (arg === "--headless" || arg === "--no-tui") {
807
+ headless = true;
808
+ continue;
809
+ }
810
+ if (arg === "--thread-id") {
811
+ threadId = args[++index] || threadId;
812
+ continue;
813
+ }
817
814
  if (arg === "--thread-mode") {
818
815
  threadMode = args[++index] || threadMode;
819
816
  if (!["auto", "resume", "start"].includes(threadMode)) {
@@ -823,6 +820,7 @@ async function start(args: string[]): Promise<void> {
823
820
  }
824
821
  codexArgs.push(arg);
825
822
  }
823
+ if (!["auto", "resume", "start"].includes(threadMode)) threadMode = "start";
826
824
 
827
825
  if (!listenUrl) listenUrl = await pickLoopbackUrl();
828
826
  if (!hasCodexPermissionMode(codexArgs)) {
@@ -830,6 +828,7 @@ async function start(args: string[]): Promise<void> {
830
828
  }
831
829
  const permissions = resolveSessionPermissions(codexArgs);
832
830
  const effectiveApprovalMode = approvalModeFromPermissions(permissions);
831
+ const model = process.env.CODEX_MODEL || codexModelFromArgs(codexArgs);
833
832
  if (hasApprovalEnv && effectiveApprovalMode !== requestedApprovalMode) {
834
833
  throw new Error(
835
834
  `Codex permission flags resolve to AGENT_RELAY_APPROVAL=${effectiveApprovalMode}, but AGENT_RELAY_APPROVAL=${requestedApprovalMode} was requested.`,
@@ -849,9 +848,13 @@ async function start(args: string[]): Promise<void> {
849
848
  AGENT_RELAY_CODEX_RUNTIME_DIR: runDir,
850
849
  CODEX_APP_SERVER_URL: listenUrl,
851
850
  CODEX_THREAD_MODE: threadMode,
851
+ CODEX_THREAD_ID: threadId || undefined,
852
+ CODEX_MODEL: model || undefined,
852
853
  AGENT_RELAY_CODEX_APPROVAL_POLICY: permissions.approvalPolicy,
853
854
  AGENT_RELAY_CODEX_SANDBOX: permissions.sandbox,
854
855
  AGENT_RELAY_APPROVAL: effectiveApprovalMode,
856
+ AGENT_RELAY_CODEX_MANAGED: "1",
857
+ AGENT_RELAY_CODEX_PARENT_PID: String(process.pid),
855
858
  };
856
859
 
857
860
  const appLog = Bun.file(join(runDir, "app-server.log"));
@@ -874,51 +877,46 @@ async function start(args: string[]): Promise<void> {
874
877
  process.once("exit", shutdown);
875
878
 
876
879
  await waitForPort(listenUrl, appServer);
880
+ const initialSessionKey = threadId ? sanitizeSessionKey(threadId) : "managed";
881
+ const sidecarDir = join(runDir, initialSessionKey);
882
+ const statePath = join(sidecarDir, "live-state.json");
883
+ const sidecarPid = spawnManagedSidecar({
884
+ runDir,
885
+ env,
886
+ sessionDir: sidecarDir,
887
+ statePath,
888
+ threadMode,
889
+ threadId: threadId || undefined,
890
+ });
891
+ const managedAgent = await waitForManagedAgent(statePath, sidecarPid);
892
+ const canonicalThreadId = managedAgent.threadId;
893
+ const managedEnv = {
894
+ ...env,
895
+ CODEX_THREAD_MODE: "resume",
896
+ CODEX_THREAD_ID: canonicalThreadId,
897
+ };
898
+
877
899
  console.log(`Agent Relay Codex session: ${listenUrl}`);
900
+ console.log(`Thread: ${canonicalThreadId}`);
901
+ console.log(`Agent: ${managedAgent.agentId}`);
878
902
  console.log(`Runtime: ${runDir}`);
903
+ appendLauncherLog(runDir, `MANAGED_THREAD thread=${canonicalThreadId} agent=${managedAgent.agentId} sidecarPid=${sidecarPid} headless=${headless}`);
879
904
 
880
- const codex = Bun.spawn([codexBinary, "--remote", listenUrl, ...codexArgs], {
881
- env,
905
+ if (headless) {
906
+ console.log(`Headless relay sidecar pid: ${sidecarPid}`);
907
+ console.log(`Attach TUI with: ${codexBinary} resume ${canonicalThreadId} --remote ${listenUrl}`);
908
+ const exitCode = await appServer.exited;
909
+ shutdown();
910
+ process.exit(exitCode);
911
+ }
912
+
913
+ const codex = Bun.spawn([codexBinary, "resume", canonicalThreadId, "--remote", listenUrl, ...codexArgs], {
914
+ env: managedEnv,
882
915
  stdin: "inherit",
883
916
  stdout: "inherit",
884
917
  stderr: "inherit",
885
918
  });
886
919
 
887
- const hookTimeoutMs = envPositiveInt("AGENT_RELAY_CODEX_HOOK_TIMEOUT_MS", DEFAULT_HOOK_HANDSHAKE_TIMEOUT_MS);
888
- const handshake = await waitForHookHandshake(runDir, hookTimeoutMs);
889
- let fallbackReason: HookWaitResult | null = null;
890
-
891
- if (!handshake.ok) {
892
- fallbackReason = handshake;
893
- } else if (handshake.pid === undefined) {
894
- fallbackReason = {
895
- ok: false,
896
- code: "HOOK_HANDSHAKE_NO_PID",
897
- message: "hook reported success without a sidecar pid",
898
- };
899
- } else if (!isAlive(handshake.pid)) {
900
- fallbackReason = {
901
- ok: false,
902
- code: "HOOK_HANDSHAKE_PID_NOT_ALIVE",
903
- message: `hook reported pid ${handshake.pid} but it is not running`,
904
- };
905
- } else {
906
- appendLauncherLog(runDir, `HOOK_HANDSHAKE_OK code=${handshake.code} pid=${handshake.pid}`);
907
- }
908
-
909
- if (fallbackReason && codex.exitCode === null) {
910
- const pid = spawnFallbackSidecar(runDir, env);
911
- appendFileSync(
912
- join(runDir, "launcher.log"),
913
- `${new Date().toISOString()} HOOK_FALLBACK_STARTED reason=${fallbackReason.code} fallbackPid=${pid}${fallbackReason.message ? ` detail=${fallbackReason.message}` : ""}\n`,
914
- );
915
- } else if (fallbackReason) {
916
- appendLauncherLog(
917
- runDir,
918
- `HOOK_FALLBACK_SKIPPED reason=${fallbackReason.code} codexExit=${codex.exitCode}${fallbackReason.message ? ` detail=${fallbackReason.message}` : ""}`,
919
- );
920
- }
921
-
922
920
  const exitCode = await codex.exited;
923
921
  shutdown();
924
922
  process.exit(exitCode);
@@ -74,6 +74,11 @@ const project = cwd.split("/").filter(Boolean).at(-1) || "unknown";
74
74
  const profile = loadAgentRelayProfile(process.env, { provider: "codex", rig, project });
75
75
  const approvalMode = profile.approval ?? parseApprovalMode(process.env.AGENT_RELAY_APPROVAL);
76
76
 
77
+ if (process.env.AGENT_RELAY_CODEX_MANAGED === "1") {
78
+ outputContext("Agent Relay is managed by codex-relay for this session; the launcher attached Relay and TUI to the same Codex App Server thread.");
79
+ process.exit(0);
80
+ }
81
+
77
82
  if (!appServerUrl || !runId) {
78
83
  outputContext(
79
84
  "Agent Relay for Codex is installed. For live incoming relay messages, start Codex with `agent-relay-codex start` so a managed app-server and sidecar can attach to this session.",
@@ -129,6 +134,7 @@ const spawnEnv: Record<string, string | undefined> = {
129
134
  AGENT_RELAY_CODEX_STATE_PATH: statePath,
130
135
  CODEX_MODEL: input.model || process.env.CODEX_MODEL || "",
131
136
  AGENT_RELAY_APPROVAL: process.env.AGENT_RELAY_APPROVAL || profile.approval || "",
137
+ AGENT_RELAY_CODEX_PARENT_PID: String(process.ppid),
132
138
  };
133
139
  if (threadId) {
134
140
  spawnEnv.CODEX_THREAD_ID = threadId;
package/live-sidecar.ts CHANGED
@@ -32,6 +32,7 @@ interface Config {
32
32
  approvalPolicy?: string;
33
33
  sandbox?: string;
34
34
  approvalMode: ApprovalMode;
35
+ parentPid?: number;
35
36
  }
36
37
 
37
38
  interface RuntimeState {
@@ -104,6 +105,10 @@ class CodexLiveSidecar {
104
105
  while (!this.stopping) {
105
106
  let sleepMs = this.config.pollIntervalMs;
106
107
  try {
108
+ if (this.parentExited()) {
109
+ await this.stop("parent-exit");
110
+ break;
111
+ }
107
112
  const now = Date.now();
108
113
  if (now - this.lastHeartbeatAt >= this.config.heartbeatIntervalMs) {
109
114
  await this.relay.heartbeat(this.agentId, this.agentSession());
@@ -145,7 +150,7 @@ class CodexLiveSidecar {
145
150
  this.log(`loop error: ${describeError(error)}; retrying in ${sleepMs}ms`);
146
151
  }
147
152
  }
148
- await delay(sleepMs);
153
+ await this.delayWithParentCheck(sleepMs);
149
154
  }
150
155
  }
151
156
 
@@ -258,6 +263,21 @@ class CodexLiveSidecar {
258
263
  this.app.close();
259
264
  }
260
265
 
266
+ private parentExited(): boolean {
267
+ return Boolean(this.config.parentPid && !isAlive(this.config.parentPid));
268
+ }
269
+
270
+ private async delayWithParentCheck(ms: number): Promise<void> {
271
+ const deadline = Date.now() + ms;
272
+ while (!this.stopping && Date.now() < deadline) {
273
+ if (this.parentExited()) {
274
+ await this.stop("parent-exit");
275
+ return;
276
+ }
277
+ await delay(Math.min(500, Math.max(0, deadline - Date.now())));
278
+ }
279
+ }
280
+
261
281
  private async resumeKnownThread(threadId: string): Promise<Thread> {
262
282
  try {
263
283
  const resumed = await this.app.threadResume({
@@ -708,6 +728,15 @@ function envNumber(env: NodeJS.ProcessEnv, name: string, fallback: number): numb
708
728
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
709
729
  }
710
730
 
731
+ function isAlive(pid: number): boolean {
732
+ try {
733
+ process.kill(pid, 0);
734
+ return true;
735
+ } catch {
736
+ return false;
737
+ }
738
+ }
739
+
711
740
  export function parseThreadMode(raw: string | undefined): Config["threadMode"] {
712
741
  if (raw === "auto" || raw === "resume" || raw === "start") return raw;
713
742
  return "start";
@@ -749,6 +778,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {
749
778
  approvalPolicy: env.AGENT_RELAY_CODEX_APPROVAL_POLICY,
750
779
  sandbox: env.AGENT_RELAY_CODEX_SANDBOX,
751
780
  }),
781
+ parentPid: envNumber(env, "AGENT_RELAY_CODEX_PARENT_PID", 0) || undefined,
752
782
  };
753
783
  }
754
784
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-codex",
3
- "version": "0.4.24",
3
+ "version": "0.4.25",
4
4
  "description": "Codex integration for Agent Relay — auto-registers sessions as agents and enables inter-agent messaging",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay",
3
- "version": "0.4.24",
3
+ "version": "0.4.25",
4
4
  "description": "Agent Relay integration for Codex sessions",
5
5
  "author": {
6
6
  "name": "Edin Mujkanovic"