agent-relay-codex 0.4.25 → 0.4.27

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
@@ -126,9 +126,9 @@ agent-relay-codex uninstall --purge # also remove runtime state and PATH entrie
126
126
 
127
127
  ## How it works
128
128
 
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.
129
+ `codex-relay` launches `codex app-server`, then opens the TUI with `codex --remote <app-server-url>`. The normal SessionStart hook registers the visible TUI thread with Agent Relay, so Agent Relay messages and direct TUI messages share the same Codex context.
130
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.
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 `codex resume --remote <app-server-url>` if you later want to attach one.
132
132
 
133
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
134
 
@@ -5,6 +5,7 @@ import { dirname, join, resolve } from "node:path";
5
5
  import { createInterface } from "node:readline/promises";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import net from "node:net";
8
+ import { CodexAppClient } from "../app-client";
8
9
  import { approvalModeFromPermissions, codexArgsForApprovalMode, parseApprovalMode } from "../approval";
9
10
  import { loadAgentRelayProfile } from "../profile";
10
11
 
@@ -43,6 +44,7 @@ Usage:
43
44
  agent-relay-codex alias install
44
45
  agent-relay-codex alias remove
45
46
  agent-relay-codex doctor
47
+ agent-relay-codex upgrade [--dry-run] [--version VERSION] [--no-restart] [--yes]
46
48
  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
49
  codex-relay [--headless] [--relay-url URL] [--listen ws://127.0.0.1:PORT] [--thread-mode auto|resume|start] [--thread-id ID] [-- <codex args...>]
48
50
 
@@ -536,6 +538,41 @@ async function waitForManagedAgent(statePath: string, sidecarPid: number, timeou
536
538
  throw new Error(`timed out waiting for managed sidecar to register with Agent Relay; inspect ${dirname(statePath)}/sidecar.log`);
537
539
  }
538
540
 
541
+ async function loadedThreadIds(listenUrl: string): Promise<string[]> {
542
+ const client = new CodexAppClient(listenUrl);
543
+ try {
544
+ await client.connect();
545
+ await client.initialize();
546
+ const loaded = await client.threadLoadedList(50);
547
+ return loaded.data.filter((id) => typeof id === "string" && id.trim());
548
+ } finally {
549
+ client.close();
550
+ }
551
+ }
552
+
553
+ async function waitForRemoteTuiThread(
554
+ listenUrl: string,
555
+ beforeIds: Set<string>,
556
+ codex: ReturnType<typeof Bun.spawn>,
557
+ timeoutMs = 15000,
558
+ ): Promise<string | null> {
559
+ const deadline = Date.now() + timeoutMs;
560
+
561
+ while (Date.now() < deadline) {
562
+ try {
563
+ const loaded = await loadedThreadIds(listenUrl);
564
+ const candidate = loaded.find((id) => !beforeIds.has(id)) ?? loaded[0];
565
+ if (candidate) return candidate;
566
+ } catch {
567
+ // The remote TUI may still be negotiating its app-server connection.
568
+ }
569
+ if (codex.exitCode !== null) return null;
570
+ await Bun.sleep(250);
571
+ }
572
+
573
+ return null;
574
+ }
575
+
539
576
  function cleanupRun(runDir: string, appServer: ReturnType<typeof Bun.spawn> | null): void {
540
577
  if (existsSync(runDir)) {
541
578
  const pidsPath = join(runDir, "sidecar-pids.txt");
@@ -777,7 +814,7 @@ async function start(args: string[]): Promise<void> {
777
814
  installCodexSupport(true);
778
815
 
779
816
  let relayUrl = process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850";
780
- let listenUrl = process.env.CODEX_APP_SERVER_URL || "";
817
+ let listenUrl = process.env.AGENT_RELAY_CODEX_LISTEN || "";
781
818
  let threadMode = process.env.CODEX_THREAD_MODE || "start";
782
819
  let headless = process.env.AGENT_RELAY_CODEX_HEADLESS === "1";
783
820
  let threadId = process.env.CODEX_THREAD_ID || "";
@@ -853,7 +890,6 @@ async function start(args: string[]): Promise<void> {
853
890
  AGENT_RELAY_CODEX_APPROVAL_POLICY: permissions.approvalPolicy,
854
891
  AGENT_RELAY_CODEX_SANDBOX: permissions.sandbox,
855
892
  AGENT_RELAY_APPROVAL: effectiveApprovalMode,
856
- AGENT_RELAY_CODEX_MANAGED: "1",
857
893
  AGENT_RELAY_CODEX_PARENT_PID: String(process.pid),
858
894
  };
859
895
 
@@ -877,12 +913,61 @@ async function start(args: string[]): Promise<void> {
877
913
  process.once("exit", shutdown);
878
914
 
879
915
  await waitForPort(listenUrl, appServer);
916
+
917
+ console.log(`Agent Relay Codex session: ${listenUrl}`);
918
+ console.log(`Runtime: ${runDir}`);
919
+
920
+ if (!headless) {
921
+ const loadedBefore = new Set(await loadedThreadIds(listenUrl).catch(() => []));
922
+ const codex = Bun.spawn([codexBinary, "--remote", listenUrl, ...codexArgs], {
923
+ env,
924
+ stdin: "inherit",
925
+ stdout: "inherit",
926
+ stderr: "inherit",
927
+ });
928
+
929
+ const tuiThreadId = await waitForRemoteTuiThread(listenUrl, loadedBefore, codex);
930
+ if (tuiThreadId) {
931
+ const sidecarDir = join(runDir, sanitizeSessionKey(tuiThreadId));
932
+ const statePath = join(sidecarDir, "live-state.json");
933
+ const sidecarPid = spawnManagedSidecar({
934
+ runDir,
935
+ env: { ...env, AGENT_RELAY_CODEX_MANAGED: "1", AGENT_RELAY_CODEX_ASSUME_LOADED_THREAD: "1" },
936
+ sessionDir: sidecarDir,
937
+ statePath,
938
+ threadMode: "resume",
939
+ threadId: tuiThreadId,
940
+ });
941
+ appendLauncherLog(runDir, `REMOTE_TUI_THREAD_BOUND thread=${tuiThreadId} sidecarPid=${sidecarPid}`);
942
+ } else {
943
+ const fallbackDir = join(runDir, "auto");
944
+ const fallbackStatePath = join(fallbackDir, "live-state.json");
945
+ const fallbackPid = spawnManagedSidecar({
946
+ runDir,
947
+ env: { ...env, AGENT_RELAY_CODEX_MANAGED: "1" },
948
+ sessionDir: fallbackDir,
949
+ statePath: fallbackStatePath,
950
+ threadMode,
951
+ threadId: threadId || undefined,
952
+ });
953
+ appendLauncherLog(runDir, `REMOTE_TUI_FALLBACK_STARTED reason=THREAD_DISCOVERY_TIMEOUT sidecarPid=${fallbackPid}`);
954
+ }
955
+
956
+ const exitCode = await codex.exited;
957
+ shutdown();
958
+ process.exit(exitCode);
959
+ }
960
+
961
+ const managedSidecarEnv = {
962
+ ...env,
963
+ AGENT_RELAY_CODEX_MANAGED: "1",
964
+ };
880
965
  const initialSessionKey = threadId ? sanitizeSessionKey(threadId) : "managed";
881
966
  const sidecarDir = join(runDir, initialSessionKey);
882
967
  const statePath = join(sidecarDir, "live-state.json");
883
968
  const sidecarPid = spawnManagedSidecar({
884
969
  runDir,
885
- env,
970
+ env: managedSidecarEnv,
886
971
  sessionDir: sidecarDir,
887
972
  statePath,
888
973
  threadMode,
@@ -890,34 +975,14 @@ async function start(args: string[]): Promise<void> {
890
975
  });
891
976
  const managedAgent = await waitForManagedAgent(statePath, sidecarPid);
892
977
  const canonicalThreadId = managedAgent.threadId;
893
- const managedEnv = {
894
- ...env,
895
- CODEX_THREAD_MODE: "resume",
896
- CODEX_THREAD_ID: canonicalThreadId,
897
- };
898
978
 
899
- console.log(`Agent Relay Codex session: ${listenUrl}`);
900
979
  console.log(`Thread: ${canonicalThreadId}`);
901
980
  console.log(`Agent: ${managedAgent.agentId}`);
902
- console.log(`Runtime: ${runDir}`);
903
981
  appendLauncherLog(runDir, `MANAGED_THREAD thread=${canonicalThreadId} agent=${managedAgent.agentId} sidecarPid=${sidecarPid} headless=${headless}`);
904
982
 
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,
915
- stdin: "inherit",
916
- stdout: "inherit",
917
- stderr: "inherit",
918
- });
919
-
920
- const exitCode = await codex.exited;
983
+ console.log(`Headless relay sidecar pid: ${sidecarPid}`);
984
+ console.log(`Attach TUI with: ${codexBinary} resume --remote ${listenUrl}`);
985
+ const exitCode = await appServer.exited;
921
986
  shutdown();
922
987
  process.exit(exitCode);
923
988
  }
@@ -941,6 +1006,11 @@ async function doctor(): Promise<void> {
941
1006
  }
942
1007
  }
943
1008
 
1009
+ function upgrade(args: string[]): void {
1010
+ const hasProviderOverride = args.some((arg) => arg === "--providers" || arg === "--provider" || arg === "--codex" || arg === "--claude" || arg === "--all");
1011
+ runChecked(["agent-relay", "upgrade", ...(hasProviderOverride ? [] : ["--providers", "codex"]), ...args]);
1012
+ }
1013
+
944
1014
  async function install(args: string[]): Promise<void> {
945
1015
  const installAlias = args.includes("--alias");
946
1016
  const skipAlias = args.includes("--no-alias");
@@ -1000,6 +1070,7 @@ async function main(): Promise<void> {
1000
1070
  }
1001
1071
  if (command === "alias" && args[0] === "remove") return removeCodexAlias();
1002
1072
  if (command === "doctor") return doctor();
1073
+ if (command === "upgrade") return upgrade(args);
1003
1074
  if (command === "start") return start(args);
1004
1075
  return start(command ? [command, ...args] : []);
1005
1076
  }
package/live-sidecar.ts CHANGED
@@ -33,6 +33,7 @@ interface Config {
33
33
  sandbox?: string;
34
34
  approvalMode: ApprovalMode;
35
35
  parentPid?: number;
36
+ assumeLoadedThread: boolean;
36
37
  }
37
38
 
38
39
  interface RuntimeState {
@@ -90,10 +91,14 @@ class CodexLiveSidecar {
90
91
  constructor(private readonly config: Config) {
91
92
  this.relay = new RelayClient(config.relayUrl, (msg) => this.log(msg));
92
93
  this.app = this.createAppClient();
94
+ this.threadId = config.threadId || "";
93
95
  }
94
96
 
95
97
  async run(): Promise<void> {
96
98
  mkdirSync(dirname(this.config.statePath), { recursive: true });
99
+ if (this.config.assumeLoadedThread && this.config.threadId) {
100
+ this.log(`assuming remote TUI thread ${this.config.threadId} is already loaded`);
101
+ }
97
102
 
98
103
  process.on("SIGINT", () => void this.stop("SIGINT"));
99
104
  process.on("SIGTERM", () => void this.stop("SIGTERM"));
@@ -231,7 +236,7 @@ class CodexLiveSidecar {
231
236
  }
232
237
 
233
238
  const thread = this.threadId
234
- ? await this.resumeKnownThread(this.threadId)
239
+ ? await this.attachKnownThread(this.threadId)
235
240
  : await this.resolveThread();
236
241
  this.threadId = thread.id;
237
242
  this.syncThreadState(thread);
@@ -278,7 +283,11 @@ class CodexLiveSidecar {
278
283
  }
279
284
  }
280
285
 
281
- private async resumeKnownThread(threadId: string): Promise<Thread> {
286
+ private async attachKnownThread(threadId: string): Promise<Thread> {
287
+ if (this.config.assumeLoadedThread) {
288
+ return this.readThreadWithFallback(threadId).catch(() => syntheticLoadedThread(threadId, this.config.cwd));
289
+ }
290
+
282
291
  try {
283
292
  const resumed = await this.app.threadResume({
284
293
  threadId,
@@ -288,6 +297,13 @@ class CodexLiveSidecar {
288
297
  });
289
298
  return normalizeThread(resumed.thread);
290
299
  } catch (error) {
300
+ if (isThreadMissingRolloutError(error)) {
301
+ try {
302
+ return await this.readThreadWithFallback(threadId);
303
+ } catch {
304
+ // Fall through to normal resolution when the thread is neither loaded nor persisted.
305
+ }
306
+ }
291
307
  this.log(`resume failed for thread ${threadId}: ${describeError(error)}; falling back to thread resolution`);
292
308
  this.threadId = "";
293
309
  return this.resolveThread();
@@ -713,10 +729,25 @@ function normalizeThread(thread: Thread): Thread {
713
729
  };
714
730
  }
715
731
 
732
+ function syntheticLoadedThread(threadId: string, cwd: string): Thread {
733
+ return {
734
+ id: threadId,
735
+ cwd,
736
+ status: { type: "idle" },
737
+ updatedAt: Date.now(),
738
+ preview: "",
739
+ turns: [],
740
+ };
741
+ }
742
+
716
743
  function isThreadMaterializationError(error: unknown): boolean {
717
744
  return describeError(error).includes("not materialized yet");
718
745
  }
719
746
 
747
+ function isThreadMissingRolloutError(error: unknown): boolean {
748
+ return describeError(error).includes("no rollout found for thread id");
749
+ }
750
+
720
751
  function describeError(error: unknown): string {
721
752
  return error instanceof Error ? error.message : String(error);
722
753
  }
@@ -779,6 +810,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {
779
810
  sandbox: env.AGENT_RELAY_CODEX_SANDBOX,
780
811
  }),
781
812
  parentPid: envNumber(env, "AGENT_RELAY_CODEX_PARENT_PID", 0) || undefined,
813
+ assumeLoadedThread: env.AGENT_RELAY_CODEX_ASSUME_LOADED_THREAD === "1" || env.AGENT_RELAY_CODEX_ASSUME_LOADED_THREAD === "true",
782
814
  };
783
815
  }
784
816
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-codex",
3
- "version": "0.4.25",
3
+ "version": "0.4.27",
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.25",
3
+ "version": "0.4.27",
4
4
  "description": "Agent Relay integration for Codex sessions",
5
5
  "author": {
6
6
  "name": "Edin Mujkanovic"