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
|
@@ -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
|
+
}
|
package/src/adapters/codex.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { accessSync, constants, existsSync,
|
|
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 [
|
|
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
|
}
|