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 +2 -2
- package/bin/agent-relay-codex.ts +97 -26
- package/live-sidecar.ts +34 -2
- package/package.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
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`,
|
|
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
|
|
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
|
|
package/bin/agent-relay-codex.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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.
|
|
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
|
|
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