agent-relay-codex 0.4.23 → 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 +11 -2
- package/bin/agent-relay-codex.ts +111 -113
- package/hooks/session-start.ts +6 -0
- package/live-sidecar.ts +31 -1
- package/package.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
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`,
|
|
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
|
package/bin/agent-relay-codex.ts
CHANGED
|
@@ -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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
|
459
|
-
|
|
460
|
-
|
|
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:
|
|
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:
|
|
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(
|
|
473
|
-
stderr: Bun.file(join(
|
|
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(
|
|
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
|
-
|
|
881
|
-
|
|
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);
|
package/hooks/session-start.ts
CHANGED
|
@@ -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
|
|
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