agent-relay-runner 0.129.0 → 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
|
@@ -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
|
+
}
|
|
@@ -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,6 +1,3 @@
|
|
|
1
|
-
import { accessSync, constants, existsSync, readFileSync, realpathSync, readdirSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { basename, join, resolve } from "node:path";
|
|
4
1
|
import type { ContextState, InteractivePrompt, LivenessSignal, Message, MessageTurnEndReason, ProviderState } from "agent-relay-sdk";
|
|
5
2
|
import { isRecord, stringValue } from "agent-relay-sdk";
|
|
6
3
|
import { isPidAlive, killPid, processTreePids, processTreePidsFromTable, waitForPidsExit } from "agent-relay-sdk/process-utils";
|
|
@@ -11,18 +8,32 @@ import { assembleLaunch, bundledCodexSkillDirs, bundledSkillConfigArgs, material
|
|
|
11
8
|
import { logger } from "../logger";
|
|
12
9
|
import type { SessionEvent } from "../session-insights";
|
|
13
10
|
import { CodexAgentMessageCapture } from "./codex-agent-message-capture";
|
|
11
|
+
import { codexTokenUsageContext, isContextState, latestCodexContext } from "./codex-rollout-context";
|
|
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
|
-
|
|
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;
|
|
@@ -1038,107 +1049,6 @@ export function codexProviderStateFromThreadStatus(status: unknown, params?: Rec
|
|
|
1038
1049
|
return undefined;
|
|
1039
1050
|
}
|
|
1040
1051
|
|
|
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
1052
|
function isSubagentThread(thread: Record<string, unknown> | undefined, params: Record<string, unknown> | undefined): boolean {
|
|
1143
1053
|
if (thread?.threadSource === "subagent" || params?.threadSource === "subagent") return true;
|
|
1144
1054
|
const source = isRecord(thread?.sessionSource) ? thread.sessionSource : isRecord(params?.sessionSource) ? params.sessionSource : undefined;
|
|
@@ -1174,127 +1084,14 @@ function activeFlags(value: unknown): string[] {
|
|
|
1174
1084
|
return value.activeFlags.filter((flag): flag is string => typeof flag === "string" && flag.length > 0);
|
|
1175
1085
|
}
|
|
1176
1086
|
|
|
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
|
-
export function codexModelConfigArgs(model?: string, effort?: string): string[] {
|
|
1192
|
-
const args: string[] = [];
|
|
1193
|
-
if (model) args.push("-c", `model=${tomlString(model)}`);
|
|
1194
|
-
if (effort) args.push("-c", `model_reasoning_effort=${tomlString(effort)}`);
|
|
1195
|
-
return args;
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
export function codexApprovalConfigArgs(approvalMode?: string): string[] {
|
|
1199
|
-
const sandboxMode = approvalMode === "open"
|
|
1200
|
-
? "danger-full-access"
|
|
1201
|
-
: approvalMode === "read-only"
|
|
1202
|
-
? "read-only"
|
|
1203
|
-
: "workspace-write";
|
|
1204
|
-
return [
|
|
1205
|
-
"-c",
|
|
1206
|
-
'approval_policy="never"',
|
|
1207
|
-
"-c",
|
|
1208
|
-
`sandbox_mode=${tomlString(sandboxMode)}`,
|
|
1209
|
-
];
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
export function codexManagedConfigArgs(): string[] {
|
|
1213
|
-
return ["-c", "check_for_update_on_startup=false"];
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
export function isCodexCliCommand(command: string): boolean {
|
|
1217
|
-
return basename(command) === "codex";
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
export function resolveRealCodexCommand(command: string, pathValue = process.env.PATH || ""): string {
|
|
1221
|
-
const candidates: string[] = [];
|
|
1222
|
-
if (command.includes("/")) candidates.push(command);
|
|
1223
|
-
for (const dir of pathValue.split(":").filter(Boolean)) candidates.push(join(dir, "codex"));
|
|
1224
|
-
|
|
1225
|
-
const seen = new Set<string>();
|
|
1226
|
-
for (const candidate of candidates) {
|
|
1227
|
-
if (seen.has(candidate)) continue;
|
|
1228
|
-
seen.add(candidate);
|
|
1229
|
-
if (!isExecutableFile(candidate)) continue;
|
|
1230
|
-
const real = realPath(candidate);
|
|
1231
|
-
if (isAgentRelayCodexShim(candidate) || isAgentRelayCodexShim(real)) continue;
|
|
1232
|
-
return candidate;
|
|
1233
|
-
}
|
|
1234
|
-
return command;
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
function isExecutableFile(path: string): boolean {
|
|
1238
|
-
try {
|
|
1239
|
-
accessSync(path, constants.X_OK);
|
|
1240
|
-
return true;
|
|
1241
|
-
} catch {
|
|
1242
|
-
return false;
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
function realPath(path: string): string {
|
|
1247
|
-
try {
|
|
1248
|
-
return realpathSync(path);
|
|
1249
|
-
} catch {
|
|
1250
|
-
return path;
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
function isAgentRelayCodexShim(path: string): boolean {
|
|
1255
|
-
return path.includes("/.agent-relay/codex/bin/codex");
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
1087
|
// #557 — bundledCodexSkillDirs + bundledSkillConfigArgs now live in ../launch-assembly
|
|
1259
1088
|
// (the single launch-arg home, shared with the assembler); re-exported so existing
|
|
1260
1089
|
// codex.ts consumers + tests keep their import path. tomlString likewise from ../relay-mcp.
|
|
1261
1090
|
export { bundledCodexSkillDirs, bundledSkillConfigArgs };
|
|
1262
1091
|
export { tomlString };
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
for (const args of argLists) {
|
|
1267
|
-
for (let i = 0; i < args.length; i++) {
|
|
1268
|
-
const arg = args[i];
|
|
1269
|
-
if (arg === "-c" || arg === "--config" || arg === "--enable" || arg === "--disable") {
|
|
1270
|
-
const value = args[i + 1];
|
|
1271
|
-
if (value !== undefined) {
|
|
1272
|
-
result.push(arg, value);
|
|
1273
|
-
i++;
|
|
1274
|
-
}
|
|
1275
|
-
} else if (arg?.startsWith("--config=") || arg?.startsWith("--enable=") || arg?.startsWith("--disable=")) {
|
|
1276
|
-
result.push(arg);
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
return result;
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
export function codexToolOutputTokenLimitConfigArgs(config: Pick<RunnerSpawnConfig, "agentProfile">): string[] {
|
|
1284
|
-
const configured = codexToolOutputTokenLimit(config.agentProfile?.providerOptions);
|
|
1285
|
-
if (configured === null) return [];
|
|
1286
|
-
const limit = configured ?? DEFAULT_CODEX_TOOL_OUTPUT_TOKEN_LIMIT;
|
|
1287
|
-
return ["-c", `tool_output_token_limit=${limit}`];
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
function codexToolOutputTokenLimit(providerOptions: unknown): number | null | undefined {
|
|
1291
|
-
if (!isRecord(providerOptions)) return undefined;
|
|
1292
|
-
const codex = providerOptions.codex;
|
|
1293
|
-
if (!isRecord(codex)) return undefined;
|
|
1294
|
-
const limit = codex.toolOutputTokenLimit;
|
|
1295
|
-
return typeof limit === "number" || limit === null ? limit : undefined;
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1092
|
+
// Rollout/token-usage context parsing lives in ./codex-rollout-context; re-exported
|
|
1093
|
+
// so codex.ts consumers + tests keep their import path.
|
|
1094
|
+
export { codexTokenUsageContext, codexRolloutContextFromText } from "./codex-rollout-context";
|
|
1298
1095
|
|
|
1299
1096
|
async function connectWithRetry(client: CodexAppClient, attempts = 40): Promise<void> {
|
|
1300
1097
|
// Give the freshly-spawned app-server a moment to bind its socket before the
|
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
|
}
|