agent-relay-runner 0.129.1 → 0.129.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
- "version": "0.129.1",
3
+ "version": "0.129.2",
4
4
  "description": "Unified provider lifecycle runner for Agent Relay",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Agent Relay runner bridge for Claude Code",
4
- "version": "0.129.1",
4
+ "version": "0.129.2",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -0,0 +1,143 @@
1
+ // Codex launch/config-arg construction: the `-c` override builders, CLI command
2
+ // resolution (shim detection), and the tool-output token limit. Extracted from
3
+ // codex.ts (file-size ratchet, epic #291); codex.ts re-exports these so existing
4
+ // consumers + tests keep their import path.
5
+ import { accessSync, constants, realpathSync } from "node:fs";
6
+ import { basename, join } from "node:path";
7
+ import { isRecord } from "agent-relay-sdk";
8
+ import type { RunnerSpawnConfig } from "../adapter";
9
+ import { tomlString } from "../relay-mcp";
10
+
11
+ export const DEFAULT_CODEX_TOOL_OUTPUT_TOKEN_LIMIT = 12_000;
12
+
13
+ // #1248 — default background-terminal poll window (300000ms/5min) is too short for
14
+ // our long-running commands (tests/releases run 5+ min), forcing agents to poll
15
+ // instead of just waiting. Verified against installed codex-cli's source (rust-v0.144.0):
16
+ // `background_terminal_max_timeout` is a top-level Config field — it replaced the older
17
+ // `background_terminal_timeout` key — and IS correctly threaded through: ConfigManager
18
+ // replays these launch `-c` overrides for every thread (config/mod.rs,
19
+ // app-server/src/config_manager.rs), so `UnifiedExecProcessManager` (unified_exec/mod.rs)
20
+ // is built with this raised ceiling and the `write_stdin` handler clamps empty-poll
21
+ // yield_time_ms against it (unified_exec/process_manager.rs). The config value alone does
22
+ // NOT make agents wait longer, though: codex's `write_stdin` tool schema
23
+ // (tools/handlers/shell_spec.rs::create_write_stdin_tool) ships a hardcoded description —
24
+ // "empty polls wait 5000-300000 ms by default" — that never reflects this override, so a
25
+ // model sees no signal it may request more and keeps polling at ~300000ms. That's why a
26
+ // live probe still saw ~300.001s polls after this override landed. codexRelayContextBlock()
27
+ // (codex.ts) tells Codex agents explicitly that they may request yield_time_ms up to this value.
28
+ const CODEX_BACKGROUND_TERMINAL_MAX_TIMEOUT_MS = 900000;
29
+
30
+ export const CODEX_BACKGROUND_TERMINAL_HINT =
31
+ `This Codex session's background-terminal poll window is configured to ${CODEX_BACKGROUND_TERMINAL_MAX_TIMEOUT_MS}ms ` +
32
+ "(15 min), well above the 300000ms ceiling the write_stdin tool's own description advertises. " +
33
+ "When polling a long-running background command with write_stdin, pass a larger " +
34
+ `yield_time_ms (up to ${CODEX_BACKGROUND_TERMINAL_MAX_TIMEOUT_MS}) instead of repeatedly ` +
35
+ "polling at the tool's documented default — it will be honored.";
36
+
37
+ export function codexModelConfigArgs(model?: string, effort?: string): string[] {
38
+ const args: string[] = [];
39
+ if (model) args.push("-c", `model=${tomlString(model)}`);
40
+ if (effort) args.push("-c", `model_reasoning_effort=${tomlString(effort)}`);
41
+ return args;
42
+ }
43
+
44
+ export function codexApprovalConfigArgs(approvalMode?: string): string[] {
45
+ const sandboxMode = approvalMode === "open"
46
+ ? "danger-full-access"
47
+ : approvalMode === "read-only"
48
+ ? "read-only"
49
+ : "workspace-write";
50
+ return [
51
+ "-c",
52
+ 'approval_policy="never"',
53
+ "-c",
54
+ `sandbox_mode=${tomlString(sandboxMode)}`,
55
+ ];
56
+ }
57
+
58
+ // #1248 — see CODEX_BACKGROUND_TERMINAL_MAX_TIMEOUT_MS above for why this override
59
+ // is correct but insufficient on its own.
60
+ export function codexManagedConfigArgs(): string[] {
61
+ return [
62
+ "-c",
63
+ "check_for_update_on_startup=false",
64
+ "-c",
65
+ `background_terminal_max_timeout=${CODEX_BACKGROUND_TERMINAL_MAX_TIMEOUT_MS}`,
66
+ ];
67
+ }
68
+
69
+ export function isCodexCliCommand(command: string): boolean {
70
+ return basename(command) === "codex";
71
+ }
72
+
73
+ export function resolveRealCodexCommand(command: string, pathValue = process.env.PATH || ""): string {
74
+ const candidates: string[] = [];
75
+ if (command.includes("/")) candidates.push(command);
76
+ for (const dir of pathValue.split(":").filter(Boolean)) candidates.push(join(dir, "codex"));
77
+
78
+ const seen = new Set<string>();
79
+ for (const candidate of candidates) {
80
+ if (seen.has(candidate)) continue;
81
+ seen.add(candidate);
82
+ if (!isExecutableFile(candidate)) continue;
83
+ const real = realPath(candidate);
84
+ if (isAgentRelayCodexShim(candidate) || isAgentRelayCodexShim(real)) continue;
85
+ return candidate;
86
+ }
87
+ return command;
88
+ }
89
+
90
+ function isExecutableFile(path: string): boolean {
91
+ try {
92
+ accessSync(path, constants.X_OK);
93
+ return true;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ function realPath(path: string): string {
100
+ try {
101
+ return realpathSync(path);
102
+ } catch {
103
+ return path;
104
+ }
105
+ }
106
+
107
+ function isAgentRelayCodexShim(path: string): boolean {
108
+ return path.includes("/.agent-relay/codex/bin/codex");
109
+ }
110
+
111
+ export function codexAppServerConfigArgs(...argLists: string[][]): string[] {
112
+ const result: string[] = [];
113
+ for (const args of argLists) {
114
+ for (let i = 0; i < args.length; i++) {
115
+ const arg = args[i];
116
+ if (arg === "-c" || arg === "--config" || arg === "--enable" || arg === "--disable") {
117
+ const value = args[i + 1];
118
+ if (value !== undefined) {
119
+ result.push(arg, value);
120
+ i++;
121
+ }
122
+ } else if (arg?.startsWith("--config=") || arg?.startsWith("--enable=") || arg?.startsWith("--disable=")) {
123
+ result.push(arg);
124
+ }
125
+ }
126
+ }
127
+ return result;
128
+ }
129
+
130
+ export function codexToolOutputTokenLimitConfigArgs(config: Pick<RunnerSpawnConfig, "agentProfile">): string[] {
131
+ const configured = codexToolOutputTokenLimit(config.agentProfile?.providerOptions);
132
+ if (configured === null) return [];
133
+ const limit = configured ?? DEFAULT_CODEX_TOOL_OUTPUT_TOKEN_LIMIT;
134
+ return ["-c", `tool_output_token_limit=${limit}`];
135
+ }
136
+
137
+ function codexToolOutputTokenLimit(providerOptions: unknown): number | null | undefined {
138
+ if (!isRecord(providerOptions)) return undefined;
139
+ const codex = providerOptions.codex;
140
+ if (!isRecord(codex)) return undefined;
141
+ const limit = codex.toolOutputTokenLimit;
142
+ return typeof limit === "number" || limit === null ? limit : undefined;
143
+ }
@@ -1,5 +1,3 @@
1
- import { accessSync, constants, existsSync, realpathSync } from "node:fs";
2
- import { basename, join, resolve } from "node:path";
3
1
  import type { ContextState, InteractivePrompt, LivenessSignal, Message, MessageTurnEndReason, ProviderState } from "agent-relay-sdk";
4
2
  import { isRecord, stringValue } from "agent-relay-sdk";
5
3
  import { isPidAlive, killPid, processTreePids, processTreePidsFromTable, waitForPidsExit } from "agent-relay-sdk/process-utils";
@@ -14,15 +12,28 @@ import { codexTokenUsageContext, isContextState, latestCodexContext } from "./co
14
12
  import { computeLivenessSignal, type LivenessInputs } from "../liveness";
15
13
 
16
14
  /** Relay context prepended to a Codex agent's first turn: the standard relay
17
- * blurb plus, when running in an isolated workspace, the deps caveat (#159). */
15
+ * blurb plus, when running in an isolated workspace, the deps caveat (#159),
16
+ * plus the #1248 background-terminal poll-ceiling hint. */
18
17
  function codexRelayContextBlock(): string {
19
- return [relayContext(), workspaceDepsNoteFromEnv()].filter(Boolean).join("\n\n");
18
+ return [relayContext(), workspaceDepsNoteFromEnv(), CODEX_BACKGROUND_TERMINAL_HINT].filter(Boolean).join("\n\n");
20
19
  }
21
20
  import { prepareCodexProfileHome, profileUsesHostProviderGlobals } from "../profile-home";
22
21
  import { CodexAppClient, type ClientEvent } from "./codex-client";
22
+ import { CODEX_BACKGROUND_TERMINAL_HINT, codexApprovalConfigArgs, codexAppServerConfigArgs, codexManagedConfigArgs, codexModelConfigArgs, codexToolOutputTokenLimitConfigArgs, isCodexCliCommand, resolveRealCodexCommand } from "./codex-launch-config";
23
23
 
24
24
  export { processTreePidsFromTable };
25
- export const DEFAULT_CODEX_TOOL_OUTPUT_TOKEN_LIMIT = 12_000;
25
+ // Launch/config-arg construction lives in ./codex-launch-config; re-exported so
26
+ // codex.ts consumers + tests keep their import path.
27
+ export {
28
+ codexApprovalConfigArgs,
29
+ codexAppServerConfigArgs,
30
+ codexManagedConfigArgs,
31
+ codexModelConfigArgs,
32
+ codexToolOutputTokenLimitConfigArgs,
33
+ DEFAULT_CODEX_TOOL_OUTPUT_TOKEN_LIMIT,
34
+ isCodexCliCommand,
35
+ resolveRealCodexCommand,
36
+ } from "./codex-launch-config";
26
37
 
27
38
  type PendingCodexApproval = {
28
39
  id: string;
@@ -1073,85 +1084,6 @@ function activeFlags(value: unknown): string[] {
1073
1084
  return value.activeFlags.filter((flag): flag is string => typeof flag === "string" && flag.length > 0);
1074
1085
  }
1075
1086
 
1076
- export function codexModelConfigArgs(model?: string, effort?: string): string[] {
1077
- const args: string[] = [];
1078
- if (model) args.push("-c", `model=${tomlString(model)}`);
1079
- if (effort) args.push("-c", `model_reasoning_effort=${tomlString(effort)}`);
1080
- return args;
1081
- }
1082
-
1083
- export function codexApprovalConfigArgs(approvalMode?: string): string[] {
1084
- const sandboxMode = approvalMode === "open"
1085
- ? "danger-full-access"
1086
- : approvalMode === "read-only"
1087
- ? "read-only"
1088
- : "workspace-write";
1089
- return [
1090
- "-c",
1091
- 'approval_policy="never"',
1092
- "-c",
1093
- `sandbox_mode=${tomlString(sandboxMode)}`,
1094
- ];
1095
- }
1096
-
1097
- // #1248 — default background-terminal poll window (300000ms/5min) is too short for
1098
- // our long-running commands (tests/releases run 5+ min), forcing agents to poll
1099
- // instead of just waiting. Verified against the installed codex-cli binary (0.144.0):
1100
- // `background_terminal_max_timeout` is a top-level (global) Config field — it replaced
1101
- // the older `background_terminal_timeout` key, which no longer appears in that build.
1102
- const CODEX_BACKGROUND_TERMINAL_MAX_TIMEOUT_MS = 900000;
1103
-
1104
- export function codexManagedConfigArgs(): string[] {
1105
- return [
1106
- "-c",
1107
- "check_for_update_on_startup=false",
1108
- "-c",
1109
- `background_terminal_max_timeout=${CODEX_BACKGROUND_TERMINAL_MAX_TIMEOUT_MS}`,
1110
- ];
1111
- }
1112
-
1113
- export function isCodexCliCommand(command: string): boolean {
1114
- return basename(command) === "codex";
1115
- }
1116
-
1117
- export function resolveRealCodexCommand(command: string, pathValue = process.env.PATH || ""): string {
1118
- const candidates: string[] = [];
1119
- if (command.includes("/")) candidates.push(command);
1120
- for (const dir of pathValue.split(":").filter(Boolean)) candidates.push(join(dir, "codex"));
1121
-
1122
- const seen = new Set<string>();
1123
- for (const candidate of candidates) {
1124
- if (seen.has(candidate)) continue;
1125
- seen.add(candidate);
1126
- if (!isExecutableFile(candidate)) continue;
1127
- const real = realPath(candidate);
1128
- if (isAgentRelayCodexShim(candidate) || isAgentRelayCodexShim(real)) continue;
1129
- return candidate;
1130
- }
1131
- return command;
1132
- }
1133
-
1134
- function isExecutableFile(path: string): boolean {
1135
- try {
1136
- accessSync(path, constants.X_OK);
1137
- return true;
1138
- } catch {
1139
- return false;
1140
- }
1141
- }
1142
-
1143
- function realPath(path: string): string {
1144
- try {
1145
- return realpathSync(path);
1146
- } catch {
1147
- return path;
1148
- }
1149
- }
1150
-
1151
- function isAgentRelayCodexShim(path: string): boolean {
1152
- return path.includes("/.agent-relay/codex/bin/codex");
1153
- }
1154
-
1155
1087
  // #557 — bundledCodexSkillDirs + bundledSkillConfigArgs now live in ../launch-assembly
1156
1088
  // (the single launch-arg home, shared with the assembler); re-exported so existing
1157
1089
  // codex.ts consumers + tests keep their import path. tomlString likewise from ../relay-mcp.
@@ -1161,41 +1093,6 @@ export { tomlString };
1161
1093
  // so codex.ts consumers + tests keep their import path.
1162
1094
  export { codexTokenUsageContext, codexRolloutContextFromText } from "./codex-rollout-context";
1163
1095
 
1164
- export function codexAppServerConfigArgs(...argLists: string[][]): string[] {
1165
- const result: string[] = [];
1166
- for (const args of argLists) {
1167
- for (let i = 0; i < args.length; i++) {
1168
- const arg = args[i];
1169
- if (arg === "-c" || arg === "--config" || arg === "--enable" || arg === "--disable") {
1170
- const value = args[i + 1];
1171
- if (value !== undefined) {
1172
- result.push(arg, value);
1173
- i++;
1174
- }
1175
- } else if (arg?.startsWith("--config=") || arg?.startsWith("--enable=") || arg?.startsWith("--disable=")) {
1176
- result.push(arg);
1177
- }
1178
- }
1179
- }
1180
- return result;
1181
- }
1182
-
1183
- export function codexToolOutputTokenLimitConfigArgs(config: Pick<RunnerSpawnConfig, "agentProfile">): string[] {
1184
- const configured = codexToolOutputTokenLimit(config.agentProfile?.providerOptions);
1185
- if (configured === null) return [];
1186
- const limit = configured ?? DEFAULT_CODEX_TOOL_OUTPUT_TOKEN_LIMIT;
1187
- return ["-c", `tool_output_token_limit=${limit}`];
1188
- }
1189
-
1190
- function codexToolOutputTokenLimit(providerOptions: unknown): number | null | undefined {
1191
- if (!isRecord(providerOptions)) return undefined;
1192
- const codex = providerOptions.codex;
1193
- if (!isRecord(codex)) return undefined;
1194
- const limit = codex.toolOutputTokenLimit;
1195
- return typeof limit === "number" || limit === null ? limit : undefined;
1196
- }
1197
-
1198
-
1199
1096
  async function connectWithRetry(client: CodexAppClient, attempts = 40): Promise<void> {
1200
1097
  // Give the freshly-spawned app-server a moment to bind its socket before the
1201
1098
  // first attempt, so we don't spend the retry budget on guaranteed refusals.