agent-relay-runner 0.129.0 → 0.129.1

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.0",
3
+ "version": "0.129.1",
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.0",
4
+ "version": "0.129.1",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -0,0 +1,120 @@
1
+ import { readFileSync, readdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { ContextState } from "agent-relay-sdk";
5
+ import { isRecord, stringValue } from "agent-relay-sdk";
6
+ import type { ManagedProcess } from "../adapter";
7
+
8
+ export function codexTokenUsageContext(params: Record<string, unknown> | undefined, now = Date.now()): ContextState | undefined {
9
+ const usage = isRecord(params?.tokenUsage) ? params.tokenUsage
10
+ : isRecord(params?.token_usage) ? params.token_usage
11
+ : isRecord(params?.usage) ? params.usage
12
+ : isRecord(params?.info) ? params.info
13
+ : params;
14
+ if (!usage) return undefined;
15
+ const total = isRecord(usage.total) ? usage.total
16
+ : isRecord(usage.totalTokenUsage) ? usage.totalTokenUsage
17
+ : isRecord(usage.total_token_usage) ? usage.total_token_usage
18
+ : undefined;
19
+ const last = isRecord(usage.last) ? usage.last
20
+ : isRecord(usage.lastTokenUsage) ? usage.lastTokenUsage
21
+ : isRecord(usage.last_token_usage) ? usage.last_token_usage
22
+ : undefined;
23
+ const tokensUsed = numberValue(last?.totalTokens) ?? numberValue(last?.total_tokens) ??
24
+ numberValue(total?.totalTokens) ?? numberValue(total?.total_tokens) ??
25
+ numberValue(usage.totalTokens) ?? numberValue(usage.total_tokens);
26
+ const tokensMax = numberValue(usage.modelContextWindow) ?? numberValue(usage.model_context_window) ??
27
+ numberValue(params?.modelContextWindow) ?? numberValue(params?.model_context_window);
28
+ if (tokensUsed === undefined || tokensMax === undefined || tokensMax <= 0) return undefined;
29
+ return {
30
+ utilization: Math.max(0, Math.min(1, tokensUsed / tokensMax)),
31
+ tokensUsed,
32
+ tokensMax,
33
+ lifecycleState: "working",
34
+ warmTopics: [],
35
+ activeMemories: [],
36
+ tasksSinceCompact: 0,
37
+ lastUpdatedAt: now,
38
+ source: "api",
39
+ confidence: "reported",
40
+ };
41
+ }
42
+
43
+ export function latestCodexContext(process: ManagedProcess): ContextState | undefined {
44
+ const meta = process.meta ?? {};
45
+ const current = isContextState(meta.context) ? meta.context : undefined;
46
+ const explicitPath = stringValue(meta.threadPath);
47
+ const threadId = stringValue(meta.threadId);
48
+ const rolloutPath = explicitPath ?? (threadId ? findCodexRolloutPath(threadId) : undefined);
49
+ if (!rolloutPath) return current;
50
+ const context = readCodexRolloutContext(rolloutPath);
51
+ if (!context) return current;
52
+ process.meta = { ...meta, context, threadPath: rolloutPath };
53
+ return context;
54
+ }
55
+
56
+ function readCodexRolloutContext(path: string): ContextState | undefined {
57
+ try {
58
+ return codexRolloutContextFromText(readFileSync(path, "utf8"));
59
+ } catch {
60
+ return undefined;
61
+ }
62
+ }
63
+
64
+ export function codexRolloutContextFromText(input: string): ContextState | undefined {
65
+ const lines = input.split(/\r?\n/);
66
+ for (let i = lines.length - 1; i >= 0; i--) {
67
+ const line = lines[i]?.trim();
68
+ if (!line) continue;
69
+ let event: unknown;
70
+ try {
71
+ event = JSON.parse(line);
72
+ } catch {
73
+ continue;
74
+ }
75
+ if (!isRecord(event)) continue;
76
+ const payload = isRecord(event.payload) ? event.payload : undefined;
77
+ if (payload?.type !== "token_count") continue;
78
+ const timestamp = typeof event.timestamp === "string" ? Date.parse(event.timestamp) : Date.now();
79
+ return codexTokenUsageContext(payload, Number.isFinite(timestamp) ? timestamp : Date.now());
80
+ }
81
+ return undefined;
82
+ }
83
+
84
+ function findCodexRolloutPath(threadId: string): string | undefined {
85
+ const root = join(process.env.CODEX_HOME || join(homedir(), ".codex"), "sessions");
86
+ return findFileContaining(root, threadId);
87
+ }
88
+
89
+ function findFileContaining(dir: string, needle: string, depth = 0): string | undefined {
90
+ if (depth > 8) return undefined;
91
+ let entries: Array<{ name: string; isFile(): boolean; isDirectory(): boolean }>;
92
+ try {
93
+ entries = readdirSync(dir, { withFileTypes: true });
94
+ } catch {
95
+ return undefined;
96
+ }
97
+ entries.sort((a, b) => b.name.localeCompare(a.name));
98
+ for (const entry of entries) {
99
+ const path = join(dir, entry.name);
100
+ if (entry.isFile() && entry.name.includes(needle) && entry.name.endsWith(".jsonl")) return path;
101
+ if (entry.isDirectory()) {
102
+ const found = findFileContaining(path, needle, depth + 1);
103
+ if (found) return found;
104
+ }
105
+ }
106
+ return undefined;
107
+ }
108
+
109
+ function numberValue(value: unknown): number | undefined {
110
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
111
+ }
112
+
113
+ export function isContextState(value: unknown): value is ContextState {
114
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
115
+ const state = value as Record<string, unknown>;
116
+ return typeof state.utilization === "number" &&
117
+ typeof state.lifecycleState === "string" &&
118
+ typeof state.source === "string" &&
119
+ typeof state.confidence === "string";
120
+ }
@@ -1,5 +1,4 @@
1
- import { accessSync, constants, existsSync, readFileSync, realpathSync, readdirSync } from "node:fs";
2
- import { homedir } from "node:os";
1
+ import { accessSync, constants, existsSync, realpathSync } from "node:fs";
3
2
  import { basename, join, resolve } from "node:path";
4
3
  import type { ContextState, InteractivePrompt, LivenessSignal, Message, MessageTurnEndReason, ProviderState } from "agent-relay-sdk";
5
4
  import { isRecord, stringValue } from "agent-relay-sdk";
@@ -11,6 +10,7 @@ import { assembleLaunch, bundledCodexSkillDirs, bundledSkillConfigArgs, material
11
10
  import { logger } from "../logger";
12
11
  import type { SessionEvent } from "../session-insights";
13
12
  import { CodexAgentMessageCapture } from "./codex-agent-message-capture";
13
+ import { codexTokenUsageContext, isContextState, latestCodexContext } from "./codex-rollout-context";
14
14
  import { computeLivenessSignal, type LivenessInputs } from "../liveness";
15
15
 
16
16
  /** Relay context prepended to a Codex agent's first turn: the standard relay
@@ -1038,107 +1038,6 @@ export function codexProviderStateFromThreadStatus(status: unknown, params?: Rec
1038
1038
  return undefined;
1039
1039
  }
1040
1040
 
1041
- export function codexTokenUsageContext(params: Record<string, unknown> | undefined, now = Date.now()): ContextState | undefined {
1042
- const usage = isRecord(params?.tokenUsage) ? params.tokenUsage
1043
- : isRecord(params?.token_usage) ? params.token_usage
1044
- : isRecord(params?.usage) ? params.usage
1045
- : isRecord(params?.info) ? params.info
1046
- : params;
1047
- if (!usage) return undefined;
1048
- const total = isRecord(usage.total) ? usage.total
1049
- : isRecord(usage.totalTokenUsage) ? usage.totalTokenUsage
1050
- : isRecord(usage.total_token_usage) ? usage.total_token_usage
1051
- : undefined;
1052
- const last = isRecord(usage.last) ? usage.last
1053
- : isRecord(usage.lastTokenUsage) ? usage.lastTokenUsage
1054
- : isRecord(usage.last_token_usage) ? usage.last_token_usage
1055
- : undefined;
1056
- const tokensUsed = numberValue(last?.totalTokens) ?? numberValue(last?.total_tokens) ??
1057
- numberValue(total?.totalTokens) ?? numberValue(total?.total_tokens) ??
1058
- numberValue(usage.totalTokens) ?? numberValue(usage.total_tokens);
1059
- const tokensMax = numberValue(usage.modelContextWindow) ?? numberValue(usage.model_context_window) ??
1060
- numberValue(params?.modelContextWindow) ?? numberValue(params?.model_context_window);
1061
- if (tokensUsed === undefined || tokensMax === undefined || tokensMax <= 0) return undefined;
1062
- return {
1063
- utilization: Math.max(0, Math.min(1, tokensUsed / tokensMax)),
1064
- tokensUsed,
1065
- tokensMax,
1066
- lifecycleState: "working",
1067
- warmTopics: [],
1068
- activeMemories: [],
1069
- tasksSinceCompact: 0,
1070
- lastUpdatedAt: now,
1071
- source: "api",
1072
- confidence: "reported",
1073
- };
1074
- }
1075
-
1076
- function latestCodexContext(process: ManagedProcess): ContextState | undefined {
1077
- const meta = process.meta ?? {};
1078
- const current = isContextState(meta.context) ? meta.context : undefined;
1079
- const explicitPath = stringValue(meta.threadPath);
1080
- const threadId = stringValue(meta.threadId);
1081
- const rolloutPath = explicitPath ?? (threadId ? findCodexRolloutPath(threadId) : undefined);
1082
- if (!rolloutPath) return current;
1083
- const context = readCodexRolloutContext(rolloutPath);
1084
- if (!context) return current;
1085
- process.meta = { ...meta, context, threadPath: rolloutPath };
1086
- return context;
1087
- }
1088
-
1089
- function readCodexRolloutContext(path: string): ContextState | undefined {
1090
- try {
1091
- return codexRolloutContextFromText(readFileSync(path, "utf8"));
1092
- } catch {
1093
- return undefined;
1094
- }
1095
- }
1096
-
1097
- export function codexRolloutContextFromText(input: string): ContextState | undefined {
1098
- const lines = input.split(/\r?\n/);
1099
- for (let i = lines.length - 1; i >= 0; i--) {
1100
- const line = lines[i]?.trim();
1101
- if (!line) continue;
1102
- let event: unknown;
1103
- try {
1104
- event = JSON.parse(line);
1105
- } catch {
1106
- continue;
1107
- }
1108
- if (!isRecord(event)) continue;
1109
- const payload = isRecord(event.payload) ? event.payload : undefined;
1110
- if (payload?.type !== "token_count") continue;
1111
- const timestamp = typeof event.timestamp === "string" ? Date.parse(event.timestamp) : Date.now();
1112
- return codexTokenUsageContext(payload, Number.isFinite(timestamp) ? timestamp : Date.now());
1113
- }
1114
- return undefined;
1115
- }
1116
-
1117
- function findCodexRolloutPath(threadId: string): string | undefined {
1118
- const root = join(process.env.CODEX_HOME || join(homedir(), ".codex"), "sessions");
1119
- return findFileContaining(root, threadId);
1120
- }
1121
-
1122
- function findFileContaining(dir: string, needle: string, depth = 0): string | undefined {
1123
- if (depth > 8) return undefined;
1124
- let entries: Array<{ name: string; isFile(): boolean; isDirectory(): boolean }>;
1125
- try {
1126
- entries = readdirSync(dir, { withFileTypes: true });
1127
- } catch {
1128
- return undefined;
1129
- }
1130
- entries.sort((a, b) => b.name.localeCompare(a.name));
1131
- for (const entry of entries) {
1132
- const path = join(dir, entry.name);
1133
- if (entry.isFile() && entry.name.includes(needle) && entry.name.endsWith(".jsonl")) return path;
1134
- if (entry.isDirectory()) {
1135
- const found = findFileContaining(path, needle, depth + 1);
1136
- if (found) return found;
1137
- }
1138
- }
1139
- return undefined;
1140
- }
1141
-
1142
1041
  function isSubagentThread(thread: Record<string, unknown> | undefined, params: Record<string, unknown> | undefined): boolean {
1143
1042
  if (thread?.threadSource === "subagent" || params?.threadSource === "subagent") return true;
1144
1043
  const source = isRecord(thread?.sessionSource) ? thread.sessionSource : isRecord(params?.sessionSource) ? params.sessionSource : undefined;
@@ -1174,20 +1073,6 @@ function activeFlags(value: unknown): string[] {
1174
1073
  return value.activeFlags.filter((flag): flag is string => typeof flag === "string" && flag.length > 0);
1175
1074
  }
1176
1075
 
1177
- function numberValue(value: unknown): number | undefined {
1178
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
1179
- }
1180
-
1181
- function isContextState(value: unknown): value is ContextState {
1182
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1183
- const state = value as Record<string, unknown>;
1184
- return typeof state.utilization === "number" &&
1185
- typeof state.lifecycleState === "string" &&
1186
- typeof state.source === "string" &&
1187
- typeof state.confidence === "string";
1188
- }
1189
-
1190
-
1191
1076
  export function codexModelConfigArgs(model?: string, effort?: string): string[] {
1192
1077
  const args: string[] = [];
1193
1078
  if (model) args.push("-c", `model=${tomlString(model)}`);
@@ -1209,8 +1094,20 @@ export function codexApprovalConfigArgs(approvalMode?: string): string[] {
1209
1094
  ];
1210
1095
  }
1211
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
+
1212
1104
  export function codexManagedConfigArgs(): string[] {
1213
- return ["-c", "check_for_update_on_startup=false"];
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
+ ];
1214
1111
  }
1215
1112
 
1216
1113
  export function isCodexCliCommand(command: string): boolean {
@@ -1260,6 +1157,9 @@ function isAgentRelayCodexShim(path: string): boolean {
1260
1157
  // codex.ts consumers + tests keep their import path. tomlString likewise from ../relay-mcp.
1261
1158
  export { bundledCodexSkillDirs, bundledSkillConfigArgs };
1262
1159
  export { tomlString };
1160
+ // Rollout/token-usage context parsing lives in ./codex-rollout-context; re-exported
1161
+ // so codex.ts consumers + tests keep their import path.
1162
+ export { codexTokenUsageContext, codexRolloutContextFromText } from "./codex-rollout-context";
1263
1163
 
1264
1164
  export function codexAppServerConfigArgs(...argLists: string[][]): string[] {
1265
1165
  const result: string[] = [];
package/src/relay-mcp.ts CHANGED
@@ -41,6 +41,23 @@ export function relayMcpEndpoint(relayUrl: string): string {
41
41
  return `${relayUrl.replace(/\/+$/, "")}${RELAY_MCP_PATH}`;
42
42
  }
43
43
 
44
+ // #1248 — per-server Codex MCP overrides. Default `tool_timeout_sec` (60s) and
45
+ // `startup_timeout_ms` are too short for our long-running tools (tests/releases run
46
+ // 5+ min), forcing agents to poll/sleep instead of just waiting on the call. Verified
47
+ // against the installed codex-cli binary (0.144.0): both fields live on the
48
+ // per-`[mcp_servers.<name>]` McpServerConfig struct, alongside `bearer_token_env_var`.
49
+ export const CODEX_MCP_TOOL_TIMEOUT_SEC = 900;
50
+ export const CODEX_MCP_STARTUP_TIMEOUT_MS = 30000;
51
+
52
+ function codexTimeoutConfigArgs(key: string): string[] {
53
+ return [
54
+ "-c",
55
+ `${key}.tool_timeout_sec=${CODEX_MCP_TOOL_TIMEOUT_SEC}`,
56
+ "-c",
57
+ `${key}.startup_timeout_ms=${CODEX_MCP_STARTUP_TIMEOUT_MS}`,
58
+ ];
59
+ }
60
+
44
61
  // Codex: `-c mcp_servers.<name>.*` overrides. `bearer_token_env_var` tells Codex to
45
62
  // read the token from the env var itself → transport resolves to streamable_http.
46
63
  // `endpoint` overrides the target URL (runner-local proxy, Stage 2 #215) — see above.
@@ -51,6 +68,7 @@ export function relayMcpCodexConfigArgs(relayUrl: string, endpoint?: string): st
51
68
  `${key}.url=${tomlString(endpoint ?? relayMcpEndpoint(relayUrl))}`,
52
69
  "-c",
53
70
  `${key}.bearer_token_env_var=${tomlString(RELAY_MCP_TOKEN_ENV)}`,
71
+ ...codexTimeoutConfigArgs(key),
54
72
  ];
55
73
  }
56
74
 
@@ -151,6 +169,7 @@ export function codexProvisionedMcpArgs(servers?: Record<string, ProvisioningMcp
151
169
  for (const [envKey, envVal] of Object.entries(server.env ?? {})) {
152
170
  args.push("-c", `${key}.env.${envKey}=${tomlString(envVal)}`);
153
171
  }
172
+ args.push(...codexTimeoutConfigArgs(key)); // #1248
154
173
  }
155
174
  return args;
156
175
  }