@mono-agent/agent-runtime 0.20.11 → 0.20.14
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/MIGRATION.md +264 -25
- package/README.md +139 -448
- package/package.json +7 -43
- package/src/agent/tools/agent-tool.js +5 -5
- package/src/agent/tools/bash.js +1 -24
- package/src/agent/tools/codex-subscription-search.js +1 -1
- package/src/agent/tools/index.js +7 -0
- package/src/agent/tools/monitor.js +140 -0
- package/src/agent/tools/pi-bridge.js +92 -7
- package/src/agent/tools/shared/bash-environment.js +31 -0
- package/src/agent/tools/shared/monitors.js +274 -0
- package/src/agent/tools/shared/process-runner.js +23 -5
- package/src/ai/cost.js +13 -68
- package/src/ai/index.js +4 -17
- package/src/ai/pi-interop.js +65 -1
- package/src/ai/pi-oauth-compat.js +1 -1
- package/src/ai/providers/codex/app-server-client.js +592 -0
- package/src/ai/providers/pi-models.js +18 -10
- package/src/ai/providers/pi-native/compaction-driver.js +49 -21
- package/src/ai/providers/pi-native/harness-adapter.js +338 -0
- package/src/ai/providers/pi-native/result-builder.js +10 -10
- package/src/ai/providers/pi-native/session-lifecycle.js +95 -40
- package/src/ai/providers/pi-native/stream-subscriber.js +23 -5
- package/src/ai/providers/pi-native/turn-runner.js +34 -15
- package/src/ai/providers/pi-native.js +48 -22
- package/src/ai/runtime/capabilities.js +11 -56
- package/src/ai/runtime/model-refs.js +118 -153
- package/src/ai/runtime/registry.js +22 -56
- package/src/ai/runtime/router.js +48 -408
- package/src/ai/runtime/session-liveness.js +3 -4
- package/src/ai/runtime/sessions.js +4 -5
- package/src/ai/runtime/tool-policy.js +0 -2
- package/src/ai/types.js +12 -108
- package/src/index.js +0 -6
- package/src/runtime.js +8 -14
- package/types/agent/tools/agent-tool.d.ts +1 -2
- package/types/agent/tools/codex-subscription-search.d.ts +1 -1
- package/types/agent/tools/index.d.ts +1 -0
- package/types/agent/tools/monitor.d.ts +44 -0
- package/types/agent/tools/pi-bridge.d.ts +3 -2
- package/types/agent/tools/shared/bash-environment.d.ts +4 -0
- package/types/agent/tools/shared/monitors.d.ts +83 -0
- package/types/agent/tools/shared/process-runner.d.ts +11 -2
- package/types/ai/cost.d.ts +1 -2
- package/types/ai/index.d.ts +1 -4
- package/types/ai/pi-interop.d.ts +20 -0
- package/types/ai/providers/codex/app-server-client.d.ts +37 -0
- package/types/ai/providers/pi-native/harness-adapter.d.ts +56 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +3 -3
- package/types/ai/providers/pi-native/session-lifecycle.d.ts +2 -1
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +2 -2
- package/types/ai/providers/pi-native/turn-runner.d.ts +33 -6
- package/types/ai/providers/pi-native.d.ts +21 -4
- package/types/ai/runtime/capabilities.d.ts +21 -70
- package/types/ai/runtime/model-refs.d.ts +0 -24
- package/types/ai/runtime/router.d.ts +3 -10
- package/types/ai/runtime/tool-policy.d.ts +0 -2
- package/types/ai/types.d.ts +28 -250
- package/types/index.d.ts +0 -1
- package/src/ai/providers/acp-client.js +0 -1149
- package/src/ai/providers/acp-privacy.js +0 -124
- package/src/ai/providers/acp-public.js +0 -21
- package/src/ai/providers/acp-session-tokens.js +0 -282
- package/src/ai/providers/acp-transport.js +0 -356
- package/src/ai/providers/acp.js +0 -543
- package/src/ai/providers/claude-cli.js +0 -883
- package/src/ai/providers/claude-sandbox.js +0 -71
- package/src/ai/providers/claude-sdk-discovery-worker.js +0 -53
- package/src/ai/providers/claude-sdk-discovery.js +0 -352
- package/src/ai/providers/claude-sdk.js +0 -1127
- package/src/ai/providers/claude-subagent-activity.js +0 -719
- package/src/ai/providers/claude-subagents.js +0 -88
- package/src/ai/providers/codex-app.js +0 -2946
- package/src/ai/providers/opencode-app.js +0 -1109
- package/src/ai/providers/opencode-discovery.js +0 -39
- package/src/ai/providers/opencode-server.js +0 -508
- package/src/ai/runtime/context-windows.js +0 -46
- package/src/ai/runtime/fast-mode.js +0 -8
- package/src/ai/streaming/codex-events.js +0 -146
- package/src/ai/streaming/opencode-events.js +0 -59
- package/types/ai/providers/acp-client.d.ts +0 -227
- package/types/ai/providers/acp-privacy.d.ts +0 -25
- package/types/ai/providers/acp-public.d.ts +0 -7
- package/types/ai/providers/acp-session-tokens.d.ts +0 -41
- package/types/ai/providers/acp-transport.d.ts +0 -45
- package/types/ai/providers/acp.d.ts +0 -93
- package/types/ai/providers/claude-cli.d.ts +0 -305
- package/types/ai/providers/claude-sandbox.d.ts +0 -79
- package/types/ai/providers/claude-sdk-discovery-worker.d.ts +0 -1
- package/types/ai/providers/claude-sdk-discovery.d.ts +0 -97
- package/types/ai/providers/claude-sdk.d.ts +0 -138
- package/types/ai/providers/claude-subagent-activity.d.ts +0 -53
- package/types/ai/providers/claude-subagents.d.ts +0 -18
- package/types/ai/providers/codex-app.d.ts +0 -151
- package/types/ai/providers/opencode-app.d.ts +0 -96
- package/types/ai/providers/opencode-discovery.d.ts +0 -4
- package/types/ai/providers/opencode-server.d.ts +0 -20
- package/types/ai/runtime/context-windows.d.ts +0 -9
- package/types/ai/runtime/fast-mode.d.ts +0 -2
- package/types/ai/streaming/codex-events.d.ts +0 -40
- package/types/ai/streaming/opencode-events.d.ts +0 -42
|
@@ -1,2946 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { createInterface } from "node:readline";
|
|
4
|
-
import { normalizeCodexItemEvent } from "../streaming/codex-events.js";
|
|
5
|
-
import { createFileChangePayload } from "../file-change-stats.js";
|
|
6
|
-
import { formatLiveInputGuidance } from "../live-input-prompt.js";
|
|
7
|
-
import { estimateCost } from "../cost.js";
|
|
8
|
-
import { codexModelSupportsFastMode, normalizeFastMode } from "../runtime/fast-mode.js";
|
|
9
|
-
import { readRuntimeBrand } from "../../agent/tools/shared/runtime-context.js";
|
|
10
|
-
import { buildCapabilitiesUsed } from "../runtime/capabilities-used.js";
|
|
11
|
-
import { resolveSandboxPolicy } from "../../agent/tools/shared/tool-context.js";
|
|
12
|
-
import { createSessionRegistry } from "../runtime/sessions.js";
|
|
13
|
-
import { createSessionLiveness } from "../runtime/session-liveness.js";
|
|
14
|
-
import {
|
|
15
|
-
isAllowAllToolPolicy,
|
|
16
|
-
TOOL_POLICY_ALLOW_ALL_ONLY,
|
|
17
|
-
} from "../runtime/tool-policy.js";
|
|
18
|
-
import { toolLifecycleMetadata } from "../tool-lifecycle.js";
|
|
19
|
-
|
|
20
|
-
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
21
|
-
const DEFAULT_THREAD_START_ATTEMPTS = 2;
|
|
22
|
-
const DEFAULT_THREAD_START_BACKOFF_MS = 5_000;
|
|
23
|
-
const MIN_THREAD_START_TIMEOUT_MS = 60_000;
|
|
24
|
-
const MAX_THREAD_START_TIMEOUT_MS = 180_000;
|
|
25
|
-
const THREAD_START_PROMPT_CHARS_PER_STEP = 50_000;
|
|
26
|
-
const THREAD_START_TIMEOUT_STEP_MS = 30_000;
|
|
27
|
-
const CODEX_DIAGNOSTIC_BYTES = 8 * 1024;
|
|
28
|
-
const CODEX_STDERR_TAIL_BYTES = 8 * 1024;
|
|
29
|
-
const CODEX_SHUTDOWN_GRACE_MS = 1_000;
|
|
30
|
-
const CODEX_KILL_GRACE_MS = 1_000;
|
|
31
|
-
const CODEX_APP_SERVER_ARGS = ["app-server", "--listen", "stdio://"];
|
|
32
|
-
const CODEX_APP_SERVER_ISOLATED_ARGS = [
|
|
33
|
-
...CODEX_APP_SERVER_ARGS,
|
|
34
|
-
"-c",
|
|
35
|
-
"project_doc_max_bytes=0",
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
const SENSITIVE_ASSIGNMENT_RE = /((?:api[_-]?key|private[_-]?key|access[_-]?key|authorization|authentication|auth|bearer|cookie|credential|password|signature|sig|secret|token)\s*[:=]\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s,\r\n]+)/giu;
|
|
39
|
-
const SENSITIVE_HEADER_RE = /((?:(?:proxy-)?authorization|cookie|set-cookie)\s*[:=]\s*)[^\r\n]*/giu;
|
|
40
|
-
const SENSITIVE_JSON_LINE_RE = /("(?:api[_-]?key|private[_-]?key|access[_-]?key|authorization|authentication|auth|bearer|cookie|credential|password|signature|sig|secret|token)"\s*:\s*)[^\r\n]*/giu;
|
|
41
|
-
const SENSITIVE_ESCAPED_JSON_LINE_RE = /(\\"(?:api[_-]?key|private[_-]?key|access[_-]?key|authorization|authentication|auth|bearer|cookie|credential|password|signature|sig|secret|token)\\"\s*:\s*)[^\r\n]*/giu;
|
|
42
|
-
|
|
43
|
-
function normalizedSensitiveName(name) {
|
|
44
|
-
return String(name || "")
|
|
45
|
-
.replace(/([a-z0-9])([A-Z])/gu, "$1_$2")
|
|
46
|
-
.replace(/[^A-Za-z0-9]+/gu, "_")
|
|
47
|
-
.replace(/^_+|_+$/gu, "")
|
|
48
|
-
.toLowerCase();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function isSensitivePayloadField(name) {
|
|
52
|
-
const normalized = normalizedSensitiveName(name);
|
|
53
|
-
return /(?:^|_)(?:token|secret|password|authorization|api_key|apikey|credential|cookie|auth|authentication|bearer|private_key|access_key|signature|sig)$/u.test(normalized);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function isSensitiveEnvironmentKey(name) {
|
|
57
|
-
const normalized = normalizedSensitiveName(name);
|
|
58
|
-
return /(?:^|_)(?:token|secret|password|authorization|api_key|apikey|credential|cookie|auth|authentication|bearer|private_key|access_key|signature|sig)(?:_|$)/u.test(normalized);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function isSensitiveCliFlag(name) {
|
|
62
|
-
const normalized = normalizedSensitiveName(String(name || "").replace(/^-+/u, ""));
|
|
63
|
-
return isSensitivePayloadField(normalized)
|
|
64
|
-
|| /(?:^|_)(?:auth|private_key|access_key|signature|sig)$/u.test(normalized);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function boundedTimeout(value, fallback) {
|
|
68
|
-
const parsed = Number(value);
|
|
69
|
-
return Number.isFinite(parsed) && parsed >= 1 ? Math.trunc(parsed) : fallback;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function sensitiveEnvironmentValues(env) {
|
|
73
|
-
return [...new Set(Object.entries(env || {})
|
|
74
|
-
.filter(([key, value]) => isSensitiveEnvironmentKey(key) && typeof value === "string" && value.length >= 8)
|
|
75
|
-
.map(([, value]) => value))]
|
|
76
|
-
.sort((left, right) => right.length - left.length);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function addOpaqueSensitiveValue(target, value, { splitCredentials = false } = {}) {
|
|
80
|
-
if (typeof value !== "string" || value.length < 8) return;
|
|
81
|
-
target.add(value);
|
|
82
|
-
if (!splitCredentials) return;
|
|
83
|
-
const schemeMatch = value.match(/^\s*(Bearer|Basic|Token)\s+(.+?)\s*$/iu);
|
|
84
|
-
const payload = schemeMatch?.[2];
|
|
85
|
-
if (payload?.length >= 8) target.add(payload);
|
|
86
|
-
if (schemeMatch?.[1]?.toLowerCase() === "basic" && payload && payload.length <= 16 * 1024) {
|
|
87
|
-
try {
|
|
88
|
-
const decoded = Buffer.from(payload, "base64").toString("utf8");
|
|
89
|
-
if (decoded && !decoded.includes("\uFFFD")) {
|
|
90
|
-
addOpaqueSensitiveValue(target, decoded);
|
|
91
|
-
const separator = decoded.indexOf(":");
|
|
92
|
-
if (separator >= 0) {
|
|
93
|
-
addOpaqueSensitiveValue(target, decoded.slice(0, separator));
|
|
94
|
-
addOpaqueSensitiveValue(target, decoded.slice(separator + 1));
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
} catch {
|
|
98
|
-
// The raw Basic payload remains protected even if it is malformed.
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function addOpaqueSensitiveValues(target, values, { splitCredentials = false } = {}) {
|
|
104
|
-
if (!values || typeof values !== "object") return;
|
|
105
|
-
for (const value of Object.values(values)) {
|
|
106
|
-
if (Array.isArray(value)) {
|
|
107
|
-
value.forEach((entry) => addOpaqueSensitiveValue(target, entry, { splitCredentials }));
|
|
108
|
-
} else {
|
|
109
|
-
addOpaqueSensitiveValue(target, value, { splitCredentials });
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function addEncodedCredentialValue(target, value) {
|
|
115
|
-
addOpaqueSensitiveValue(target, value);
|
|
116
|
-
try {
|
|
117
|
-
const decoded = decodeURIComponent(value);
|
|
118
|
-
addOpaqueSensitiveValue(target, decoded);
|
|
119
|
-
addOpaqueSensitiveValue(target, encodeURIComponent(decoded));
|
|
120
|
-
} catch {
|
|
121
|
-
// Invalid percent escapes are still covered by the original raw value.
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function addUrlSensitiveValues(target, rawUrl) {
|
|
126
|
-
if (typeof rawUrl !== "string" || !rawUrl) return;
|
|
127
|
-
try {
|
|
128
|
-
const parsed = new URL(rawUrl);
|
|
129
|
-
addEncodedCredentialValue(target, parsed.username);
|
|
130
|
-
addEncodedCredentialValue(target, parsed.password);
|
|
131
|
-
for (const value of parsed.searchParams.values()) {
|
|
132
|
-
// Query parameter names are provider-defined. All opaque query values are
|
|
133
|
-
// treated as credentials rather than betting on a finite key allowlist.
|
|
134
|
-
addEncodedCredentialValue(target, value);
|
|
135
|
-
}
|
|
136
|
-
for (const part of parsed.search.slice(1).split("&")) {
|
|
137
|
-
if (part.includes("=")) addEncodedCredentialValue(target, part.slice(part.indexOf("=") + 1));
|
|
138
|
-
}
|
|
139
|
-
} catch {
|
|
140
|
-
// Non-URL templates are passed through unchanged and may still be covered
|
|
141
|
-
// by a surrounding secret-bearing CLI flag or payload-field redaction.
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function addHeaderArgumentSensitiveValues(target, header) {
|
|
146
|
-
if (typeof header !== "string") return;
|
|
147
|
-
const separator = header.indexOf(":");
|
|
148
|
-
const value = separator >= 0 ? header.slice(separator + 1).trim() : header.trim();
|
|
149
|
-
addOpaqueSensitiveValue(target, value, { splitCredentials: true });
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function addCliSensitiveValues(target, args) {
|
|
153
|
-
if (!Array.isArray(args)) return;
|
|
154
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
155
|
-
const argument = args[index];
|
|
156
|
-
if (typeof argument !== "string") continue;
|
|
157
|
-
addUrlSensitiveValues(target, argument);
|
|
158
|
-
const equals = argument.indexOf("=");
|
|
159
|
-
if (equals > 0) {
|
|
160
|
-
const flag = argument.slice(0, equals);
|
|
161
|
-
const value = argument.slice(equals + 1);
|
|
162
|
-
// `--url=...` and `--endpoint=...` are not themselves secret flags, but
|
|
163
|
-
// their inline RHS can contain URL userinfo or query credentials.
|
|
164
|
-
addUrlSensitiveValues(target, value);
|
|
165
|
-
if (/^(?:-H|--header|--http-header)$/iu.test(flag)) {
|
|
166
|
-
addHeaderArgumentSensitiveValues(target, value);
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
if (isSensitiveCliFlag(flag)) {
|
|
170
|
-
addOpaqueSensitiveValue(target, value, { splitCredentials: true });
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
if (isSensitiveCliFlag(argument) && typeof args[index + 1] === "string") {
|
|
175
|
-
addOpaqueSensitiveValue(target, args[index + 1], { splitCredentials: true });
|
|
176
|
-
index += 1;
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
if (/^(?:-H|--header|--http-header)$/iu.test(argument) && typeof args[index + 1] === "string") {
|
|
180
|
-
addHeaderArgumentSensitiveValues(target, args[index + 1]);
|
|
181
|
-
index += 1;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function codexRequestSensitiveValues(options = {}) {
|
|
187
|
-
const values = new Set(sensitiveEnvironmentValues({
|
|
188
|
-
...process.env,
|
|
189
|
-
...(options.codexAppServerEnv || {}),
|
|
190
|
-
}));
|
|
191
|
-
for (const server of Object.values(options.mcpServers || {})) {
|
|
192
|
-
if (!server || typeof server !== "object") continue;
|
|
193
|
-
// MCP env/header names are provider-defined and need not contain words such
|
|
194
|
-
// as "token" or "secret". Treat every opaque value on these credential-
|
|
195
|
-
// bearing surfaces as sensitive instead of relying on a key-name heuristic.
|
|
196
|
-
addOpaqueSensitiveValues(values, server.env);
|
|
197
|
-
addOpaqueSensitiveValues(values, server.headers, { splitCredentials: true });
|
|
198
|
-
addUrlSensitiveValues(values, server.url);
|
|
199
|
-
addCliSensitiveValues(values, server.args);
|
|
200
|
-
}
|
|
201
|
-
addCliSensitiveValues(values, options.codexAppServerArgs);
|
|
202
|
-
return [...values].sort((left, right) => right.length - left.length);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function leadingSensitiveOverlap(text, sensitiveValue) {
|
|
206
|
-
const maxLength = Math.min(text.length, sensitiveValue.length, CODEX_STDERR_TAIL_BYTES);
|
|
207
|
-
if (maxLength < 8) return 0;
|
|
208
|
-
const pattern = text.slice(0, maxLength);
|
|
209
|
-
const failure = new Array(pattern.length).fill(0);
|
|
210
|
-
for (let index = 1, matched = 0; index < pattern.length; index += 1) {
|
|
211
|
-
while (matched > 0 && pattern[index] !== pattern[matched]) matched = failure[matched - 1];
|
|
212
|
-
if (pattern[index] === pattern[matched]) matched += 1;
|
|
213
|
-
failure[index] = matched;
|
|
214
|
-
}
|
|
215
|
-
let matched = 0;
|
|
216
|
-
for (const character of sensitiveValue.slice(-maxLength)) {
|
|
217
|
-
while (matched > 0 && character !== pattern[matched]) matched = failure[matched - 1];
|
|
218
|
-
if (character === pattern[matched]) matched += 1;
|
|
219
|
-
}
|
|
220
|
-
return matched >= 8 ? matched : 0;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function redactCodexDiagnostic(text, sensitiveValues, truncatedStart = false) {
|
|
224
|
-
let redacted = String(text || "");
|
|
225
|
-
for (const value of sensitiveValues) {
|
|
226
|
-
if (truncatedStart) {
|
|
227
|
-
const overlap = leadingSensitiveOverlap(redacted, value);
|
|
228
|
-
if (overlap > 0) redacted = `[REDACTED]${redacted.slice(overlap)}`;
|
|
229
|
-
}
|
|
230
|
-
redacted = redacted.split(value).join("[REDACTED]");
|
|
231
|
-
}
|
|
232
|
-
return redacted
|
|
233
|
-
.replace(SENSITIVE_HEADER_RE, "$1[REDACTED]")
|
|
234
|
-
.replace(/\bBearer\s+[A-Za-z0-9._~+\/-]{12,}/giu, "Bearer [REDACTED]")
|
|
235
|
-
.replace(/\b(?:sk|pk|sess|oauth)[-_][A-Za-z0-9._-]{12,}\b/giu, "[REDACTED]")
|
|
236
|
-
.replace(SENSITIVE_ESCAPED_JSON_LINE_RE, '$1\\"[REDACTED]\\"')
|
|
237
|
-
.replace(SENSITIVE_JSON_LINE_RE, '$1"[REDACTED]"')
|
|
238
|
-
.replace(SENSITIVE_ASSIGNMENT_RE, "$1[REDACTED]");
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function utf8Head(text, limit) {
|
|
242
|
-
if (limit <= 0) return "";
|
|
243
|
-
const bytes = Buffer.from(String(text || ""));
|
|
244
|
-
if (bytes.length <= limit) return bytes.toString("utf8");
|
|
245
|
-
let end = limit;
|
|
246
|
-
while (end > 0 && (bytes[end] & 0xc0) === 0x80) end -= 1;
|
|
247
|
-
return bytes.subarray(0, end).toString("utf8");
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function boundCodexDiagnostic(text, limit = CODEX_DIAGNOSTIC_BYTES) {
|
|
251
|
-
const value = String(text || "");
|
|
252
|
-
const byteLength = Buffer.byteLength(value);
|
|
253
|
-
if (byteLength <= limit) return value;
|
|
254
|
-
let droppedBytes = byteLength - limit;
|
|
255
|
-
let marker = `\n[truncated ${droppedBytes} later bytes]`;
|
|
256
|
-
let bodyLimit = Math.max(0, limit - Buffer.byteLength(marker));
|
|
257
|
-
droppedBytes = byteLength - bodyLimit;
|
|
258
|
-
marker = `\n[truncated ${droppedBytes} later bytes]`;
|
|
259
|
-
bodyLimit = Math.max(0, limit - Buffer.byteLength(marker));
|
|
260
|
-
return utf8Head(value, bodyLimit) + marker;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function safeDiagnosticString(value) {
|
|
264
|
-
if (typeof value === "string") return value;
|
|
265
|
-
if (value instanceof Error && typeof value.message === "string") return value.message;
|
|
266
|
-
try {
|
|
267
|
-
const serialized = JSON.stringify(value);
|
|
268
|
-
return typeof serialized === "string" ? serialized : String(value ?? "");
|
|
269
|
-
} catch {
|
|
270
|
-
try {
|
|
271
|
-
return String(value);
|
|
272
|
-
} catch {
|
|
273
|
-
return "Codex app-server diagnostic unavailable";
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function sanitizeCodexDiagnostic(value, sensitiveValues, limit = CODEX_DIAGNOSTIC_BYTES) {
|
|
279
|
-
return boundCodexDiagnostic(
|
|
280
|
-
redactCodexDiagnostic(safeDiagnosticString(value), sensitiveValues),
|
|
281
|
-
limit,
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function sanitizeCodexProtocolCode(value, sensitiveValues) {
|
|
286
|
-
return typeof value === "number"
|
|
287
|
-
? value
|
|
288
|
-
: sanitizeCodexDiagnostic(value, sensitiveValues, 256);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function boundedCodexDiagnosticPayload(value, sensitiveValues, limit) {
|
|
292
|
-
const sanitized = redactCodexPayload(value, sensitiveValues);
|
|
293
|
-
try {
|
|
294
|
-
if (Buffer.byteLength(JSON.stringify(sanitized) || "") <= limit) return sanitized;
|
|
295
|
-
} catch {
|
|
296
|
-
// Fall through to a safe string summary for non-serializable values.
|
|
297
|
-
}
|
|
298
|
-
return sanitizeCodexDiagnostic(value, sensitiveValues, limit);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function redactCodexPayload(value, sensitiveValues, seen = new WeakSet(), depth = 0) {
|
|
302
|
-
if (typeof value === "string") return redactCodexDiagnostic(value, sensitiveValues);
|
|
303
|
-
if (value === null || typeof value !== "object") return value;
|
|
304
|
-
if (value instanceof Error) {
|
|
305
|
-
const errorCode = /** @type {any} */ (value).code;
|
|
306
|
-
return {
|
|
307
|
-
name: redactCodexDiagnostic(value.name || "Error", sensitiveValues),
|
|
308
|
-
message: sanitizeCodexDiagnostic(value.message || value, sensitiveValues),
|
|
309
|
-
...(errorCode !== undefined
|
|
310
|
-
? { code: sanitizeCodexProtocolCode(errorCode, sensitiveValues) }
|
|
311
|
-
: {}),
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
if (depth >= 20) return "[truncated nested Codex payload]";
|
|
315
|
-
if (seen.has(value)) return "[circular Codex payload]";
|
|
316
|
-
seen.add(value);
|
|
317
|
-
if (Array.isArray(value)) {
|
|
318
|
-
const result = value.map((entry) => redactCodexPayload(entry, sensitiveValues, seen, depth + 1));
|
|
319
|
-
seen.delete(value);
|
|
320
|
-
return result;
|
|
321
|
-
}
|
|
322
|
-
const result = {};
|
|
323
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
324
|
-
result[key] = isSensitivePayloadField(key)
|
|
325
|
-
? "[REDACTED]"
|
|
326
|
-
: redactCodexPayload(entry, sensitiveValues, seen, depth + 1);
|
|
327
|
-
}
|
|
328
|
-
seen.delete(value);
|
|
329
|
-
return result;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function sanitizeCodexResponseError(error, sensitiveValues) {
|
|
333
|
-
const sanitized = redactCodexPayload(error, sensitiveValues);
|
|
334
|
-
let serialized;
|
|
335
|
-
try {
|
|
336
|
-
serialized = JSON.stringify(sanitized);
|
|
337
|
-
} catch {
|
|
338
|
-
serialized = "";
|
|
339
|
-
}
|
|
340
|
-
if (Buffer.byteLength(serialized || "") <= CODEX_DIAGNOSTIC_BYTES) return sanitized;
|
|
341
|
-
|
|
342
|
-
const data = error && typeof error === "object" ? error.data : null;
|
|
343
|
-
const nestedError = data && typeof data === "object" ? data.error : null;
|
|
344
|
-
const info = data?.info ?? nestedError?.info ?? error?.info;
|
|
345
|
-
return {
|
|
346
|
-
...(error?.code !== undefined
|
|
347
|
-
? { code: sanitizeCodexProtocolCode(error.code, sensitiveValues) }
|
|
348
|
-
: {}),
|
|
349
|
-
message: sanitizeCodexDiagnostic(codexErrorMessage(error), sensitiveValues, 6 * 1024),
|
|
350
|
-
...(info !== undefined
|
|
351
|
-
? { data: { info: boundedCodexDiagnosticPayload(info, sensitiveValues, 1_024) } }
|
|
352
|
-
: {}),
|
|
353
|
-
diagnostic_truncated: true,
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const CODEX_DIAGNOSTIC_NOTIFICATION_METHODS = new Set([
|
|
358
|
-
"warning",
|
|
359
|
-
"error",
|
|
360
|
-
"configWarning",
|
|
361
|
-
"guardianWarning",
|
|
362
|
-
]);
|
|
363
|
-
|
|
364
|
-
function sanitizeCodexNotification(notification, sensitiveValues) {
|
|
365
|
-
const safe = redactCodexPayload(notification, sensitiveValues);
|
|
366
|
-
if (!safe || typeof safe !== "object") return safe;
|
|
367
|
-
if (CODEX_DIAGNOSTIC_NOTIFICATION_METHODS.has(safe.method)) {
|
|
368
|
-
const params = safe.params && typeof safe.params === "object" ? safe.params : {};
|
|
369
|
-
return {
|
|
370
|
-
...safe,
|
|
371
|
-
params: {
|
|
372
|
-
...(params.code !== undefined
|
|
373
|
-
? { code: sanitizeCodexProtocolCode(params.code, sensitiveValues) }
|
|
374
|
-
: {}),
|
|
375
|
-
message: sanitizeCodexDiagnostic(params.message || params.error || params, sensitiveValues),
|
|
376
|
-
},
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
if (safe.method === "turn/completed" && safe.params?.turn?.error !== undefined) {
|
|
380
|
-
return {
|
|
381
|
-
...safe,
|
|
382
|
-
params: {
|
|
383
|
-
...safe.params,
|
|
384
|
-
turn: {
|
|
385
|
-
...safe.params.turn,
|
|
386
|
-
error: sanitizeCodexResponseError(safe.params.turn.error, sensitiveValues),
|
|
387
|
-
},
|
|
388
|
-
},
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
if ((safe.method === "item/started" || safe.method === "item/completed") && safe.params?.item?.error !== undefined) {
|
|
392
|
-
return {
|
|
393
|
-
...safe,
|
|
394
|
-
params: {
|
|
395
|
-
...safe.params,
|
|
396
|
-
item: {
|
|
397
|
-
...safe.params.item,
|
|
398
|
-
error: sanitizeCodexResponseError(safe.params.item.error, sensitiveValues),
|
|
399
|
-
},
|
|
400
|
-
},
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
return safe;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function utf8Tail(text, limit) {
|
|
407
|
-
if (limit <= 0) return "";
|
|
408
|
-
const bytes = Buffer.from(String(text || ""));
|
|
409
|
-
if (bytes.length <= limit) return bytes.toString("utf8");
|
|
410
|
-
let start = bytes.length - limit;
|
|
411
|
-
while (start < bytes.length && (bytes[start] & 0xc0) === 0x80) start += 1;
|
|
412
|
-
return bytes.subarray(start).toString("utf8");
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function createCodexStderrTail(sensitiveValues, limit = CODEX_STDERR_TAIL_BYTES) {
|
|
416
|
-
let buffer = Buffer.alloc(0);
|
|
417
|
-
let bytesDropped = 0;
|
|
418
|
-
return {
|
|
419
|
-
push(chunk) {
|
|
420
|
-
const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk ?? ""));
|
|
421
|
-
if (incoming.length === 0) return;
|
|
422
|
-
if (incoming.length >= limit) {
|
|
423
|
-
bytesDropped += buffer.length + incoming.length - limit;
|
|
424
|
-
buffer = Buffer.from(incoming.subarray(incoming.length - limit));
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
const overflow = Math.max(0, buffer.length + incoming.length - limit);
|
|
428
|
-
bytesDropped += overflow;
|
|
429
|
-
buffer = Buffer.concat([buffer.subarray(overflow), incoming], Math.min(limit, buffer.length + incoming.length));
|
|
430
|
-
},
|
|
431
|
-
toString() {
|
|
432
|
-
const redacted = redactCodexDiagnostic(
|
|
433
|
-
buffer.toString("utf8").replace(/^\uFFFD/u, ""),
|
|
434
|
-
sensitiveValues,
|
|
435
|
-
bytesDropped > 0,
|
|
436
|
-
).trim();
|
|
437
|
-
if (bytesDropped === 0) return utf8Tail(redacted, limit);
|
|
438
|
-
const marker = `[truncated ${bytesDropped} earlier bytes]\n`;
|
|
439
|
-
const bodyLimit = Math.max(0, limit - Buffer.byteLength(marker));
|
|
440
|
-
return marker + utf8Tail(redacted, bodyLimit);
|
|
441
|
-
},
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const CODEX_APP_CAPABILITIES = {
|
|
446
|
-
kind: "codex-app",
|
|
447
|
-
runtime: "app-server",
|
|
448
|
-
streaming: true,
|
|
449
|
-
structured_output: true,
|
|
450
|
-
// codex-app emits the started thread id, surfaced as provider_session_id.
|
|
451
|
-
// With options.sessionKeepAlive the app-server subprocess + thread stay
|
|
452
|
-
// live in codexSessions, so a follow-up run can resume the thread via
|
|
453
|
-
// options.sessionId. The protocol still has no thread/load primitive, so
|
|
454
|
-
// resume only works while the subprocess is alive.
|
|
455
|
-
supports_session_resume: true,
|
|
456
|
-
native_runtime_config: null,
|
|
457
|
-
supports_mcp: true,
|
|
458
|
-
supports_skills: true,
|
|
459
|
-
supports_builtin_tools: true,
|
|
460
|
-
supports_live_input: true,
|
|
461
|
-
// Codex owns its collaboration agents and exposes their lifecycle through
|
|
462
|
-
// app-server. This does not mean the bridge can inject caller-defined
|
|
463
|
-
// nativeSubagents profiles; that richer request contract is rejected below.
|
|
464
|
-
supports_native_subagents: true,
|
|
465
|
-
supports_fast_mode: true,
|
|
466
|
-
tool_policy: TOOL_POLICY_ALLOW_ALL_ONLY,
|
|
467
|
-
};
|
|
468
|
-
|
|
469
|
-
function promptFromMessages(messages) {
|
|
470
|
-
return Array.isArray(messages)
|
|
471
|
-
? messages.map((message) => typeof message.content === "string" ? message.content : JSON.stringify(message.content)).join("\n\n")
|
|
472
|
-
: String(messages || "");
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function pushUniqueText(texts, text) {
|
|
476
|
-
const value = typeof text === "string" ? text.trim() : "";
|
|
477
|
-
if (!value) return;
|
|
478
|
-
if (texts.some((existing) => existing.trim() === value)) return;
|
|
479
|
-
texts.push(value);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function userTextInput(text) {
|
|
483
|
-
return [{ type: "text", text: String(text || ""), text_elements: [] }];
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
function integerOption(value, fallback, { min = 0, max = Number.MAX_SAFE_INTEGER } = {}) {
|
|
487
|
-
const n = Number(value);
|
|
488
|
-
if (!Number.isFinite(n)) return fallback;
|
|
489
|
-
return Math.min(max, Math.max(min, Math.trunc(n)));
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function clamp(value, min, max) {
|
|
493
|
-
return Math.min(max, Math.max(min, value));
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function defaultThreadStartTimeoutMs(systemPrompt) {
|
|
497
|
-
const promptChars = String(systemPrompt || "").length;
|
|
498
|
-
const sizeSteps = Math.max(0, Math.ceil(promptChars / THREAD_START_PROMPT_CHARS_PER_STEP) - 1);
|
|
499
|
-
return clamp(
|
|
500
|
-
MIN_THREAD_START_TIMEOUT_MS + (sizeSteps * THREAD_START_TIMEOUT_STEP_MS),
|
|
501
|
-
MIN_THREAD_START_TIMEOUT_MS,
|
|
502
|
-
MAX_THREAD_START_TIMEOUT_MS,
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function threadStartPolicy(systemPrompt, options = {}) {
|
|
507
|
-
return {
|
|
508
|
-
timeoutMs: integerOption(
|
|
509
|
-
options.codexThreadStartTimeoutMs,
|
|
510
|
-
defaultThreadStartTimeoutMs(systemPrompt),
|
|
511
|
-
{ min: 1, max: Number.MAX_SAFE_INTEGER },
|
|
512
|
-
),
|
|
513
|
-
attempts: integerOption(
|
|
514
|
-
options.codexThreadStartAttempts,
|
|
515
|
-
DEFAULT_THREAD_START_ATTEMPTS,
|
|
516
|
-
{ min: 1, max: 5 },
|
|
517
|
-
),
|
|
518
|
-
backoffMs: integerOption(
|
|
519
|
-
options.codexThreadStartBackoffMs,
|
|
520
|
-
DEFAULT_THREAD_START_BACKOFF_MS,
|
|
521
|
-
{ min: 0, max: 300_000 },
|
|
522
|
-
),
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
function delay(ms, signal) {
|
|
527
|
-
const timeoutMs = Math.max(0, Number(ms) || 0);
|
|
528
|
-
if (!timeoutMs || signal?.aborted) return Promise.resolve();
|
|
529
|
-
return new Promise((resolve) => {
|
|
530
|
-
const timer = setTimeout(resolve, timeoutMs);
|
|
531
|
-
timer.unref?.();
|
|
532
|
-
signal?.addEventListener?.("abort", () => {
|
|
533
|
-
clearTimeout(timer);
|
|
534
|
-
resolve();
|
|
535
|
-
}, { once: true });
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// `thread/start.sandbox` selects only the thread-level filesystem class; it
|
|
540
|
-
// does not own network. `sandboxPolicyForRun()` supplies authoritative per-turn
|
|
541
|
-
// networkAccess, per the app-server 0.147.0 / gpt-5.6-sol retained-thread matrix.
|
|
542
|
-
function sandboxForRun(options) {
|
|
543
|
-
if (options.codexNoToolsProbe === true) return "read-only";
|
|
544
|
-
if (options.permissionMode === "bypassPermissions") return "danger-full-access";
|
|
545
|
-
if (options.permissionMode === "plan") return "read-only";
|
|
546
|
-
return "workspace-write";
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
function approvalPolicyForRun(options) {
|
|
550
|
-
// mono-agent channel turns are unattended: there is no interactive app-server
|
|
551
|
-
// approval UI on the other end of stdio. `never` lets Codex execute within the
|
|
552
|
-
// selected sandbox and deny escalations itself instead of waiting forever for
|
|
553
|
-
// a client response that cannot arrive.
|
|
554
|
-
return "never";
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
function sandboxPolicyForRun(options) {
|
|
558
|
-
if (options.codexNoToolsProbe === true) return { type: "readOnly", networkAccess: false };
|
|
559
|
-
if (options.permissionMode === "bypassPermissions") return { type: "dangerFullAccess" };
|
|
560
|
-
// App-server 0.147.0 / gpt-5.6-sol retained-thread matrix proved per-turn
|
|
561
|
-
// networkAccess authoritative in both directions.
|
|
562
|
-
const networkAccess = options.codexSandboxNetworkAccess === true;
|
|
563
|
-
if (options.permissionMode === "plan") return { type: "readOnly", networkAccess };
|
|
564
|
-
return {
|
|
565
|
-
type: "workspaceWrite",
|
|
566
|
-
writableRoots: [options.cwd || process.cwd()],
|
|
567
|
-
networkAccess,
|
|
568
|
-
excludeTmpdirEnvVar: false,
|
|
569
|
-
excludeSlashTmp: false,
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
function codexToolPolicyProblem(options) {
|
|
574
|
-
const allowedTools = Array.isArray(options.allowedTools) ? options.allowedTools : null;
|
|
575
|
-
const disallowedTools = Array.isArray(options.disallowedTools) ? options.disallowedTools : [];
|
|
576
|
-
if (options.codexNoToolsProbe === true) {
|
|
577
|
-
const mcpServerCount = Object.keys(options.mcpServers || {}).length;
|
|
578
|
-
if (allowedTools?.length === 0 && disallowedTools.length === 0 && mcpServerCount === 0 && options.sessionKeepAlive !== true) {
|
|
579
|
-
return null;
|
|
580
|
-
}
|
|
581
|
-
return "Codex no-tool probe mode requires an empty tool policy, no MCP servers, and a disposable session.";
|
|
582
|
-
}
|
|
583
|
-
return isAllowAllToolPolicy(allowedTools, disallowedTools)
|
|
584
|
-
? null
|
|
585
|
-
: "Direct Codex cannot enforce allowedTools/disallowedTools. Use allow-all-only (omit allowedTools or include \"*\", with no disallowedTools) or another runtime.";
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
const CODEX_NO_TOOL_ACTION_ITEMS = new Set([
|
|
589
|
-
"commandExecution",
|
|
590
|
-
"fileChange",
|
|
591
|
-
"mcpToolCall",
|
|
592
|
-
"dynamicToolCall",
|
|
593
|
-
"collabAgentToolCall",
|
|
594
|
-
"collabToolCall",
|
|
595
|
-
"subAgentActivity",
|
|
596
|
-
"webSearch",
|
|
597
|
-
"imageView",
|
|
598
|
-
"sleep",
|
|
599
|
-
"imageGeneration",
|
|
600
|
-
]);
|
|
601
|
-
|
|
602
|
-
const CODEX_NO_TOOL_REQUEST_METHODS = new Set([
|
|
603
|
-
"item/commandExecution/requestApproval",
|
|
604
|
-
"item/fileChange/requestApproval",
|
|
605
|
-
"item/tool/requestUserInput",
|
|
606
|
-
"item/permissions/requestApproval",
|
|
607
|
-
"item/tool/call",
|
|
608
|
-
"mcpServer/elicitation/request",
|
|
609
|
-
"applyPatchApproval",
|
|
610
|
-
"execCommandApproval",
|
|
611
|
-
]);
|
|
612
|
-
|
|
613
|
-
// These protocol items are conversation or lifecycle records rather than
|
|
614
|
-
// actionable child work. Keep them out of the unknown-item fallback: message
|
|
615
|
-
// and reasoning content has dedicated handling, while the remaining records
|
|
616
|
-
// would otherwise create misleading tool rows.
|
|
617
|
-
const CODEX_PASSIVE_CHILD_ITEM_TYPES = new Set([
|
|
618
|
-
"userMessage",
|
|
619
|
-
"hookPrompt",
|
|
620
|
-
"agentMessage",
|
|
621
|
-
"plan",
|
|
622
|
-
"reasoning",
|
|
623
|
-
"enteredReviewMode",
|
|
624
|
-
"exitedReviewMode",
|
|
625
|
-
"contextCompaction",
|
|
626
|
-
"subAgentActivity",
|
|
627
|
-
]);
|
|
628
|
-
|
|
629
|
-
function codexMcpConfig(mcpServers = {}) {
|
|
630
|
-
const servers = {};
|
|
631
|
-
const invalidNames = [];
|
|
632
|
-
for (const [name, cfg] of Object.entries(mcpServers || {})) {
|
|
633
|
-
if (!/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
634
|
-
invalidNames.push(name);
|
|
635
|
-
continue;
|
|
636
|
-
}
|
|
637
|
-
if (cfg?.command) {
|
|
638
|
-
servers[name] = {
|
|
639
|
-
command: cfg.command,
|
|
640
|
-
...(Array.isArray(cfg.args) ? { args: cfg.args } : {}),
|
|
641
|
-
...(cfg.env && typeof cfg.env === "object" ? { env: cfg.env } : {}),
|
|
642
|
-
...(cfg.cwd && typeof cfg.cwd === "string" ? { cwd: cfg.cwd } : {}),
|
|
643
|
-
enabled: true,
|
|
644
|
-
required: false,
|
|
645
|
-
};
|
|
646
|
-
} else if (cfg?.url) {
|
|
647
|
-
servers[name] = {
|
|
648
|
-
url: cfg.url,
|
|
649
|
-
...(cfg.headers && typeof cfg.headers === "object" ? { http_headers: cfg.headers } : {}),
|
|
650
|
-
enabled: true,
|
|
651
|
-
required: false,
|
|
652
|
-
};
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
return { servers, invalidNames };
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
function codexMcpServerNameProblem(invalidNames) {
|
|
659
|
-
if (!invalidNames.length) return null;
|
|
660
|
-
const names = invalidNames.map((name) => JSON.stringify(name)).join(", ");
|
|
661
|
-
return `Direct Codex cannot configure MCP server ${invalidNames.length === 1 ? "name" : "names"} ${names}. Rename ${invalidNames.length === 1 ? "it" : "them"} to use only ASCII letters, numbers, "_", or "-".`;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
function stableConfigJson(value) {
|
|
665
|
-
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
666
|
-
if (Array.isArray(value)) return `[${value.map((entry) => stableConfigJson(entry)).join(",")}]`;
|
|
667
|
-
return `{${Object.keys(value)
|
|
668
|
-
.filter((key) => value[key] !== undefined)
|
|
669
|
-
.sort()
|
|
670
|
-
.map((key) => `${JSON.stringify(key)}:${stableConfigJson(value[key])}`)
|
|
671
|
-
.join(",")}}`;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
function codexMcpConfigFingerprint(config) {
|
|
675
|
-
return createHash("sha256").update(stableConfigJson(config)).digest("hex");
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
function codexConfiguredMcpApprovalResponse(request, configuredMcpServerNames) {
|
|
679
|
-
const params = request?.params;
|
|
680
|
-
if (
|
|
681
|
-
request?.method !== "mcpServer/elicitation/request"
|
|
682
|
-
|| params?._meta?.codex_approval_kind !== "mcp_tool_call"
|
|
683
|
-
|| typeof params?.serverName !== "string"
|
|
684
|
-
|| !configuredMcpServerNames.has(params.serverName)
|
|
685
|
-
) {
|
|
686
|
-
return null;
|
|
687
|
-
}
|
|
688
|
-
return { action: "accept", content: {}, _meta: null };
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
function codexErrorMessage(error) {
|
|
692
|
-
if (!error) return "Codex app-server error";
|
|
693
|
-
if (typeof error === "string") return error;
|
|
694
|
-
const data = error.data || error.error || {};
|
|
695
|
-
const info = data.info || data.code || error.code;
|
|
696
|
-
if (info && typeof info === "object" && "activeTurnNotSteerable" in info) {
|
|
697
|
-
return "Codex active turn is not steerable";
|
|
698
|
-
}
|
|
699
|
-
return error.message || data.message || safeDiagnosticString(error);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
function isActiveTurnNotSteerable(error) {
|
|
703
|
-
const info = error?.data?.info || error?.data?.error?.info || error?.info;
|
|
704
|
-
return info === "activeTurnNotSteerable" || Boolean(info?.activeTurnNotSteerable);
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
function isNoActiveTurnToSteer(error) {
|
|
708
|
-
return /no active turn to steer/i.test(codexErrorMessage(error));
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
function isCodexRequestTimeout(error, method = null) {
|
|
712
|
-
return error?.code === "CODEX_APP_SERVER_REQUEST_TIMEOUT"
|
|
713
|
-
&& (!method || error.method === method);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
function codexErrorDiagnostics(error, sensitiveValues = []) {
|
|
717
|
-
if (!error) return {};
|
|
718
|
-
if (isCodexRequestTimeout(error)) {
|
|
719
|
-
return {
|
|
720
|
-
codex_error_code: "codex_app_server_request_timeout",
|
|
721
|
-
codex_request_method: error.method ? sanitizeCodexDiagnostic(error.method, sensitiveValues, 256) : null,
|
|
722
|
-
codex_request_timeout_ms: error.timeoutMs || null,
|
|
723
|
-
...(error.stderrTail
|
|
724
|
-
? { stderr_tail: sanitizeCodexDiagnostic(error.stderrTail, sensitiveValues) }
|
|
725
|
-
: {}),
|
|
726
|
-
};
|
|
727
|
-
}
|
|
728
|
-
return error.code
|
|
729
|
-
? { codex_error_code: sanitizeCodexDiagnostic(error.code, sensitiveValues, 256) }
|
|
730
|
-
: {};
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
function withoutCodexRequestErrorDiagnostics(diagnostics) {
|
|
734
|
-
const {
|
|
735
|
-
codex_error_code: _codexErrorCode,
|
|
736
|
-
codex_request_method: _codexRequestMethod,
|
|
737
|
-
codex_request_timeout_ms: _codexRequestTimeoutMs,
|
|
738
|
-
stderr_tail: _stderrTail,
|
|
739
|
-
...rest
|
|
740
|
-
} = diagnostics || {};
|
|
741
|
-
return rest;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
/**
|
|
745
|
-
* @param {{command?: string, args?: string[], cwd?: any, env?: any, redactionValues?: string[], onNotification?: (msg: any) => void, onServerRequest?: (msg: any) => Promise<any> | any, shutdownGraceMs?: number, killGraceMs?: number}} [options]
|
|
746
|
-
*/
|
|
747
|
-
export function createCodexAppServerClient({
|
|
748
|
-
command = "codex",
|
|
749
|
-
// project_doc_max_bytes=0 keeps codex from injecting its own project docs;
|
|
750
|
-
// the host supplies the full context through developerInstructions.
|
|
751
|
-
args = CODEX_APP_SERVER_ISOLATED_ARGS,
|
|
752
|
-
cwd,
|
|
753
|
-
env = {},
|
|
754
|
-
redactionValues = [],
|
|
755
|
-
onNotification = () => {},
|
|
756
|
-
onServerRequest = (message) => {
|
|
757
|
-
throw new Error(`Unsupported Codex app-server request: ${String(message?.method || "unknown")}`);
|
|
758
|
-
},
|
|
759
|
-
shutdownGraceMs = CODEX_SHUTDOWN_GRACE_MS,
|
|
760
|
-
killGraceMs = CODEX_KILL_GRACE_MS,
|
|
761
|
-
} = {}) {
|
|
762
|
-
const childEnv = { ...process.env, ...env };
|
|
763
|
-
const configuredSensitiveValues = new Set();
|
|
764
|
-
for (const value of redactionValues) {
|
|
765
|
-
addOpaqueSensitiveValue(configuredSensitiveValues, value, { splitCredentials: true });
|
|
766
|
-
}
|
|
767
|
-
const sensitiveValues = [...new Set([
|
|
768
|
-
...sensitiveEnvironmentValues(childEnv),
|
|
769
|
-
...configuredSensitiveValues,
|
|
770
|
-
])].sort((left, right) => right.length - left.length);
|
|
771
|
-
const child = spawn(command, args, {
|
|
772
|
-
cwd,
|
|
773
|
-
env: childEnv,
|
|
774
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
775
|
-
});
|
|
776
|
-
const pending = new Map();
|
|
777
|
-
const stderrTail = createCodexStderrTail(sensitiveValues);
|
|
778
|
-
const shutdownTimers = new Set();
|
|
779
|
-
let nextId = 1;
|
|
780
|
-
let closed = false;
|
|
781
|
-
let processSettled = false;
|
|
782
|
-
let closing = false;
|
|
783
|
-
/** @type {Promise<void> | null} */
|
|
784
|
-
let closePromise = null;
|
|
785
|
-
let resolveClosed;
|
|
786
|
-
const closedPromise = new Promise((resolve) => { resolveClosed = resolve; });
|
|
787
|
-
|
|
788
|
-
function rejectAll(err) {
|
|
789
|
-
for (const { reject, timer } of pending.values()) {
|
|
790
|
-
clearTimeout(timer);
|
|
791
|
-
reject(err);
|
|
792
|
-
}
|
|
793
|
-
pending.clear();
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
function safeTransportError(error) {
|
|
797
|
-
const message = sanitizeCodexDiagnostic(error?.message || error || "codex app-server failed", sensitiveValues);
|
|
798
|
-
const safe = new Error(message || "codex app-server failed");
|
|
799
|
-
return error?.code === undefined ? safe : Object.assign(safe, { code: error.code });
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
function onStderrData(chunk) {
|
|
803
|
-
stderrTail.push(chunk);
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
child.stderr.on("data", onStderrData);
|
|
807
|
-
|
|
808
|
-
function writeProtocolMessage(payload) {
|
|
809
|
-
if (closed || child.stdin?.destroyed || child.stdin?.writableEnded) return;
|
|
810
|
-
child.stdin.write(`${JSON.stringify(payload)}\n`, () => {});
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
function respondToServerRequest(message) {
|
|
814
|
-
// Preserve request visibility for the normal event/fail-fast path, then
|
|
815
|
-
// always settle the JSON-RPC request. Never leave the app-server blocked on
|
|
816
|
-
// an inbound request that this unattended client cannot service.
|
|
817
|
-
const safeMessage = redactCodexPayload(message, sensitiveValues);
|
|
818
|
-
onNotification(safeMessage);
|
|
819
|
-
Promise.resolve()
|
|
820
|
-
.then(() => onServerRequest(safeMessage))
|
|
821
|
-
.then(
|
|
822
|
-
(result) => writeProtocolMessage({ id: message.id, result: result ?? {} }),
|
|
823
|
-
() => writeProtocolMessage({
|
|
824
|
-
id: message.id,
|
|
825
|
-
error: { code: -32601, message: `Unsupported Codex app-server request: ${String(message.method || "unknown")}` },
|
|
826
|
-
}),
|
|
827
|
-
);
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
const rl = createInterface({ input: child.stdout });
|
|
831
|
-
function onLine(line) {
|
|
832
|
-
if (!line.trim()) return;
|
|
833
|
-
let message;
|
|
834
|
-
try {
|
|
835
|
-
message = JSON.parse(line);
|
|
836
|
-
} catch {
|
|
837
|
-
onNotification({
|
|
838
|
-
method: "warning",
|
|
839
|
-
params: {
|
|
840
|
-
message: sanitizeCodexDiagnostic(
|
|
841
|
-
`Malformed Codex app-server output: ${line}`,
|
|
842
|
-
sensitiveValues,
|
|
843
|
-
),
|
|
844
|
-
},
|
|
845
|
-
});
|
|
846
|
-
return;
|
|
847
|
-
}
|
|
848
|
-
if (Object.prototype.hasOwnProperty.call(message, "id") && (message.result !== undefined || message.error !== undefined)) {
|
|
849
|
-
const entry = pending.get(message.id);
|
|
850
|
-
if (!entry) return;
|
|
851
|
-
pending.delete(message.id);
|
|
852
|
-
clearTimeout(entry.timer);
|
|
853
|
-
if (message.error) {
|
|
854
|
-
const responseError = sanitizeCodexResponseError(message.error, sensitiveValues);
|
|
855
|
-
entry.reject(Object.assign(
|
|
856
|
-
new Error(sanitizeCodexDiagnostic(codexErrorMessage(responseError), sensitiveValues)),
|
|
857
|
-
{ responseError },
|
|
858
|
-
));
|
|
859
|
-
}
|
|
860
|
-
else entry.resolve(message.result);
|
|
861
|
-
return;
|
|
862
|
-
}
|
|
863
|
-
if (Object.prototype.hasOwnProperty.call(message, "id") && message.method) {
|
|
864
|
-
respondToServerRequest(message);
|
|
865
|
-
return;
|
|
866
|
-
}
|
|
867
|
-
if (message.method) onNotification(sanitizeCodexNotification(message, sensitiveValues));
|
|
868
|
-
}
|
|
869
|
-
rl.on("line", onLine);
|
|
870
|
-
|
|
871
|
-
function cleanupTransport() {
|
|
872
|
-
for (const timer of shutdownTimers) clearTimeout(timer);
|
|
873
|
-
shutdownTimers.clear();
|
|
874
|
-
rl.off("line", onLine);
|
|
875
|
-
try { rl.close(); } catch {}
|
|
876
|
-
child.stderr?.off?.("data", onStderrData);
|
|
877
|
-
child.off("error", onChildError);
|
|
878
|
-
child.off("close", onChildClose);
|
|
879
|
-
try { child.stdin?.destroy?.(); } catch {}
|
|
880
|
-
try { child.stdout?.destroy?.(); } catch {}
|
|
881
|
-
try { child.stderr?.destroy?.(); } catch {}
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
function settleClosed(error) {
|
|
885
|
-
if (processSettled) return;
|
|
886
|
-
processSettled = true;
|
|
887
|
-
closed = true;
|
|
888
|
-
rejectAll(error);
|
|
889
|
-
cleanupTransport();
|
|
890
|
-
resolveClosed(error);
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
function onChildError(error) {
|
|
894
|
-
const safe = safeTransportError(error);
|
|
895
|
-
closed = true;
|
|
896
|
-
rejectAll(safe);
|
|
897
|
-
// A spawn failure has no live process and may not emit `close`. By contrast,
|
|
898
|
-
// ChildProcess also emits `error` when signaling a live child fails (EPERM,
|
|
899
|
-
// ESRCH races). Only `close` proves that such a process actually exited.
|
|
900
|
-
if (child.pid === undefined) {
|
|
901
|
-
settleClosed(safe);
|
|
902
|
-
return;
|
|
903
|
-
}
|
|
904
|
-
if (!closing) void close();
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
function onChildClose(code, signal) {
|
|
908
|
-
if (closing) {
|
|
909
|
-
settleClosed(new Error("codex app-server closed"));
|
|
910
|
-
return;
|
|
911
|
-
}
|
|
912
|
-
const summary = signal === null
|
|
913
|
-
? `codex app-server exited ${code ?? "unknown"}`
|
|
914
|
-
: `codex app-server terminated by ${signal}`;
|
|
915
|
-
const detail = stderrTail.toString();
|
|
916
|
-
settleClosed(new Error(detail ? `${summary}: ${detail}` : summary));
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
child.on("error", onChildError);
|
|
920
|
-
child.once("close", onChildClose);
|
|
921
|
-
|
|
922
|
-
function request(method, params, { timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS } = {}) {
|
|
923
|
-
if (closed || child.stdin?.destroyed || child.stdin?.writableEnded) {
|
|
924
|
-
return Promise.reject(new Error("codex app-server is not running"));
|
|
925
|
-
}
|
|
926
|
-
const id = nextId++;
|
|
927
|
-
const payload = { id, method, params };
|
|
928
|
-
return new Promise((resolve, reject) => {
|
|
929
|
-
const timer = setTimeout(() => {
|
|
930
|
-
pending.delete(id);
|
|
931
|
-
reject(Object.assign(new Error(`codex app-server request timed out: ${method}`), {
|
|
932
|
-
code: "CODEX_APP_SERVER_REQUEST_TIMEOUT",
|
|
933
|
-
method,
|
|
934
|
-
timeoutMs,
|
|
935
|
-
stderrTail: stderrTail.toString(),
|
|
936
|
-
}));
|
|
937
|
-
}, timeoutMs);
|
|
938
|
-
timer.unref?.();
|
|
939
|
-
pending.set(id, { resolve, reject, timer });
|
|
940
|
-
child.stdin.write(`${JSON.stringify(payload)}\n`, (err) => {
|
|
941
|
-
if (!err) return;
|
|
942
|
-
pending.delete(id);
|
|
943
|
-
clearTimeout(timer);
|
|
944
|
-
reject(safeTransportError(err));
|
|
945
|
-
});
|
|
946
|
-
});
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
function waitForProcessClose(timeoutMs) {
|
|
950
|
-
if (processSettled) return Promise.resolve(true);
|
|
951
|
-
return new Promise((resolve) => {
|
|
952
|
-
let settled = false;
|
|
953
|
-
let timer;
|
|
954
|
-
const finish = (didClose) => {
|
|
955
|
-
if (settled) return;
|
|
956
|
-
settled = true;
|
|
957
|
-
if (timer !== undefined) {
|
|
958
|
-
clearTimeout(timer);
|
|
959
|
-
shutdownTimers.delete(timer);
|
|
960
|
-
}
|
|
961
|
-
resolve(didClose);
|
|
962
|
-
};
|
|
963
|
-
timer = setTimeout(() => finish(false), timeoutMs);
|
|
964
|
-
timer.unref?.();
|
|
965
|
-
shutdownTimers.add(timer);
|
|
966
|
-
closedPromise.then(() => finish(true));
|
|
967
|
-
});
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
function close() {
|
|
971
|
-
if (closePromise !== null) return closePromise;
|
|
972
|
-
closePromise = (async () => {
|
|
973
|
-
closing = true;
|
|
974
|
-
closed = true;
|
|
975
|
-
rejectAll(new Error("codex app-server closed"));
|
|
976
|
-
if (processSettled) return;
|
|
977
|
-
|
|
978
|
-
try { child.stdin?.end?.(); } catch {}
|
|
979
|
-
try { child.kill("SIGTERM"); } catch {}
|
|
980
|
-
if (await waitForProcessClose(boundedTimeout(shutdownGraceMs, CODEX_SHUTDOWN_GRACE_MS))) return;
|
|
981
|
-
|
|
982
|
-
try { child.kill("SIGKILL"); } catch {}
|
|
983
|
-
if (await waitForProcessClose(boundedTimeout(killGraceMs, CODEX_KILL_GRACE_MS))) return;
|
|
984
|
-
|
|
985
|
-
try { child.unref?.(); } catch {}
|
|
986
|
-
settleClosed(new Error("codex app-server did not exit after SIGKILL"));
|
|
987
|
-
})();
|
|
988
|
-
return closePromise;
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
return { request, close, child, closed: closedPromise };
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
function mapThreadItem(method, item) {
|
|
995
|
-
if (!item || typeof item !== "object") return null;
|
|
996
|
-
const type = method.endsWith("started") ? "item.started" : "item.completed";
|
|
997
|
-
if (item.type === "agentMessage") {
|
|
998
|
-
return { type, item: { type: "agent_message", id: item.id, text: item.text || "" } };
|
|
999
|
-
}
|
|
1000
|
-
if (item.type === "commandExecution") {
|
|
1001
|
-
return {
|
|
1002
|
-
type,
|
|
1003
|
-
item: {
|
|
1004
|
-
type: "command_execution",
|
|
1005
|
-
id: item.id,
|
|
1006
|
-
command: item.command,
|
|
1007
|
-
aggregated_output: item.aggregatedOutput || "",
|
|
1008
|
-
exit_code: item.exitCode,
|
|
1009
|
-
status: item.status,
|
|
1010
|
-
},
|
|
1011
|
-
};
|
|
1012
|
-
}
|
|
1013
|
-
if (item.type === "fileChange") {
|
|
1014
|
-
return {
|
|
1015
|
-
type,
|
|
1016
|
-
item: {
|
|
1017
|
-
type: "file_change",
|
|
1018
|
-
id: item.id,
|
|
1019
|
-
changes: item.changes || [],
|
|
1020
|
-
status: item.status,
|
|
1021
|
-
},
|
|
1022
|
-
};
|
|
1023
|
-
}
|
|
1024
|
-
if (item.type === "mcpToolCall") {
|
|
1025
|
-
return {
|
|
1026
|
-
type,
|
|
1027
|
-
item: {
|
|
1028
|
-
type: "mcp_tool_call",
|
|
1029
|
-
id: item.id,
|
|
1030
|
-
server: item.server,
|
|
1031
|
-
tool: item.tool,
|
|
1032
|
-
arguments: item.arguments,
|
|
1033
|
-
result: item.result,
|
|
1034
|
-
error: item.error,
|
|
1035
|
-
status: item.status,
|
|
1036
|
-
},
|
|
1037
|
-
};
|
|
1038
|
-
}
|
|
1039
|
-
if (item.type === "collabAgentToolCall" || item.type === "collabToolCall") {
|
|
1040
|
-
const name = `codex_${item.tool || "subagent"}`;
|
|
1041
|
-
if (method.endsWith("started")) {
|
|
1042
|
-
return {
|
|
1043
|
-
type: "assistant",
|
|
1044
|
-
message: {
|
|
1045
|
-
content: [{
|
|
1046
|
-
type: "tool_use",
|
|
1047
|
-
id: item.id,
|
|
1048
|
-
name,
|
|
1049
|
-
input: {
|
|
1050
|
-
prompt: item.prompt,
|
|
1051
|
-
model: item.model,
|
|
1052
|
-
reasoningEffort: item.reasoningEffort,
|
|
1053
|
-
receiverThreadIds: item.receiverThreadIds
|
|
1054
|
-
|| [item.newThreadId, item.receiverThreadId].filter(Boolean),
|
|
1055
|
-
},
|
|
1056
|
-
}],
|
|
1057
|
-
},
|
|
1058
|
-
};
|
|
1059
|
-
}
|
|
1060
|
-
return {
|
|
1061
|
-
type: "user",
|
|
1062
|
-
message: {
|
|
1063
|
-
content: [{
|
|
1064
|
-
type: "tool_result",
|
|
1065
|
-
tool_use_id: item.id,
|
|
1066
|
-
content: {
|
|
1067
|
-
status: item.status,
|
|
1068
|
-
receiverThreadIds: item.receiverThreadIds
|
|
1069
|
-
|| [item.newThreadId, item.receiverThreadId].filter(Boolean),
|
|
1070
|
-
agentsStates: item.agentsStates || [],
|
|
1071
|
-
...(item.error ? { error: item.error } : {}),
|
|
1072
|
-
},
|
|
1073
|
-
is_error: item.status === "failed" || Boolean(item.error),
|
|
1074
|
-
tool_lifecycle: toolLifecycleMetadata(item.status === "failed" || Boolean(item.error)
|
|
1075
|
-
? { state: "error", failure_kind: "runtime_error", detail_code: "codex_collab_failed" }
|
|
1076
|
-
: { state: "success" }),
|
|
1077
|
-
}],
|
|
1078
|
-
},
|
|
1079
|
-
};
|
|
1080
|
-
}
|
|
1081
|
-
if (item.type === "dynamicToolCall") {
|
|
1082
|
-
if (method.endsWith("started")) {
|
|
1083
|
-
return {
|
|
1084
|
-
type: "assistant",
|
|
1085
|
-
message: {
|
|
1086
|
-
content: [{
|
|
1087
|
-
type: "tool_use",
|
|
1088
|
-
id: item.id,
|
|
1089
|
-
name: item.namespace ? `${item.namespace}__${item.tool}` : item.tool,
|
|
1090
|
-
input: item.arguments || {},
|
|
1091
|
-
}],
|
|
1092
|
-
},
|
|
1093
|
-
};
|
|
1094
|
-
}
|
|
1095
|
-
return {
|
|
1096
|
-
type: "user",
|
|
1097
|
-
message: {
|
|
1098
|
-
content: [{
|
|
1099
|
-
type: "tool_result",
|
|
1100
|
-
tool_use_id: item.id,
|
|
1101
|
-
content: item.contentItems || item.result || item.error || "",
|
|
1102
|
-
is_error: item.status === "failed" || item.success === false || Boolean(item.error),
|
|
1103
|
-
tool_lifecycle: toolLifecycleMetadata(item.status === "failed" || item.success === false || Boolean(item.error)
|
|
1104
|
-
? { state: "error", failure_kind: "runtime_error", detail_code: "codex_dynamic_tool_failed" }
|
|
1105
|
-
: { state: "success" }),
|
|
1106
|
-
}],
|
|
1107
|
-
},
|
|
1108
|
-
};
|
|
1109
|
-
}
|
|
1110
|
-
if (item.type === "reasoning") {
|
|
1111
|
-
const text = [...(item.summary || []), ...(item.content || [])].join("\n").trim();
|
|
1112
|
-
return text ? { type: "assistant", message: { content: [{ type: "thinking", text }] } } : null;
|
|
1113
|
-
}
|
|
1114
|
-
return null;
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
function normalizedCollabTool(tool) {
|
|
1118
|
-
return String(tool || "")
|
|
1119
|
-
.replace(/[^A-Za-z0-9]+/gu, "")
|
|
1120
|
-
.toLowerCase();
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
function isCodexCollabItem(item) {
|
|
1124
|
-
return item?.type === "collabAgentToolCall" || item?.type === "collabToolCall";
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
function codexCollabReceiverEntries(item) {
|
|
1128
|
-
const entries = [];
|
|
1129
|
-
const seen = new Set();
|
|
1130
|
-
const add = (nativeId, source = {}) => {
|
|
1131
|
-
const id = typeof nativeId === "string" ? nativeId.trim() : "";
|
|
1132
|
-
if (!id || seen.has(id)) return;
|
|
1133
|
-
seen.add(id);
|
|
1134
|
-
const agentPath = typeof source.agentPath === "string" && source.agentPath.trim()
|
|
1135
|
-
? source.agentPath
|
|
1136
|
-
: undefined;
|
|
1137
|
-
const name = [source.name, source.nickname, source.agentNickname, source.agentRole]
|
|
1138
|
-
.find((value) => typeof value === "string" && value.trim());
|
|
1139
|
-
entries.push({
|
|
1140
|
-
nativeId: id,
|
|
1141
|
-
...(agentPath === undefined ? {} : { agentPath }),
|
|
1142
|
-
...(name === undefined ? {} : { name }),
|
|
1143
|
-
});
|
|
1144
|
-
};
|
|
1145
|
-
|
|
1146
|
-
add(item?.newThreadId, item);
|
|
1147
|
-
add(item?.receiverThreadId, item);
|
|
1148
|
-
for (const id of item?.receiverThreadIds || []) add(id, item);
|
|
1149
|
-
for (const receiver of item?.receiverAgents || []) {
|
|
1150
|
-
add(receiver?.threadId ?? receiver?.id, receiver);
|
|
1151
|
-
}
|
|
1152
|
-
if (item?.agentStatus && typeof item.agentStatus === "object") {
|
|
1153
|
-
add(item.agentStatus.threadId ?? item.agentStatus.id, item.agentStatus);
|
|
1154
|
-
}
|
|
1155
|
-
for (const id of Object.keys(item?.agentsStates || {})) add(id, item?.agentsStates?.[id]);
|
|
1156
|
-
return entries;
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
function codexAgentName(agentPath, fallback = "codex") {
|
|
1160
|
-
const path = typeof agentPath === "string" ? agentPath.trim() : "";
|
|
1161
|
-
if (!path) return fallback;
|
|
1162
|
-
const segments = path.split("/").filter(Boolean);
|
|
1163
|
-
return segments.at(-1) || path;
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
function codexItemFailed(item) {
|
|
1167
|
-
const agentStatus = typeof item?.agentStatus === "string"
|
|
1168
|
-
? item.agentStatus
|
|
1169
|
-
: item?.agentStatus?.status;
|
|
1170
|
-
const status = String(item?.status || agentStatus || "").toLowerCase();
|
|
1171
|
-
const exitCode = item?.exitCode ?? item?.exit_code;
|
|
1172
|
-
return Boolean(
|
|
1173
|
-
item?.error
|
|
1174
|
-
|| item?.success === false
|
|
1175
|
-
|| status === "failed"
|
|
1176
|
-
|| status === "errored"
|
|
1177
|
-
|| status === "error"
|
|
1178
|
-
|| status === "interrupted"
|
|
1179
|
-
|| status === "cancelled"
|
|
1180
|
-
|| status === "notfound"
|
|
1181
|
-
|| (typeof exitCode === "number" && exitCode !== 0),
|
|
1182
|
-
);
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
function codexActivityContent(value) {
|
|
1186
|
-
if (typeof value === "string") return value;
|
|
1187
|
-
if (value === undefined || value === null) return "";
|
|
1188
|
-
try {
|
|
1189
|
-
return JSON.stringify(value);
|
|
1190
|
-
} catch {
|
|
1191
|
-
return String(value);
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
function codexChildToolName(item) {
|
|
1196
|
-
if (item?.type === "commandExecution") return "command_execution";
|
|
1197
|
-
if (item?.type === "mcpToolCall") {
|
|
1198
|
-
return item.server && item.tool ? `mcp__${item.server}__${item.tool}` : item.tool || "mcp_tool_call";
|
|
1199
|
-
}
|
|
1200
|
-
if (item?.type === "dynamicToolCall") {
|
|
1201
|
-
return item.namespace && item.tool ? `${item.namespace}__${item.tool}` : item.tool || "dynamic_tool_call";
|
|
1202
|
-
}
|
|
1203
|
-
return item?.tool || item?.type || "tool";
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
function codexItemDurationMs(item) {
|
|
1207
|
-
const value = Number(item?.durationMs ?? item?.duration_ms);
|
|
1208
|
-
return Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
function boundedCodexActivityItemId(value, limit = 256) {
|
|
1212
|
-
const id = String(value || "item");
|
|
1213
|
-
if (Buffer.byteLength(id) <= limit) return id;
|
|
1214
|
-
const digest = createHash("sha256").update(id).digest("hex");
|
|
1215
|
-
const suffix = `:${digest}`;
|
|
1216
|
-
return `${utf8Head(id, Math.max(0, limit - Buffer.byteLength(suffix)))}${suffix}`;
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
function usageFromTokenUsage(tokenUsage) {
|
|
1220
|
-
const last = tokenUsage?.last || tokenUsage?.total || {};
|
|
1221
|
-
return {
|
|
1222
|
-
input_tokens: last.inputTokens ?? null,
|
|
1223
|
-
output_tokens: last.outputTokens ?? null,
|
|
1224
|
-
cache_read_tokens: last.cachedInputTokens ?? null,
|
|
1225
|
-
};
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
function contextUsageFromTokenUsage(tokenUsage) {
|
|
1229
|
-
const last = tokenUsage?.last;
|
|
1230
|
-
const total = Number(last?.totalTokens);
|
|
1231
|
-
if (!last || !Number.isFinite(total) || total <= 0) return null;
|
|
1232
|
-
const input = Number(last.inputTokens) || 0;
|
|
1233
|
-
const cachedInput = Number(last.cachedInputTokens) || 0;
|
|
1234
|
-
const output = Number(last.outputTokens) || 0;
|
|
1235
|
-
const reasoning = Number(last.reasoningOutputTokens) || 0;
|
|
1236
|
-
const contextWindow = Number(tokenUsage?.modelContextWindow) || 0;
|
|
1237
|
-
return {
|
|
1238
|
-
tokens: {
|
|
1239
|
-
input: Math.max(0, input - cachedInput),
|
|
1240
|
-
cachedInput,
|
|
1241
|
-
output,
|
|
1242
|
-
reasoning,
|
|
1243
|
-
total,
|
|
1244
|
-
},
|
|
1245
|
-
...(contextWindow > 0 ? { contextWindow } : {}),
|
|
1246
|
-
};
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
const noopNotificationHandler = () => {};
|
|
1250
|
-
const rejectIdleServerRequest = (request) => {
|
|
1251
|
-
throw new Error(
|
|
1252
|
-
`Codex app-server request arrived while its provider session was idle: ${String(request?.method || "unknown")}`,
|
|
1253
|
-
);
|
|
1254
|
-
};
|
|
1255
|
-
|
|
1256
|
-
async function closeCodexClient(client) {
|
|
1257
|
-
if (!client?.close) return;
|
|
1258
|
-
try {
|
|
1259
|
-
await client.close();
|
|
1260
|
-
} catch {
|
|
1261
|
-
// Teardown is best-effort at result boundaries, but the returned promise is
|
|
1262
|
-
// always observed so a custom client cannot create an unhandled rejection.
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
// Live keep-alive sessions keyed by codex thread id.
|
|
1267
|
-
const codexSessions = createSessionRegistry({
|
|
1268
|
-
isBusy: (entry) => entry.busy === true,
|
|
1269
|
-
onEvict: async (entry) => {
|
|
1270
|
-
await closeCodexClient(entry.client);
|
|
1271
|
-
},
|
|
1272
|
-
});
|
|
1273
|
-
// Synchronous liveness primitives over the registry. Codex only needs the
|
|
1274
|
-
// await-free busy claim (its resume handling is simpler than pi's — no durable
|
|
1275
|
-
// reopen / create-on-miss reservation), so it consumes claim(); its keep-alive
|
|
1276
|
-
// register + deletes stay direct registry ops.
|
|
1277
|
-
const codexLiveness = createSessionLiveness(codexSessions);
|
|
1278
|
-
|
|
1279
|
-
export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
1280
|
-
const start = Date.now();
|
|
1281
|
-
const resolved = options.model;
|
|
1282
|
-
const requestedReference = resolved?.reference || `codex:${resolved?.model || ""}`;
|
|
1283
|
-
// Resolve every credential-bearing value before the app-server client is
|
|
1284
|
-
// constructed. The same set protects transport errors and provider events,
|
|
1285
|
-
// including MCP servers whose custom env/header names are not recognizable
|
|
1286
|
-
// through key-name heuristics.
|
|
1287
|
-
const sensitiveValues = codexRequestSensitiveValues(options);
|
|
1288
|
-
const safeDiagnostic = (value, limit) => sanitizeCodexDiagnostic(value, sensitiveValues, limit);
|
|
1289
|
-
const safeResponseError = (error) => sanitizeCodexResponseError(error, sensitiveValues);
|
|
1290
|
-
// Test seam: lets tests drive the bridge with a stub app-server client.
|
|
1291
|
-
const makeClient = options.codexClientFactory || createCodexAppServerClient;
|
|
1292
|
-
const keepAlive = options.sessionKeepAlive === true;
|
|
1293
|
-
const noToolsProbe = options.codexNoToolsProbe === true;
|
|
1294
|
-
const {
|
|
1295
|
-
servers: configuredMcpServers,
|
|
1296
|
-
invalidNames: invalidMcpServerNames,
|
|
1297
|
-
} = codexMcpConfig(options.mcpServers);
|
|
1298
|
-
const configuredMcpServerNameList = Object.keys(configuredMcpServers);
|
|
1299
|
-
const configuredMcpServerNames = new Set(configuredMcpServerNameList);
|
|
1300
|
-
const mcpConfigFingerprint = codexMcpConfigFingerprint(configuredMcpServers);
|
|
1301
|
-
// The bridge TTL is a backstop behind the host's session policy; the grace
|
|
1302
|
-
// keeps the host's lazy expiry firing first so eviction stays host-driven.
|
|
1303
|
-
const sessionTtlMs = Number.isFinite(Number(options.sessionIdleTimeoutMs))
|
|
1304
|
-
? Number(options.sessionIdleTimeoutMs) + 60_000
|
|
1305
|
-
: undefined;
|
|
1306
|
-
const resumeSessionId = typeof options.sessionId === "string" && options.sessionId.trim()
|
|
1307
|
-
? options.sessionId
|
|
1308
|
-
: null;
|
|
1309
|
-
const prompt = promptFromMessages(options.messages);
|
|
1310
|
-
// Effort arrives pre-normalized; codex has no "max" reasoning tier, so clamp
|
|
1311
|
-
// to its ceiling here instead of failing the app-server turn.
|
|
1312
|
-
const requestedEffort = typeof options.effort === "string" && options.effort.trim()
|
|
1313
|
-
? options.effort
|
|
1314
|
-
: null;
|
|
1315
|
-
const normalizedEffort = requestedEffort === "max" ? "xhigh" : requestedEffort;
|
|
1316
|
-
const events = [];
|
|
1317
|
-
const texts = [];
|
|
1318
|
-
const agentTextByItem = new Map();
|
|
1319
|
-
const childTextByItem = new Map();
|
|
1320
|
-
const childMessageDeltaCounts = new Map();
|
|
1321
|
-
const childReasoningDeltaCounts = new Map();
|
|
1322
|
-
const subagentGroupsBySpawnId = new Map();
|
|
1323
|
-
const subagentBindingsByThread = new Map();
|
|
1324
|
-
const pendingChildNotifications = new Map();
|
|
1325
|
-
const observedSubagentActivityItems = new Set();
|
|
1326
|
-
const compactionStatuses = new Map();
|
|
1327
|
-
const activeCompactions = new Map();
|
|
1328
|
-
const nativeCompactionTurnKeys = new Set();
|
|
1329
|
-
const legacyCompactionTurnKeys = new Set();
|
|
1330
|
-
let threadId = null;
|
|
1331
|
-
let activeTurnId = null;
|
|
1332
|
-
let actualModel = resolved?.model || null;
|
|
1333
|
-
let turnCompleted = false;
|
|
1334
|
-
let errorMessage = null;
|
|
1335
|
-
let failureKind = null;
|
|
1336
|
-
let usage = {};
|
|
1337
|
-
let codexDiagnostics = {};
|
|
1338
|
-
let noToolsViolation = null;
|
|
1339
|
-
let serverRequestViolation = null;
|
|
1340
|
-
let sentMcpServerNames = [];
|
|
1341
|
-
let resolveTurn;
|
|
1342
|
-
let resolveTurnReady;
|
|
1343
|
-
let resolveLiveInputStop;
|
|
1344
|
-
let turnReadyResolved = false;
|
|
1345
|
-
let liveInputStopped = false;
|
|
1346
|
-
let subagentCallIndex = 0;
|
|
1347
|
-
const fileChangeSnapshots = new Map();
|
|
1348
|
-
const codexItemContext = {
|
|
1349
|
-
fileChangePayload: (raw) => createFileChangePayload(raw, {
|
|
1350
|
-
cwd: options.cwd || process.cwd(),
|
|
1351
|
-
snapshots: fileChangeSnapshots,
|
|
1352
|
-
}),
|
|
1353
|
-
};
|
|
1354
|
-
const turnDone = new Promise((resolve) => { resolveTurn = resolve; });
|
|
1355
|
-
const turnReady = new Promise((resolve) => { resolveTurnReady = resolve; });
|
|
1356
|
-
const liveInputStop = new Promise((resolve) => { resolveLiveInputStop = resolve; });
|
|
1357
|
-
|
|
1358
|
-
function stopLiveInput() {
|
|
1359
|
-
if (liveInputStopped) return;
|
|
1360
|
-
liveInputStopped = true;
|
|
1361
|
-
resolveLiveInputStop();
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
function setActiveTurnId(turnId, { steerReady = false } = {}) {
|
|
1365
|
-
activeTurnId = turnId || activeTurnId;
|
|
1366
|
-
if (steerReady && !turnReadyResolved && threadId && activeTurnId) {
|
|
1367
|
-
turnReadyResolved = true;
|
|
1368
|
-
resolveTurnReady();
|
|
1369
|
-
}
|
|
1370
|
-
// An abort that fired before the turn id was known could not interrupt;
|
|
1371
|
-
// deliver it as soon as the turn becomes addressable.
|
|
1372
|
-
if (abortRequested && !interruptSent && threadId && activeTurnId) {
|
|
1373
|
-
interruptSent = true;
|
|
1374
|
-
client?.request("turn/interrupt", { threadId, turnId: activeTurnId }).catch(() => {});
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
function emitEvent(event) {
|
|
1379
|
-
if (!event) return;
|
|
1380
|
-
const safeEvent = redactCodexPayload(event, sensitiveValues);
|
|
1381
|
-
events.push(safeEvent);
|
|
1382
|
-
options.onEvent?.(safeEvent);
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
function invokeLiveInputCallback(message, callbackName, ...args) {
|
|
1386
|
-
const callback = message?.[callbackName];
|
|
1387
|
-
if (typeof callback !== "function") return;
|
|
1388
|
-
try {
|
|
1389
|
-
callback.apply(message, args);
|
|
1390
|
-
} catch (err) {
|
|
1391
|
-
const detail = safeDiagnostic(err, 512);
|
|
1392
|
-
emitEvent({
|
|
1393
|
-
type: "runtime_warning",
|
|
1394
|
-
warning_kind: "live_input_callback_failed",
|
|
1395
|
-
message: safeDiagnostic(`Live-input ${callbackName} callback failed: ${detail}`, 1_024),
|
|
1396
|
-
});
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
const compactionTurnKey = (params = {}) => `${params.threadId || threadId || "thread"}:${params.turnId || activeTurnId || "turn"}`;
|
|
1401
|
-
|
|
1402
|
-
/**
|
|
1403
|
-
* @param {{operationId: string, status: string, turnKey?: string, reason?: string, message?: string}} event
|
|
1404
|
-
*/
|
|
1405
|
-
function emitCompaction({
|
|
1406
|
-
operationId,
|
|
1407
|
-
status,
|
|
1408
|
-
turnKey,
|
|
1409
|
-
reason,
|
|
1410
|
-
message,
|
|
1411
|
-
}) {
|
|
1412
|
-
const previous = compactionStatuses.get(operationId);
|
|
1413
|
-
if (previous === status || previous === "succeeded" || previous === "failed" || previous === "skipped") return;
|
|
1414
|
-
compactionStatuses.set(operationId, status);
|
|
1415
|
-
if (status === "running") activeCompactions.set(operationId, { turnKey: turnKey || compactionTurnKey() });
|
|
1416
|
-
else activeCompactions.delete(operationId);
|
|
1417
|
-
emitEvent({
|
|
1418
|
-
type: "context_compaction",
|
|
1419
|
-
operationId,
|
|
1420
|
-
status,
|
|
1421
|
-
sdk: "codex",
|
|
1422
|
-
trigger: "automatic",
|
|
1423
|
-
timestamp: Date.now(),
|
|
1424
|
-
model: actualModel ? `codex:${actualModel}` : requestedReference,
|
|
1425
|
-
...(reason ? { reason } : {}),
|
|
1426
|
-
...(message ? { message } : {}),
|
|
1427
|
-
});
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
function finalizeOpenCompactions(reason, message) {
|
|
1431
|
-
for (const [operationId, active] of [...activeCompactions]) {
|
|
1432
|
-
emitCompaction({
|
|
1433
|
-
operationId,
|
|
1434
|
-
status: "failed",
|
|
1435
|
-
turnKey: active.turnKey,
|
|
1436
|
-
reason,
|
|
1437
|
-
message,
|
|
1438
|
-
});
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
function handleContextCompactionItem(method, params) {
|
|
1443
|
-
const item = params.item;
|
|
1444
|
-
const turnKey = compactionTurnKey(params);
|
|
1445
|
-
nativeCompactionTurnKeys.add(turnKey);
|
|
1446
|
-
if (legacyCompactionTurnKeys.has(turnKey)) return;
|
|
1447
|
-
const operationId = `codex:${item.id}`;
|
|
1448
|
-
emitCompaction({
|
|
1449
|
-
operationId,
|
|
1450
|
-
status: method === "item/started" ? "running" : "succeeded",
|
|
1451
|
-
turnKey,
|
|
1452
|
-
});
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
function handleLegacyCompaction(params) {
|
|
1456
|
-
const turnKey = compactionTurnKey(params);
|
|
1457
|
-
const active = [...activeCompactions].find(([, value]) => value.turnKey === turnKey);
|
|
1458
|
-
if (active) {
|
|
1459
|
-
emitCompaction({ operationId: active[0], status: "succeeded", turnKey });
|
|
1460
|
-
return;
|
|
1461
|
-
}
|
|
1462
|
-
if (nativeCompactionTurnKeys.has(turnKey) || legacyCompactionTurnKeys.has(turnKey)) return;
|
|
1463
|
-
legacyCompactionTurnKeys.add(turnKey);
|
|
1464
|
-
emitCompaction({
|
|
1465
|
-
operationId: `codex:${turnKey}:legacy`,
|
|
1466
|
-
status: "succeeded",
|
|
1467
|
-
turnKey,
|
|
1468
|
-
});
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
function handleAgentText(text) {
|
|
1472
|
-
const safeText = redactCodexDiagnostic(text, sensitiveValues);
|
|
1473
|
-
pushUniqueText(texts, safeText);
|
|
1474
|
-
emitEvent({ type: "assistant", message: { content: [{ type: "text", text: safeText }] } });
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
function notificationThreadId(params = {}) {
|
|
1478
|
-
if (typeof params.threadId === "string" && params.threadId) return params.threadId;
|
|
1479
|
-
if (typeof params.item?.senderThreadId === "string" && params.item.senderThreadId) {
|
|
1480
|
-
return params.item.senderThreadId;
|
|
1481
|
-
}
|
|
1482
|
-
return null;
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
function notificationTurnId(params = {}) {
|
|
1486
|
-
const candidate = params.turn?.id ?? params.turnId;
|
|
1487
|
-
return typeof candidate === "string" && candidate ? candidate : null;
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
function isRootThreadNotification(params = {}) {
|
|
1491
|
-
const sourceThreadId = notificationThreadId(params);
|
|
1492
|
-
return sourceThreadId === null || threadId === null || sourceThreadId === threadId;
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
function isRootActiveTurnNotification(params = {}) {
|
|
1496
|
-
if (!isRootThreadNotification(params)) return false;
|
|
1497
|
-
const sourceTurnId = notificationTurnId(params);
|
|
1498
|
-
return sourceTurnId === null || activeTurnId === null || sourceTurnId === activeTurnId;
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
function ensureSubagentGroup(spawnId, item = {}) {
|
|
1502
|
-
const id = typeof spawnId === "string" && spawnId ? spawnId : "codex-subagent";
|
|
1503
|
-
let group = subagentGroupsBySpawnId.get(id);
|
|
1504
|
-
if (!group) {
|
|
1505
|
-
group = {
|
|
1506
|
-
id,
|
|
1507
|
-
callIndex: subagentCallIndex++,
|
|
1508
|
-
name: "codex",
|
|
1509
|
-
prompt: typeof item.prompt === "string" ? safeDiagnostic(item.prompt, 2_000) : undefined,
|
|
1510
|
-
primaryThreadId: null,
|
|
1511
|
-
started: false,
|
|
1512
|
-
finished: false,
|
|
1513
|
-
startedAt: Date.now(),
|
|
1514
|
-
completedTurns: new Set(),
|
|
1515
|
-
bindings: new Set(),
|
|
1516
|
-
openActivities: new Map(),
|
|
1517
|
-
settledActivities: new Set(),
|
|
1518
|
-
primaryOutcome: null,
|
|
1519
|
-
};
|
|
1520
|
-
subagentGroupsBySpawnId.set(id, group);
|
|
1521
|
-
} else if (group.prompt === undefined && typeof item.prompt === "string") {
|
|
1522
|
-
group.prompt = safeDiagnostic(item.prompt, 2_000);
|
|
1523
|
-
}
|
|
1524
|
-
return group;
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
function metadataForSubagent(group, binding = null) {
|
|
1528
|
-
const agentPath = typeof binding?.agentPath === "string" && binding.agentPath.trim()
|
|
1529
|
-
? binding.agentPath
|
|
1530
|
-
: undefined;
|
|
1531
|
-
const name = binding?.name || codexAgentName(agentPath, group.name || "codex");
|
|
1532
|
-
return {
|
|
1533
|
-
id: group.id,
|
|
1534
|
-
...(binding?.nativeId ? { nativeId: binding.nativeId } : {}),
|
|
1535
|
-
name,
|
|
1536
|
-
callIndex: group.callIndex,
|
|
1537
|
-
...(agentPath === undefined ? {} : { label: agentPath, agentPath }),
|
|
1538
|
-
};
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
function observedSubagentCapabilities() {
|
|
1542
|
-
const names = [];
|
|
1543
|
-
const seenNames = new Set();
|
|
1544
|
-
for (const binding of subagentBindingsByThread.values()) {
|
|
1545
|
-
const name = metadataForSubagent(binding.group, binding).name;
|
|
1546
|
-
if (typeof name !== "string" || !name.trim() || seenNames.has(name)) continue;
|
|
1547
|
-
seenNames.add(name);
|
|
1548
|
-
names.push(name);
|
|
1549
|
-
}
|
|
1550
|
-
return {
|
|
1551
|
-
invoked: subagentGroupsBySpawnId.size > 0,
|
|
1552
|
-
names,
|
|
1553
|
-
};
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
function emitSubagentActivity(group, binding, activity) {
|
|
1557
|
-
// Once the canonical terminal row is emitted, no late provider frame may
|
|
1558
|
-
// append more activity to that group.
|
|
1559
|
-
if (group.finished && activity.phase !== "agent_completed") return;
|
|
1560
|
-
if (activity.phase === "started" && typeof activity.id === "string") {
|
|
1561
|
-
if (group.openActivities.has(activity.id) || group.settledActivities.has(activity.id)) return;
|
|
1562
|
-
group.openActivities.set(activity.id, {
|
|
1563
|
-
binding,
|
|
1564
|
-
name: activity.name,
|
|
1565
|
-
startedAt: Date.now(),
|
|
1566
|
-
});
|
|
1567
|
-
} else if (activity.phase === "completed" && typeof activity.id === "string") {
|
|
1568
|
-
if (group.settledActivities.has(activity.id)) return;
|
|
1569
|
-
group.openActivities.delete(activity.id);
|
|
1570
|
-
group.settledActivities.add(activity.id);
|
|
1571
|
-
}
|
|
1572
|
-
emitEvent({
|
|
1573
|
-
type: "subagent_activity",
|
|
1574
|
-
subagent: metadataForSubagent(group, binding),
|
|
1575
|
-
...activity,
|
|
1576
|
-
});
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
function drainOpenSubagentActivities(group, reason, onlyBinding = null) {
|
|
1580
|
-
for (const [id, activity] of [...group.openActivities]) {
|
|
1581
|
-
if (onlyBinding && activity.binding !== onlyBinding) continue;
|
|
1582
|
-
emitSubagentActivity(group, activity.binding, {
|
|
1583
|
-
phase: "completed",
|
|
1584
|
-
id,
|
|
1585
|
-
name: activity.name,
|
|
1586
|
-
isError: true,
|
|
1587
|
-
executionMs: Math.max(0, Date.now() - activity.startedAt),
|
|
1588
|
-
content: safeDiagnostic(reason, 2_000),
|
|
1589
|
-
});
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
function ensureSubagentStarted(group, binding = null) {
|
|
1594
|
-
if (group.started) return;
|
|
1595
|
-
group.started = true;
|
|
1596
|
-
group.startedAt = Date.now();
|
|
1597
|
-
const subagent = metadataForSubagent(group, binding);
|
|
1598
|
-
group.name = subagent.name;
|
|
1599
|
-
emitSubagentActivity(group, binding, {
|
|
1600
|
-
phase: "agent_started",
|
|
1601
|
-
id: `agent:${group.id}`,
|
|
1602
|
-
name: `Agent(${subagent.name})`,
|
|
1603
|
-
arguments: {
|
|
1604
|
-
name: subagent.name,
|
|
1605
|
-
...(subagent.agentPath === undefined ? {} : { description: subagent.agentPath }),
|
|
1606
|
-
...(group.prompt === undefined ? {} : { prompt: group.prompt }),
|
|
1607
|
-
},
|
|
1608
|
-
});
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
/**
|
|
1612
|
-
* @param {any} group
|
|
1613
|
-
* @param {any} binding
|
|
1614
|
-
* @param {{status?: string, error?: any, content?: string, executionMs?: number}} [outcome]
|
|
1615
|
-
*/
|
|
1616
|
-
function finishSubagentGroup(group, binding, {
|
|
1617
|
-
status = "completed",
|
|
1618
|
-
error,
|
|
1619
|
-
content,
|
|
1620
|
-
executionMs,
|
|
1621
|
-
} = {}) {
|
|
1622
|
-
if (group.finished) return;
|
|
1623
|
-
ensureSubagentStarted(group, binding);
|
|
1624
|
-
const isError = Boolean(error) || codexItemFailed({ status });
|
|
1625
|
-
const subagent = metadataForSubagent(group, binding);
|
|
1626
|
-
const summary = content
|
|
1627
|
-
|| (error ? safeDiagnostic(error, 2_000) : `${status || "completed"}`);
|
|
1628
|
-
drainOpenSubagentActivities(
|
|
1629
|
-
group,
|
|
1630
|
-
error
|
|
1631
|
-
? safeDiagnostic(error, 2_000)
|
|
1632
|
-
: "Codex subagent ended before this activity completed.",
|
|
1633
|
-
);
|
|
1634
|
-
// Mark terminal before invoking host callbacks so even re-entrant provider
|
|
1635
|
-
// frames cannot append an event after this row.
|
|
1636
|
-
group.finished = true;
|
|
1637
|
-
emitSubagentActivity(group, binding, {
|
|
1638
|
-
phase: "agent_completed",
|
|
1639
|
-
id: `agent:${group.id}`,
|
|
1640
|
-
name: `Agent(${subagent.name})`,
|
|
1641
|
-
isError,
|
|
1642
|
-
...(Number.isFinite(executionMs) ? { executionMs } : {}),
|
|
1643
|
-
content: safeDiagnostic(summary, 2_000),
|
|
1644
|
-
});
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
function maybeFinishSubagentGroup(group) {
|
|
1648
|
-
if (group.finished || !group.primaryOutcome) return;
|
|
1649
|
-
if ([...group.bindings].some((binding) => !binding.terminal)) return;
|
|
1650
|
-
finishSubagentGroup(
|
|
1651
|
-
group,
|
|
1652
|
-
group.primaryOutcome.binding,
|
|
1653
|
-
group.primaryOutcome.outcome,
|
|
1654
|
-
);
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
function recordPrimarySubagentOutcome(group, binding, outcome) {
|
|
1658
|
-
if (!group.primaryOutcome) group.primaryOutcome = { binding, outcome };
|
|
1659
|
-
maybeFinishSubagentGroup(group);
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
function finalizeOpenSubagents(status, content) {
|
|
1663
|
-
for (const group of subagentGroupsBySpawnId.values()) {
|
|
1664
|
-
if (group.finished) continue;
|
|
1665
|
-
const binding = group.primaryThreadId
|
|
1666
|
-
? subagentBindingsByThread.get(group.primaryThreadId)
|
|
1667
|
-
: null;
|
|
1668
|
-
finishSubagentGroup(group, binding, { status, error: content, content });
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
function subagentActivityId(group, binding, itemId, suffix = "") {
|
|
1673
|
-
const nativeId = binding?.nativeId || "child";
|
|
1674
|
-
return `agent:${group.id}:${nativeId}:${itemId || "item"}${suffix}`;
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
function flushPendingChildNotifications(nativeId) {
|
|
1678
|
-
const queued = pendingChildNotifications.get(nativeId);
|
|
1679
|
-
if (!queued?.length) return;
|
|
1680
|
-
pendingChildNotifications.delete(nativeId);
|
|
1681
|
-
for (const notification of queued) handleChildNotification(notification);
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
function bindSubagentThread(group, receiver, { primary = false, flush = true } = {}) {
|
|
1685
|
-
const nativeId = typeof receiver?.nativeId === "string" ? receiver.nativeId.trim() : "";
|
|
1686
|
-
if (!nativeId) return null;
|
|
1687
|
-
let binding = subagentBindingsByThread.get(nativeId);
|
|
1688
|
-
if (binding && binding.group !== group) return binding;
|
|
1689
|
-
if (!binding) {
|
|
1690
|
-
binding = {
|
|
1691
|
-
group,
|
|
1692
|
-
nativeId,
|
|
1693
|
-
agentPath: undefined,
|
|
1694
|
-
name: undefined,
|
|
1695
|
-
lastText: "",
|
|
1696
|
-
turnStartedAt: new Map(),
|
|
1697
|
-
turnStates: new Map(),
|
|
1698
|
-
terminal: false,
|
|
1699
|
-
usage: null,
|
|
1700
|
-
};
|
|
1701
|
-
subagentBindingsByThread.set(nativeId, binding);
|
|
1702
|
-
}
|
|
1703
|
-
group.bindings.add(binding);
|
|
1704
|
-
if (typeof receiver.agentPath === "string" && receiver.agentPath.trim()) {
|
|
1705
|
-
binding.agentPath = receiver.agentPath;
|
|
1706
|
-
binding.name = codexAgentName(receiver.agentPath, binding.name || group.name);
|
|
1707
|
-
}
|
|
1708
|
-
if (typeof receiver.name === "string" && receiver.name.trim()) binding.name = receiver.name.trim();
|
|
1709
|
-
// A root spawn can report multiple receiver ids. The first is the primary
|
|
1710
|
-
// result source; every additional binding remains part of the same group
|
|
1711
|
-
// and must terminate before the group terminal row is emitted.
|
|
1712
|
-
if (group.primaryThreadId === null && (primary || group.bindings.size === 1)) {
|
|
1713
|
-
group.primaryThreadId = nativeId;
|
|
1714
|
-
}
|
|
1715
|
-
ensureSubagentStarted(group, binding);
|
|
1716
|
-
if (flush) flushPendingChildNotifications(nativeId);
|
|
1717
|
-
return binding;
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
function handleCollabSpawn(method, params, item, sourceBinding = null) {
|
|
1721
|
-
const isRootSpawn = sourceBinding === null;
|
|
1722
|
-
const group = sourceBinding?.group || ensureSubagentGroup(item.id, item);
|
|
1723
|
-
const receivers = codexCollabReceiverEntries(item);
|
|
1724
|
-
const bindings = [];
|
|
1725
|
-
for (const receiver of receivers) {
|
|
1726
|
-
const binding = bindSubagentThread(group, receiver, {
|
|
1727
|
-
primary: isRootSpawn,
|
|
1728
|
-
// Bind the complete receiver set before replaying out-of-order child
|
|
1729
|
-
// frames, otherwise the first queued completion can close the group
|
|
1730
|
-
// before later receivers are known to it.
|
|
1731
|
-
flush: false,
|
|
1732
|
-
});
|
|
1733
|
-
if (binding) bindings.push(binding);
|
|
1734
|
-
}
|
|
1735
|
-
for (const binding of bindings) flushPendingChildNotifications(binding.nativeId);
|
|
1736
|
-
if (sourceBinding) {
|
|
1737
|
-
const phase = method === "item/started" ? "started" : "completed";
|
|
1738
|
-
emitSubagentActivity(group, sourceBinding, {
|
|
1739
|
-
phase,
|
|
1740
|
-
id: subagentActivityId(group, sourceBinding, item.id),
|
|
1741
|
-
name: `${metadataForSubagent(group, sourceBinding).name}▸spawn_agent`,
|
|
1742
|
-
...(phase === "started"
|
|
1743
|
-
? { arguments: { prompt: item.prompt || "", receivers: receivers.map(({ nativeId }) => nativeId) } }
|
|
1744
|
-
: {
|
|
1745
|
-
isError: codexItemFailed(item),
|
|
1746
|
-
content: safeDiagnostic(codexActivityContent({
|
|
1747
|
-
status: item.status,
|
|
1748
|
-
receivers: receivers.map(({ nativeId }) => nativeId),
|
|
1749
|
-
...(item.error ? { error: item.error } : {}),
|
|
1750
|
-
}), 2_000),
|
|
1751
|
-
}),
|
|
1752
|
-
});
|
|
1753
|
-
return;
|
|
1754
|
-
}
|
|
1755
|
-
if (method === "item/completed" && receivers.length === 0 && codexItemFailed(item)) {
|
|
1756
|
-
finishSubagentGroup(group, null, {
|
|
1757
|
-
status: item.status || "failed",
|
|
1758
|
-
error: item.error || "Codex failed to spawn the subagent",
|
|
1759
|
-
});
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
function handleSubAgentActivityItem(params, item, sourceBinding = null) {
|
|
1764
|
-
const sourceThreadId = notificationThreadId(params) || threadId || "root";
|
|
1765
|
-
const signalKey = `${sourceThreadId}:${item.id}:${item.kind}`;
|
|
1766
|
-
if (observedSubagentActivityItems.has(signalKey)) return;
|
|
1767
|
-
observedSubagentActivityItems.add(signalKey);
|
|
1768
|
-
|
|
1769
|
-
const targetNativeId = typeof item.agentThreadId === "string" ? item.agentThreadId : null;
|
|
1770
|
-
const targetBinding = targetNativeId ? subagentBindingsByThread.get(targetNativeId) : null;
|
|
1771
|
-
const group = sourceBinding?.group
|
|
1772
|
-
|| targetBinding?.group
|
|
1773
|
-
|| ensureSubagentGroup(item.id, item);
|
|
1774
|
-
const binding = targetNativeId
|
|
1775
|
-
? bindSubagentThread(group, {
|
|
1776
|
-
nativeId: targetNativeId,
|
|
1777
|
-
agentPath: item.agentPath,
|
|
1778
|
-
}, { primary: sourceBinding === null && targetBinding === undefined })
|
|
1779
|
-
: sourceBinding;
|
|
1780
|
-
|
|
1781
|
-
const kind = String(item.kind || "started");
|
|
1782
|
-
if (kind === "interrupted") {
|
|
1783
|
-
if (binding) {
|
|
1784
|
-
for (const turnId of binding.turnStates.keys()) binding.turnStates.set(turnId, "completed");
|
|
1785
|
-
binding.terminal = true;
|
|
1786
|
-
drainOpenSubagentActivities(
|
|
1787
|
-
group,
|
|
1788
|
-
`Codex subagent ${binding.agentPath || binding.nativeId} was interrupted before this activity completed.`,
|
|
1789
|
-
binding,
|
|
1790
|
-
);
|
|
1791
|
-
}
|
|
1792
|
-
if (binding?.nativeId === group.primaryThreadId) {
|
|
1793
|
-
recordPrimarySubagentOutcome(group, binding, {
|
|
1794
|
-
status: "interrupted",
|
|
1795
|
-
error: `Codex subagent ${binding.agentPath || binding.nativeId} was interrupted`,
|
|
1796
|
-
});
|
|
1797
|
-
} else {
|
|
1798
|
-
emitSubagentActivity(group, binding || sourceBinding, {
|
|
1799
|
-
phase: "message",
|
|
1800
|
-
id: subagentActivityId(group, binding || sourceBinding, item.id, ":interrupted"),
|
|
1801
|
-
name: `${metadataForSubagent(group, binding || sourceBinding).name}▸status`,
|
|
1802
|
-
kind: "status",
|
|
1803
|
-
role: "assistant",
|
|
1804
|
-
content: `interrupted ${item.agentPath || targetNativeId || "subagent"}`,
|
|
1805
|
-
});
|
|
1806
|
-
maybeFinishSubagentGroup(group);
|
|
1807
|
-
}
|
|
1808
|
-
} else if (kind === "interacted" || sourceBinding) {
|
|
1809
|
-
emitSubagentActivity(group, binding || sourceBinding, {
|
|
1810
|
-
phase: "message",
|
|
1811
|
-
id: subagentActivityId(group, binding || sourceBinding, item.id, `:${kind}`),
|
|
1812
|
-
name: `${metadataForSubagent(group, binding || sourceBinding).name}▸status`,
|
|
1813
|
-
kind: "status",
|
|
1814
|
-
role: "assistant",
|
|
1815
|
-
content: `${kind} ${item.agentPath || targetNativeId || "subagent"}`,
|
|
1816
|
-
});
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
function emitChildMessage(binding, {
|
|
1821
|
-
itemId,
|
|
1822
|
-
kind,
|
|
1823
|
-
content,
|
|
1824
|
-
index = 0,
|
|
1825
|
-
}) {
|
|
1826
|
-
const text = safeDiagnostic(content, 2_000);
|
|
1827
|
-
if (!text) return;
|
|
1828
|
-
const { group } = binding;
|
|
1829
|
-
emitSubagentActivity(group, binding, {
|
|
1830
|
-
phase: "message",
|
|
1831
|
-
id: subagentActivityId(group, binding, itemId, `:${kind}:${index}`),
|
|
1832
|
-
name: `${metadataForSubagent(group, binding).name}▸${kind}`,
|
|
1833
|
-
kind,
|
|
1834
|
-
role: "assistant",
|
|
1835
|
-
content: text,
|
|
1836
|
-
});
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
function handleChildItem(method, params, binding) {
|
|
1840
|
-
const item = params.item;
|
|
1841
|
-
if (!item || typeof item !== "object") return;
|
|
1842
|
-
const { group } = binding;
|
|
1843
|
-
if (isCodexCollabItem(item) && normalizedCollabTool(item.tool) === "spawnagent") {
|
|
1844
|
-
handleCollabSpawn(method, params, item, binding);
|
|
1845
|
-
return;
|
|
1846
|
-
}
|
|
1847
|
-
if (item.type === "subAgentActivity") {
|
|
1848
|
-
handleSubAgentActivityItem(params, item, binding);
|
|
1849
|
-
return;
|
|
1850
|
-
}
|
|
1851
|
-
const messageKey = `${binding.nativeId}:${item.id}`;
|
|
1852
|
-
if (item.type === "agentMessage") {
|
|
1853
|
-
if (method !== "item/completed") return;
|
|
1854
|
-
const text = item.text || childTextByItem.get(messageKey) || "";
|
|
1855
|
-
binding.lastText = safeDiagnostic(text, 2_000);
|
|
1856
|
-
if (!childMessageDeltaCounts.has(messageKey)) {
|
|
1857
|
-
emitChildMessage(binding, { itemId: item.id, kind: "text", content: text });
|
|
1858
|
-
}
|
|
1859
|
-
return;
|
|
1860
|
-
}
|
|
1861
|
-
if (item.type === "reasoning") {
|
|
1862
|
-
if (method !== "item/completed" || childReasoningDeltaCounts.has(messageKey)) return;
|
|
1863
|
-
const text = [...(item.summary || []), ...(item.content || [])].join("\n").trim();
|
|
1864
|
-
emitChildMessage(binding, { itemId: item.id, kind: "thinking", content: text });
|
|
1865
|
-
return;
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
const raw = mapThreadItem(method, item);
|
|
1869
|
-
const normalized = /** @type {any} */ (normalizeCodexItemEvent(raw, codexItemContext) || raw);
|
|
1870
|
-
if (normalized?.type === "file_change") {
|
|
1871
|
-
const phase = method === "item/started" ? "started" : "completed";
|
|
1872
|
-
const executionMs = codexItemDurationMs(item);
|
|
1873
|
-
emitSubagentActivity(group, binding, {
|
|
1874
|
-
phase,
|
|
1875
|
-
id: subagentActivityId(group, binding, item.id),
|
|
1876
|
-
name: `${metadataForSubagent(group, binding).name}▸file_change`,
|
|
1877
|
-
...(phase === "started"
|
|
1878
|
-
? { arguments: { changes: normalized.changes || [], status: normalized.status } }
|
|
1879
|
-
: {
|
|
1880
|
-
isError: normalized.is_error === true,
|
|
1881
|
-
...(executionMs === undefined ? {} : { executionMs }),
|
|
1882
|
-
content: safeDiagnostic(codexActivityContent({
|
|
1883
|
-
changes: normalized.changes || [],
|
|
1884
|
-
status: normalized.status,
|
|
1885
|
-
...(normalized.error ? { error: normalized.error } : {}),
|
|
1886
|
-
}), 2_000),
|
|
1887
|
-
}),
|
|
1888
|
-
});
|
|
1889
|
-
return;
|
|
1890
|
-
}
|
|
1891
|
-
const blocks = normalized?.message?.content;
|
|
1892
|
-
if (Array.isArray(blocks)) {
|
|
1893
|
-
for (const block of blocks) {
|
|
1894
|
-
if (block?.type === "tool_use") {
|
|
1895
|
-
emitSubagentActivity(group, binding, {
|
|
1896
|
-
phase: "started",
|
|
1897
|
-
id: subagentActivityId(group, binding, block.id || item.id),
|
|
1898
|
-
name: `${metadataForSubagent(group, binding).name}▸${block.name || item.type || "tool"}`,
|
|
1899
|
-
arguments: block.input || {},
|
|
1900
|
-
});
|
|
1901
|
-
} else if (block?.type === "tool_result") {
|
|
1902
|
-
const executionMs = codexItemDurationMs(item);
|
|
1903
|
-
emitSubagentActivity(group, binding, {
|
|
1904
|
-
phase: "completed",
|
|
1905
|
-
id: subagentActivityId(group, binding, block.tool_use_id || item.id),
|
|
1906
|
-
name: `${metadataForSubagent(group, binding).name}▸${codexChildToolName(item)}`,
|
|
1907
|
-
isError: block.is_error === true,
|
|
1908
|
-
...(executionMs === undefined ? {} : { executionMs }),
|
|
1909
|
-
content: safeDiagnostic(codexActivityContent(block.content), 2_000),
|
|
1910
|
-
});
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
return;
|
|
1914
|
-
}
|
|
1915
|
-
if (item.type === "webSearch" || item.type === "imageView") {
|
|
1916
|
-
const phase = method === "item/started" ? "started" : "completed";
|
|
1917
|
-
emitSubagentActivity(group, binding, {
|
|
1918
|
-
phase,
|
|
1919
|
-
id: subagentActivityId(group, binding, item.id),
|
|
1920
|
-
name: `${metadataForSubagent(group, binding).name}▸${item.type}`,
|
|
1921
|
-
...(phase === "started"
|
|
1922
|
-
? { arguments: item.type === "webSearch" ? { query: item.query } : { path: item.path } }
|
|
1923
|
-
: {
|
|
1924
|
-
isError: codexItemFailed(item),
|
|
1925
|
-
content: safeDiagnostic(codexActivityContent(item.results || item.result || item.status), 2_000),
|
|
1926
|
-
}),
|
|
1927
|
-
});
|
|
1928
|
-
return;
|
|
1929
|
-
}
|
|
1930
|
-
if (item.type === "sleep") {
|
|
1931
|
-
const phase = method === "item/started" ? "started" : "completed";
|
|
1932
|
-
const durationMs = codexItemDurationMs(item);
|
|
1933
|
-
emitSubagentActivity(group, binding, {
|
|
1934
|
-
phase,
|
|
1935
|
-
id: subagentActivityId(group, binding, item.id),
|
|
1936
|
-
name: `${metadataForSubagent(group, binding).name}▸sleep`,
|
|
1937
|
-
...(phase === "started"
|
|
1938
|
-
? { arguments: durationMs === undefined ? {} : { durationMs } }
|
|
1939
|
-
: {
|
|
1940
|
-
isError: codexItemFailed(item),
|
|
1941
|
-
content: safeDiagnostic(codexActivityContent({
|
|
1942
|
-
...(durationMs === undefined ? {} : { durationMs }),
|
|
1943
|
-
...(item.status === undefined ? {} : { status: item.status }),
|
|
1944
|
-
...(item.error === undefined ? {} : { error: item.error }),
|
|
1945
|
-
}), 2_000),
|
|
1946
|
-
}),
|
|
1947
|
-
});
|
|
1948
|
-
return;
|
|
1949
|
-
}
|
|
1950
|
-
if (item.type === "imageGeneration") {
|
|
1951
|
-
const phase = method === "item/started" ? "started" : "completed";
|
|
1952
|
-
const startedDetails = {
|
|
1953
|
-
...(item.status === undefined ? {} : { status: safeDiagnostic(item.status, 128) }),
|
|
1954
|
-
...(item.revisedPrompt === undefined
|
|
1955
|
-
? {}
|
|
1956
|
-
: {
|
|
1957
|
-
revisedPrompt: item.revisedPrompt === null
|
|
1958
|
-
? null
|
|
1959
|
-
: safeDiagnostic(item.revisedPrompt, 1_000),
|
|
1960
|
-
}),
|
|
1961
|
-
};
|
|
1962
|
-
const details = {
|
|
1963
|
-
...(item.status === undefined ? {} : { status: safeDiagnostic(item.status, 128) }),
|
|
1964
|
-
...(item.savedPath === undefined ? {} : { savedPath: safeDiagnostic(item.savedPath, 512) }),
|
|
1965
|
-
...(item.revisedPrompt === undefined
|
|
1966
|
-
? {}
|
|
1967
|
-
: {
|
|
1968
|
-
revisedPrompt: item.revisedPrompt === null
|
|
1969
|
-
? null
|
|
1970
|
-
: safeDiagnostic(item.revisedPrompt, 1_000),
|
|
1971
|
-
}),
|
|
1972
|
-
...(item.result === undefined
|
|
1973
|
-
? {}
|
|
1974
|
-
: { resultBytes: Buffer.byteLength(String(item.result)) }),
|
|
1975
|
-
...(item.error === undefined ? {} : { error: safeDiagnostic(item.error, 512) }),
|
|
1976
|
-
};
|
|
1977
|
-
emitSubagentActivity(group, binding, {
|
|
1978
|
-
phase,
|
|
1979
|
-
id: subagentActivityId(group, binding, item.id),
|
|
1980
|
-
name: `${metadataForSubagent(group, binding).name}▸imageGeneration`,
|
|
1981
|
-
...(phase === "started"
|
|
1982
|
-
? { arguments: startedDetails }
|
|
1983
|
-
: {
|
|
1984
|
-
isError: codexItemFailed(item),
|
|
1985
|
-
content: safeDiagnostic(codexActivityContent(details), 2_000),
|
|
1986
|
-
}),
|
|
1987
|
-
});
|
|
1988
|
-
return;
|
|
1989
|
-
}
|
|
1990
|
-
if (
|
|
1991
|
-
CODEX_PASSIVE_CHILD_ITEM_TYPES.has(item.type)
|
|
1992
|
-
|| typeof item.type !== "string"
|
|
1993
|
-
|| !item.type.trim()
|
|
1994
|
-
|| typeof item.id !== "string"
|
|
1995
|
-
|| !item.id.trim()
|
|
1996
|
-
) {
|
|
1997
|
-
return;
|
|
1998
|
-
}
|
|
1999
|
-
// The protocol has no action/category discriminator. Favor visibility for
|
|
2000
|
-
// unknown non-passive items until the bridge gains a first-class mapper;
|
|
2001
|
-
// bound opaque details so a new provider payload cannot inflate the stream.
|
|
2002
|
-
const phase = method === "item/started" ? "started" : "completed";
|
|
2003
|
-
const executionMs = codexItemDurationMs(item);
|
|
2004
|
-
const itemId = boundedCodexActivityItemId(item.id);
|
|
2005
|
-
const itemType = safeDiagnostic(item.type, 128);
|
|
2006
|
-
emitSubagentActivity(group, binding, {
|
|
2007
|
-
phase,
|
|
2008
|
-
id: subagentActivityId(group, binding, itemId),
|
|
2009
|
-
name: `${metadataForSubagent(group, binding).name}▸${itemType}`,
|
|
2010
|
-
...(phase === "started"
|
|
2011
|
-
? { arguments: { item: safeDiagnostic(item, 1_000) } }
|
|
2012
|
-
: {
|
|
2013
|
-
isError: codexItemFailed(item),
|
|
2014
|
-
...(executionMs === undefined ? {} : { executionMs }),
|
|
2015
|
-
content: safeDiagnostic(item, 2_000),
|
|
2016
|
-
}),
|
|
2017
|
-
});
|
|
2018
|
-
}
|
|
2019
|
-
|
|
2020
|
-
function handleChildNotification(notification) {
|
|
2021
|
-
const { method, params = {} } = notification;
|
|
2022
|
-
const sourceThreadId = notificationThreadId(params);
|
|
2023
|
-
const binding = sourceThreadId ? subagentBindingsByThread.get(sourceThreadId) : null;
|
|
2024
|
-
if (!binding) {
|
|
2025
|
-
if (!sourceThreadId) return;
|
|
2026
|
-
const queued = pendingChildNotifications.get(sourceThreadId) || [];
|
|
2027
|
-
if (queued.length >= 1_000) queued.shift();
|
|
2028
|
-
queued.push(notification);
|
|
2029
|
-
pendingChildNotifications.set(sourceThreadId, queued);
|
|
2030
|
-
return;
|
|
2031
|
-
}
|
|
2032
|
-
const { group } = binding;
|
|
2033
|
-
if (method === "turn/started") {
|
|
2034
|
-
const childTurnId = notificationTurnId(params) || "turn";
|
|
2035
|
-
binding.turnStartedAt.set(childTurnId, Date.now());
|
|
2036
|
-
binding.turnStates.set(childTurnId, "running");
|
|
2037
|
-
binding.terminal = false;
|
|
2038
|
-
return;
|
|
2039
|
-
}
|
|
2040
|
-
if (method === "turn/completed") {
|
|
2041
|
-
const childTurnId = notificationTurnId(params) || "turn";
|
|
2042
|
-
const terminalKey = `${binding.nativeId}:${childTurnId}`;
|
|
2043
|
-
if (group.completedTurns.has(terminalKey)) return;
|
|
2044
|
-
group.completedTurns.add(terminalKey);
|
|
2045
|
-
const status = params.turn?.status || "completed";
|
|
2046
|
-
const error = params.turn?.error?.message || params.turn?.error;
|
|
2047
|
-
const startedAt = binding.turnStartedAt.get(childTurnId);
|
|
2048
|
-
const executionMs = startedAt === undefined ? undefined : Date.now() - startedAt;
|
|
2049
|
-
binding.turnStates.set(childTurnId, "completed");
|
|
2050
|
-
binding.terminal = [...binding.turnStates.values()].every((state) => state === "completed");
|
|
2051
|
-
drainOpenSubagentActivities(
|
|
2052
|
-
group,
|
|
2053
|
-
error
|
|
2054
|
-
? safeDiagnostic(error, 2_000)
|
|
2055
|
-
: `Codex subagent turn ${status} before this activity completed.`,
|
|
2056
|
-
binding,
|
|
2057
|
-
);
|
|
2058
|
-
if (binding.nativeId === group.primaryThreadId) {
|
|
2059
|
-
recordPrimarySubagentOutcome(group, binding, {
|
|
2060
|
-
status,
|
|
2061
|
-
error,
|
|
2062
|
-
content: error || binding.lastText || status,
|
|
2063
|
-
executionMs,
|
|
2064
|
-
});
|
|
2065
|
-
} else {
|
|
2066
|
-
emitSubagentActivity(group, binding, {
|
|
2067
|
-
phase: "message",
|
|
2068
|
-
id: subagentActivityId(group, binding, childTurnId, ":completed"),
|
|
2069
|
-
name: `${metadataForSubagent(group, binding).name}▸status`,
|
|
2070
|
-
kind: "status",
|
|
2071
|
-
role: "assistant",
|
|
2072
|
-
content: safeDiagnostic(error || `${status} ${binding.agentPath || binding.nativeId}`, 2_000),
|
|
2073
|
-
});
|
|
2074
|
-
maybeFinishSubagentGroup(group);
|
|
2075
|
-
}
|
|
2076
|
-
return;
|
|
2077
|
-
}
|
|
2078
|
-
if (method === "item/agentMessage/delta") {
|
|
2079
|
-
const itemId = params.itemId || "message";
|
|
2080
|
-
const key = `${binding.nativeId}:${itemId}`;
|
|
2081
|
-
const current = childTextByItem.get(key) || "";
|
|
2082
|
-
childTextByItem.set(key, `${current}${params.delta || ""}`);
|
|
2083
|
-
const index = childMessageDeltaCounts.get(key) || 0;
|
|
2084
|
-
childMessageDeltaCounts.set(key, index + 1);
|
|
2085
|
-
emitChildMessage(binding, { itemId, kind: "text", content: params.delta || "", index });
|
|
2086
|
-
return;
|
|
2087
|
-
}
|
|
2088
|
-
if (method === "item/reasoning/summaryTextDelta" || method === "item/reasoning/textDelta") {
|
|
2089
|
-
const itemId = params.itemId || "reasoning";
|
|
2090
|
-
const key = `${binding.nativeId}:${itemId}`;
|
|
2091
|
-
const index = childReasoningDeltaCounts.get(key) || 0;
|
|
2092
|
-
childReasoningDeltaCounts.set(key, index + 1);
|
|
2093
|
-
emitChildMessage(binding, { itemId, kind: "thinking", content: params.delta || "", index });
|
|
2094
|
-
return;
|
|
2095
|
-
}
|
|
2096
|
-
if (method === "item/started" || method === "item/completed") {
|
|
2097
|
-
handleChildItem(method, params, binding);
|
|
2098
|
-
return;
|
|
2099
|
-
}
|
|
2100
|
-
if (method === "thread/tokenUsage/updated") {
|
|
2101
|
-
binding.usage = usageFromTokenUsage(params.tokenUsage);
|
|
2102
|
-
return;
|
|
2103
|
-
}
|
|
2104
|
-
if (method === "warning" || method === "error" || method === "configWarning" || method === "guardianWarning") {
|
|
2105
|
-
emitChildMessage(binding, {
|
|
2106
|
-
itemId: notificationTurnId(params) || "warning",
|
|
2107
|
-
kind: method === "error" ? "error" : "warning",
|
|
2108
|
-
content: params.message || params.error || params,
|
|
2109
|
-
});
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
|
-
function failNoToolsProbe(action) {
|
|
2114
|
-
if (!noToolsProbe || noToolsViolation) return;
|
|
2115
|
-
const safeAction = safeDiagnostic(action, 512);
|
|
2116
|
-
noToolsViolation = safeAction;
|
|
2117
|
-
errorMessage = `Codex attempted ${safeAction} during a no-tool readiness probe`;
|
|
2118
|
-
failureKind = "tool_policy_violation";
|
|
2119
|
-
codexDiagnostics = { ...codexDiagnostics, codex_error_code: "codex_no_tools_violation", codex_tool_action: safeAction };
|
|
2120
|
-
emitEvent({
|
|
2121
|
-
type: "runtime_warning",
|
|
2122
|
-
warning_kind: "codex_no_tools_violation",
|
|
2123
|
-
message: "Codex attempted a tool action during the no-tool readiness probe; the turn was interrupted.",
|
|
2124
|
-
});
|
|
2125
|
-
if (threadId && activeTurnId && !interruptSent) {
|
|
2126
|
-
interruptSent = true;
|
|
2127
|
-
client?.request("turn/interrupt", { threadId, turnId: activeTurnId }).catch(() => {});
|
|
2128
|
-
}
|
|
2129
|
-
turnCompleted = true;
|
|
2130
|
-
stopLiveInput();
|
|
2131
|
-
resolveTurn({ id: activeTurnId, status: "interrupted" });
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
function failUnsupportedServerRequest(method) {
|
|
2135
|
-
if (noToolsProbe) {
|
|
2136
|
-
failNoToolsProbe(method);
|
|
2137
|
-
return;
|
|
2138
|
-
}
|
|
2139
|
-
if (serverRequestViolation) return;
|
|
2140
|
-
const safeMethod = safeDiagnostic(method, 512);
|
|
2141
|
-
serverRequestViolation = safeMethod;
|
|
2142
|
-
errorMessage = `Codex requested unsupported client interaction (${safeMethod}); the unattended turn was stopped.`;
|
|
2143
|
-
failureKind = "skipped_capability_mismatch";
|
|
2144
|
-
codexDiagnostics = {
|
|
2145
|
-
...codexDiagnostics,
|
|
2146
|
-
codex_error_code: "codex_server_request_unsupported",
|
|
2147
|
-
codex_server_request_method: safeMethod,
|
|
2148
|
-
};
|
|
2149
|
-
emitEvent({
|
|
2150
|
-
type: "runtime_warning",
|
|
2151
|
-
warning_kind: "codex_server_request_unsupported",
|
|
2152
|
-
message: errorMessage,
|
|
2153
|
-
});
|
|
2154
|
-
turnCompleted = true;
|
|
2155
|
-
stopLiveInput();
|
|
2156
|
-
resolveTurn({ id: activeTurnId, status: "interrupted" });
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
function assertNoUnsupportedServerRequest() {
|
|
2160
|
-
if (serverRequestViolation) {
|
|
2161
|
-
throw new Error(errorMessage || `Unsupported Codex app-server request: ${serverRequestViolation}`);
|
|
2162
|
-
}
|
|
2163
|
-
}
|
|
2164
|
-
|
|
2165
|
-
function rejectUnsupportedChildServerRequest(request, method, binding) {
|
|
2166
|
-
if (!binding) return false;
|
|
2167
|
-
const params = request?.params || {};
|
|
2168
|
-
const sourceThreadId = notificationThreadId(params);
|
|
2169
|
-
handleChildNotification({
|
|
2170
|
-
method: "error",
|
|
2171
|
-
params: {
|
|
2172
|
-
threadId: sourceThreadId,
|
|
2173
|
-
...(notificationTurnId(params) ? { turnId: notificationTurnId(params) } : {}),
|
|
2174
|
-
message: `Codex subagent requested unsupported client interaction (${safeDiagnostic(method, 512)}).`,
|
|
2175
|
-
},
|
|
2176
|
-
});
|
|
2177
|
-
return true;
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
function handleNotification(notification) {
|
|
2181
|
-
const safeNotification = sanitizeCodexNotification(notification, sensitiveValues);
|
|
2182
|
-
const { method, params = {} } = safeNotification;
|
|
2183
|
-
if (noToolsProbe) {
|
|
2184
|
-
const itemType = params.item?.type;
|
|
2185
|
-
if (
|
|
2186
|
-
CODEX_NO_TOOL_REQUEST_METHODS.has(method)
|
|
2187
|
-
|| ((method === "item/started" || method === "item/completed") && CODEX_NO_TOOL_ACTION_ITEMS.has(itemType))
|
|
2188
|
-
) {
|
|
2189
|
-
failNoToolsProbe(typeof itemType === "string" ? itemType : method);
|
|
2190
|
-
return;
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
const sourceThreadId = notificationThreadId(params);
|
|
2194
|
-
if (threadId && sourceThreadId && sourceThreadId !== threadId) {
|
|
2195
|
-
handleChildNotification(safeNotification);
|
|
2196
|
-
return;
|
|
2197
|
-
}
|
|
2198
|
-
if (method === "turn/started") {
|
|
2199
|
-
if (!isRootThreadNotification(params)) return;
|
|
2200
|
-
setActiveTurnId(params.turn?.id, { steerReady: true });
|
|
2201
|
-
emitEvent({ type: "cli_event", raw: { type: "turn_started", turn: params.turn } });
|
|
2202
|
-
return;
|
|
2203
|
-
}
|
|
2204
|
-
if (method === "turn/completed") {
|
|
2205
|
-
if (!isRootActiveTurnNotification(params)) return;
|
|
2206
|
-
setActiveTurnId(params.turn?.id);
|
|
2207
|
-
turnCompleted = true;
|
|
2208
|
-
stopLiveInput();
|
|
2209
|
-
if (params.turn?.status === "failed") {
|
|
2210
|
-
errorMessage = safeDiagnostic(params.turn?.error?.message || params.turn?.error || "Codex turn failed");
|
|
2211
|
-
failureKind = "provider_unavailable";
|
|
2212
|
-
}
|
|
2213
|
-
if (activeCompactions.size > 0) {
|
|
2214
|
-
const cancelled = params.turn?.status === "cancelled" || params.turn?.status === "interrupted";
|
|
2215
|
-
finalizeOpenCompactions(
|
|
2216
|
-
cancelled ? "cancelled" : "incomplete",
|
|
2217
|
-
cancelled ? "Compaction was interrupted." : "Compaction ended without a completion event.",
|
|
2218
|
-
);
|
|
2219
|
-
}
|
|
2220
|
-
const safeTurn = params.turn?.error === undefined
|
|
2221
|
-
? params.turn
|
|
2222
|
-
: { ...params.turn, error: safeResponseError(params.turn.error) };
|
|
2223
|
-
emitEvent({ type: "cli_event", raw: { type: "turn_completed", turn: safeTurn } });
|
|
2224
|
-
resolveTurn(params.turn);
|
|
2225
|
-
return;
|
|
2226
|
-
}
|
|
2227
|
-
if (method === "thread/tokenUsage/updated") {
|
|
2228
|
-
if (!isRootActiveTurnNotification(params)) return;
|
|
2229
|
-
usage = usageFromTokenUsage(params.tokenUsage);
|
|
2230
|
-
const contextUsage = contextUsageFromTokenUsage(params.tokenUsage);
|
|
2231
|
-
if (contextUsage) {
|
|
2232
|
-
emitEvent({
|
|
2233
|
-
type: "context_usage",
|
|
2234
|
-
sdk: "codex",
|
|
2235
|
-
model: actualModel ? `codex:${actualModel}` : requestedReference,
|
|
2236
|
-
timestamp: Date.now(),
|
|
2237
|
-
...(typeof params.turnId === "string" && params.turnId.length > 0
|
|
2238
|
-
? { measurementId: params.turnId }
|
|
2239
|
-
: {}),
|
|
2240
|
-
...contextUsage,
|
|
2241
|
-
});
|
|
2242
|
-
}
|
|
2243
|
-
return;
|
|
2244
|
-
}
|
|
2245
|
-
if (method === "model/rerouted") {
|
|
2246
|
-
if (!isRootActiveTurnNotification(params)) return;
|
|
2247
|
-
if (typeof params.toModel === "string" && params.toModel.trim().length > 0) actualModel = params.toModel;
|
|
2248
|
-
return;
|
|
2249
|
-
}
|
|
2250
|
-
if (method === "thread/compacted") {
|
|
2251
|
-
if (!isRootActiveTurnNotification(params)) return;
|
|
2252
|
-
handleLegacyCompaction(params);
|
|
2253
|
-
return;
|
|
2254
|
-
}
|
|
2255
|
-
if (method === "item/agentMessage/delta") {
|
|
2256
|
-
if (!isRootActiveTurnNotification(params)) return;
|
|
2257
|
-
const current = agentTextByItem.get(params.itemId) || "";
|
|
2258
|
-
agentTextByItem.set(params.itemId, `${current}${params.delta || ""}`);
|
|
2259
|
-
return;
|
|
2260
|
-
}
|
|
2261
|
-
if (method === "item/reasoning/summaryTextDelta" || method === "item/reasoning/textDelta") {
|
|
2262
|
-
if (!isRootActiveTurnNotification(params)) return;
|
|
2263
|
-
emitEvent({ type: "assistant", message: { content: [{ type: "thinking", text: params.delta || "" }] } });
|
|
2264
|
-
return;
|
|
2265
|
-
}
|
|
2266
|
-
if (method === "warning" || method === "error" || method === "configWarning" || method === "guardianWarning") {
|
|
2267
|
-
emitEvent({
|
|
2268
|
-
type: "runtime_warning",
|
|
2269
|
-
warning_kind: method.replace(/\W+/g, "_"),
|
|
2270
|
-
message: safeDiagnostic(params.message || params.error || params),
|
|
2271
|
-
});
|
|
2272
|
-
return;
|
|
2273
|
-
}
|
|
2274
|
-
if (method === "item/started" || method === "item/completed") {
|
|
2275
|
-
if (!isRootActiveTurnNotification(params)) return;
|
|
2276
|
-
if (isCodexCollabItem(params.item) && normalizedCollabTool(params.item.tool) === "spawnagent") {
|
|
2277
|
-
handleCollabSpawn(method, params, params.item);
|
|
2278
|
-
return;
|
|
2279
|
-
}
|
|
2280
|
-
if (params.item?.type === "subAgentActivity") {
|
|
2281
|
-
handleSubAgentActivityItem(params, params.item);
|
|
2282
|
-
return;
|
|
2283
|
-
}
|
|
2284
|
-
if (params.item?.type === "contextCompaction") {
|
|
2285
|
-
handleContextCompactionItem(method, params);
|
|
2286
|
-
return;
|
|
2287
|
-
}
|
|
2288
|
-
const raw = mapThreadItem(method, params.item);
|
|
2289
|
-
if (params.item?.type === "agentMessage") {
|
|
2290
|
-
const text = params.item.text || agentTextByItem.get(params.item.id) || "";
|
|
2291
|
-
if (method === "item/completed") handleAgentText(text);
|
|
2292
|
-
return;
|
|
2293
|
-
}
|
|
2294
|
-
if (raw) emitEvent(normalizeCodexItemEvent(raw, codexItemContext) || raw);
|
|
2295
|
-
}
|
|
2296
|
-
}
|
|
2297
|
-
|
|
2298
|
-
let client = null;
|
|
2299
|
-
let resumeEntry = null;
|
|
2300
|
-
let sessionRetained = false;
|
|
2301
|
-
let abortRequested = false;
|
|
2302
|
-
let interruptSent = false;
|
|
2303
|
-
let unownedChildRequestObserved = false;
|
|
2304
|
-
// Mutable holder so keep-alive clients can outlive this run: each run
|
|
2305
|
-
// installs its own notification and server-request handlers. The bridge
|
|
2306
|
-
// restores idle handlers once the session is no longer executing a turn.
|
|
2307
|
-
const notificationTarget = { handler: handleNotification };
|
|
2308
|
-
function handleServerRequest(request) {
|
|
2309
|
-
const method = typeof request?.method === "string" ? request.method : "unknown";
|
|
2310
|
-
// A same-batch request can arrive after turn/completed but before the
|
|
2311
|
-
// generate promise reaches finally. Never let that frame approve work or
|
|
2312
|
-
// mutate the already-final provider result.
|
|
2313
|
-
if (turnCompleted || abortRequested || !activeTurnId) return rejectIdleServerRequest(request);
|
|
2314
|
-
const params = request?.params || {};
|
|
2315
|
-
const sourceThreadId = notificationThreadId(params);
|
|
2316
|
-
const sourceTurnId = notificationTurnId(params);
|
|
2317
|
-
const isChildRequest = Boolean(
|
|
2318
|
-
threadId && sourceThreadId && sourceThreadId !== threadId,
|
|
2319
|
-
);
|
|
2320
|
-
if (!isChildRequest && sourceTurnId !== activeTurnId) {
|
|
2321
|
-
throw new Error(
|
|
2322
|
-
`Codex app-server request does not match the active turn: ${method}`,
|
|
2323
|
-
);
|
|
2324
|
-
}
|
|
2325
|
-
// A foreign thread id is not enough to establish child ownership. Retained
|
|
2326
|
-
// app-server processes can still deliver work from an earlier logical run;
|
|
2327
|
-
// only a live binding created by this run's root/descendant spawn records
|
|
2328
|
-
// may inherit configured-MCP approval.
|
|
2329
|
-
const currentChildBinding = isChildRequest
|
|
2330
|
-
? subagentBindingsByThread.get(sourceThreadId)
|
|
2331
|
-
: null;
|
|
2332
|
-
if (isChildRequest && (
|
|
2333
|
-
!currentChildBinding
|
|
2334
|
-
|| currentChildBinding.terminal
|
|
2335
|
-
|| currentChildBinding.group.finished
|
|
2336
|
-
|| !sourceTurnId
|
|
2337
|
-
|| currentChildBinding.turnStates.get(sourceTurnId) !== "running"
|
|
2338
|
-
)) {
|
|
2339
|
-
unownedChildRequestObserved = true;
|
|
2340
|
-
throw new Error(`Unsupported Codex app-server request: ${method}`);
|
|
2341
|
-
}
|
|
2342
|
-
const approval = noToolsProbe
|
|
2343
|
-
? null
|
|
2344
|
-
: codexConfiguredMcpApprovalResponse(request, configuredMcpServerNames);
|
|
2345
|
-
if (approval) return approval;
|
|
2346
|
-
// A child turn can fail an interaction it cannot service without
|
|
2347
|
-
// terminating the still-active root turn. The app-server receives the
|
|
2348
|
-
// JSON-RPC error and reports the child lifecycle separately.
|
|
2349
|
-
if (rejectUnsupportedChildServerRequest(request, method, currentChildBinding)) {
|
|
2350
|
-
throw new Error(`Unsupported Codex app-server request: ${method}`);
|
|
2351
|
-
}
|
|
2352
|
-
failUnsupportedServerRequest(method);
|
|
2353
|
-
throw new Error(`Unsupported Codex app-server request: ${method}`);
|
|
2354
|
-
}
|
|
2355
|
-
const serverRequestTarget = { handler: handleServerRequest };
|
|
2356
|
-
function createClient() {
|
|
2357
|
-
const args = options.codexAppServerArgs !== undefined
|
|
2358
|
-
? options.codexAppServerArgs
|
|
2359
|
-
: options.codexLoadProjectDocs === true
|
|
2360
|
-
? CODEX_APP_SERVER_ARGS
|
|
2361
|
-
: CODEX_APP_SERVER_ISOLATED_ARGS;
|
|
2362
|
-
return makeClient({
|
|
2363
|
-
command: options.codexAppServerCommand,
|
|
2364
|
-
args,
|
|
2365
|
-
cwd: options.cwd,
|
|
2366
|
-
env: options.codexAppServerEnv,
|
|
2367
|
-
redactionValues: sensitiveValues,
|
|
2368
|
-
onNotification: (notification) => notificationTarget.handler(
|
|
2369
|
-
sanitizeCodexNotification(notification, sensitiveValues),
|
|
2370
|
-
),
|
|
2371
|
-
onServerRequest: (request) => serverRequestTarget.handler(request),
|
|
2372
|
-
});
|
|
2373
|
-
}
|
|
2374
|
-
|
|
2375
|
-
async function initializeClient(nextClient) {
|
|
2376
|
-
const brand = options.toolContext?.runtimeBrand ?? readRuntimeBrand();
|
|
2377
|
-
await nextClient.request("initialize", {
|
|
2378
|
-
clientInfo: { name: brand.clientInfoName, title: brand.clientInfoTitle, version: "0" },
|
|
2379
|
-
capabilities: { experimentalApi: true },
|
|
2380
|
-
});
|
|
2381
|
-
assertNoUnsupportedServerRequest();
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
|
-
async function requestThreadStart(params) {
|
|
2385
|
-
const policy = threadStartPolicy(systemPrompt, options);
|
|
2386
|
-
const startedAt = Date.now();
|
|
2387
|
-
let lastError = null;
|
|
2388
|
-
for (let attempt = 1; attempt <= policy.attempts; attempt += 1) {
|
|
2389
|
-
if (!client) {
|
|
2390
|
-
client = createClient();
|
|
2391
|
-
await initializeClient(client);
|
|
2392
|
-
}
|
|
2393
|
-
try {
|
|
2394
|
-
const thread = await client.request("thread/start", params, { timeoutMs: policy.timeoutMs });
|
|
2395
|
-
assertNoUnsupportedServerRequest();
|
|
2396
|
-
codexDiagnostics = {
|
|
2397
|
-
...withoutCodexRequestErrorDiagnostics(codexDiagnostics),
|
|
2398
|
-
codex_thread_start_attempts: attempt,
|
|
2399
|
-
codex_thread_start_timeout_ms: policy.timeoutMs,
|
|
2400
|
-
codex_thread_start_duration_ms: Date.now() - startedAt,
|
|
2401
|
-
...(attempt > 1 ? { codex_thread_start_retried: true } : {}),
|
|
2402
|
-
};
|
|
2403
|
-
return thread;
|
|
2404
|
-
} catch (err) {
|
|
2405
|
-
lastError = err;
|
|
2406
|
-
codexDiagnostics = {
|
|
2407
|
-
...codexDiagnostics,
|
|
2408
|
-
...codexErrorDiagnostics(err, sensitiveValues),
|
|
2409
|
-
codex_thread_start_attempts: attempt,
|
|
2410
|
-
codex_thread_start_timeout_ms: policy.timeoutMs,
|
|
2411
|
-
codex_thread_start_duration_ms: Date.now() - startedAt,
|
|
2412
|
-
...(attempt > 1 ? { codex_thread_start_retried: true } : {}),
|
|
2413
|
-
};
|
|
2414
|
-
if (!isCodexRequestTimeout(err, "thread/start") || attempt >= policy.attempts || options.abortSignal?.aborted) {
|
|
2415
|
-
throw err;
|
|
2416
|
-
}
|
|
2417
|
-
emitEvent({
|
|
2418
|
-
type: "runtime_warning",
|
|
2419
|
-
warning_kind: "codex_thread_start_retry",
|
|
2420
|
-
message: `Codex app-server thread/start timed out after ${policy.timeoutMs}ms; retrying with a fresh app-server.`,
|
|
2421
|
-
});
|
|
2422
|
-
await closeCodexClient(client);
|
|
2423
|
-
client = null;
|
|
2424
|
-
await delay(policy.backoffMs, options.abortSignal);
|
|
2425
|
-
}
|
|
2426
|
-
}
|
|
2427
|
-
throw lastError || new Error("codex app-server request timed out: thread/start");
|
|
2428
|
-
}
|
|
2429
|
-
|
|
2430
|
-
const abortHandler = () => {
|
|
2431
|
-
abortRequested = true;
|
|
2432
|
-
stopLiveInput();
|
|
2433
|
-
if (threadId && activeTurnId && !interruptSent) {
|
|
2434
|
-
interruptSent = true;
|
|
2435
|
-
client?.request("turn/interrupt", { threadId, turnId: activeTurnId }).catch(() => {});
|
|
2436
|
-
}
|
|
2437
|
-
// Resumed sessions stay alive across an interrupt; only fresh runs tear
|
|
2438
|
-
// down their subprocess on abort.
|
|
2439
|
-
if (!resumeEntry) void closeCodexClient(client);
|
|
2440
|
-
};
|
|
2441
|
-
|
|
2442
|
-
async function steerLiveInput() {
|
|
2443
|
-
if (!options.liveInput) return;
|
|
2444
|
-
const iterator = options.liveInput[Symbol.asyncIterator]();
|
|
2445
|
-
try {
|
|
2446
|
-
while (!turnCompleted && !liveInputStopped) {
|
|
2447
|
-
const next = await Promise.race([
|
|
2448
|
-
iterator.next(),
|
|
2449
|
-
liveInputStop.then(() => ({ done: true, value: undefined })),
|
|
2450
|
-
]);
|
|
2451
|
-
if (next.done || turnCompleted || liveInputStopped) break;
|
|
2452
|
-
const message = next.value;
|
|
2453
|
-
if (!threadId || !activeTurnId || !turnReadyResolved) {
|
|
2454
|
-
await Promise.race([
|
|
2455
|
-
turnReady,
|
|
2456
|
-
liveInputStop,
|
|
2457
|
-
]);
|
|
2458
|
-
if (turnCompleted || liveInputStopped || !turnReadyResolved) break;
|
|
2459
|
-
}
|
|
2460
|
-
const input = userTextInput(formatLiveInputGuidance(message.body, options.prompts));
|
|
2461
|
-
try {
|
|
2462
|
-
const response = await client.request("turn/steer", {
|
|
2463
|
-
threadId,
|
|
2464
|
-
expectedTurnId: activeTurnId,
|
|
2465
|
-
input,
|
|
2466
|
-
});
|
|
2467
|
-
activeTurnId = response?.turnId || activeTurnId;
|
|
2468
|
-
invokeLiveInputCallback(message, "acknowledge");
|
|
2469
|
-
} catch (err) {
|
|
2470
|
-
const providerError = err?.responseError;
|
|
2471
|
-
if (isNoActiveTurnToSteer(providerError || err)) {
|
|
2472
|
-
await Promise.race([
|
|
2473
|
-
turnReady,
|
|
2474
|
-
liveInputStop,
|
|
2475
|
-
]);
|
|
2476
|
-
if (turnCompleted || liveInputStopped) break;
|
|
2477
|
-
try {
|
|
2478
|
-
const response = await client.request("turn/steer", {
|
|
2479
|
-
threadId,
|
|
2480
|
-
expectedTurnId: activeTurnId,
|
|
2481
|
-
input,
|
|
2482
|
-
});
|
|
2483
|
-
activeTurnId = response?.turnId || activeTurnId;
|
|
2484
|
-
invokeLiveInputCallback(message, "acknowledge");
|
|
2485
|
-
continue;
|
|
2486
|
-
} catch (retryErr) {
|
|
2487
|
-
invokeLiveInputCallback(message, "reject", retryErr);
|
|
2488
|
-
const retryProviderError = retryErr?.responseError
|
|
2489
|
-
? safeResponseError(retryErr.responseError)
|
|
2490
|
-
: null;
|
|
2491
|
-
emitEvent({
|
|
2492
|
-
type: "runtime_warning",
|
|
2493
|
-
warning_kind: isActiveTurnNotSteerable(retryProviderError) ? "active_turn_not_steerable" : "live_input_rejected",
|
|
2494
|
-
message: safeDiagnostic(codexErrorMessage(retryProviderError || retryErr)),
|
|
2495
|
-
});
|
|
2496
|
-
// Preserve FIFO fallback: once one message is rejected, later
|
|
2497
|
-
// entries must not overtake it inside this provider attempt.
|
|
2498
|
-
break;
|
|
2499
|
-
}
|
|
2500
|
-
}
|
|
2501
|
-
invokeLiveInputCallback(message, "reject", err);
|
|
2502
|
-
emitEvent({
|
|
2503
|
-
type: "runtime_warning",
|
|
2504
|
-
warning_kind: isActiveTurnNotSteerable(providerError) ? "active_turn_not_steerable" : "live_input_rejected",
|
|
2505
|
-
message: safeDiagnostic(codexErrorMessage(
|
|
2506
|
-
providerError ? safeResponseError(providerError) : err,
|
|
2507
|
-
)),
|
|
2508
|
-
});
|
|
2509
|
-
break;
|
|
2510
|
-
}
|
|
2511
|
-
}
|
|
2512
|
-
} finally {
|
|
2513
|
-
if (typeof iterator.return === "function") {
|
|
2514
|
-
try { void Promise.resolve(iterator.return()).catch(() => {}); } catch { /* best-effort */ }
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
}
|
|
2518
|
-
|
|
2519
|
-
function sessionUnavailableResult(kind, error, codexErrorCode) {
|
|
2520
|
-
const subagentCapabilities = observedSubagentCapabilities();
|
|
2521
|
-
return {
|
|
2522
|
-
text: null,
|
|
2523
|
-
structuredResult: undefined,
|
|
2524
|
-
structuredResultSource: null,
|
|
2525
|
-
events: [],
|
|
2526
|
-
usage: {},
|
|
2527
|
-
durationMs: Date.now() - start,
|
|
2528
|
-
numTurns: 0,
|
|
2529
|
-
model: resolved?.reference || `codex:${resolved?.model || ""}`,
|
|
2530
|
-
effort: options.effort || null,
|
|
2531
|
-
sdk: "codex",
|
|
2532
|
-
providerSessionId: resumeSessionId,
|
|
2533
|
-
provider_session_id: resumeSessionId,
|
|
2534
|
-
cancelled: false,
|
|
2535
|
-
error,
|
|
2536
|
-
failureKind: kind,
|
|
2537
|
-
diagnostics: { codex_error_code: codexErrorCode },
|
|
2538
|
-
capabilitiesUsed: buildCapabilitiesUsed({
|
|
2539
|
-
promptCacheActive: null,
|
|
2540
|
-
thinkingEnabled: null,
|
|
2541
|
-
structuredOutputEnforced: !!options.outputSchema,
|
|
2542
|
-
subagentInvoked: subagentCapabilities.invoked,
|
|
2543
|
-
mcpServersUsed: sentMcpServerNames,
|
|
2544
|
-
nativeSubagentsUsed: subagentCapabilities.names,
|
|
2545
|
-
toolCompactionApplied: false,
|
|
2546
|
-
contextCompactionApplied: null,
|
|
2547
|
-
}),
|
|
2548
|
-
};
|
|
2549
|
-
}
|
|
2550
|
-
|
|
2551
|
-
if (
|
|
2552
|
-
Array.isArray(options.nativeSubagents?.teammates)
|
|
2553
|
-
&& options.nativeSubagents.teammates.length > 0
|
|
2554
|
-
) {
|
|
2555
|
-
return sessionUnavailableResult(
|
|
2556
|
-
"skipped_capability_mismatch",
|
|
2557
|
-
"Direct Codex owns its native collaboration agents and does not accept mono-agent nativeSubagents teammate/profile definitions. Remove nativeSubagents, use codexLoadProjectDocs for repository instructions, or route this run to Claude.",
|
|
2558
|
-
"codex_native_subagent_definitions_unsupported",
|
|
2559
|
-
);
|
|
2560
|
-
}
|
|
2561
|
-
|
|
2562
|
-
const mcpServerNameProblem = codexMcpServerNameProblem(invalidMcpServerNames);
|
|
2563
|
-
if (mcpServerNameProblem) {
|
|
2564
|
-
return sessionUnavailableResult(
|
|
2565
|
-
"skipped_capability_mismatch",
|
|
2566
|
-
mcpServerNameProblem,
|
|
2567
|
-
"codex_mcp_server_name_invalid",
|
|
2568
|
-
);
|
|
2569
|
-
}
|
|
2570
|
-
|
|
2571
|
-
if (resolveSandboxPolicy(options.toolContext, options.sandboxPolicy) !== undefined) {
|
|
2572
|
-
return sessionUnavailableResult(
|
|
2573
|
-
"skipped_capability_mismatch",
|
|
2574
|
-
"Direct Codex cannot enforce mono-agent's native srt sandbox scopes. Remove the mono-agent sandbox policy or use a Pi runtime for exact readableRoots, writableRoots, denyWrite, and network rules.",
|
|
2575
|
-
"codex_sandbox_policy_unsupported",
|
|
2576
|
-
);
|
|
2577
|
-
}
|
|
2578
|
-
|
|
2579
|
-
const toolPolicyProblem = codexToolPolicyProblem(options);
|
|
2580
|
-
if (toolPolicyProblem) {
|
|
2581
|
-
return sessionUnavailableResult(
|
|
2582
|
-
"skipped_capability_mismatch",
|
|
2583
|
-
toolPolicyProblem,
|
|
2584
|
-
"codex_tool_policy_unsupported",
|
|
2585
|
-
);
|
|
2586
|
-
}
|
|
2587
|
-
|
|
2588
|
-
if (resumeSessionId) {
|
|
2589
|
-
// Await-free busy claim (get -> busy check -> set-busy in one span). A miss
|
|
2590
|
-
// fails fast: the host sent no conversation history for a resume, so silently
|
|
2591
|
-
// starting a fresh thread would lose context. A busy entry is executing a
|
|
2592
|
-
// turn already.
|
|
2593
|
-
const claimed = codexLiveness.claim(resumeSessionId);
|
|
2594
|
-
if (!claimed.ok) {
|
|
2595
|
-
// @ts-check does not narrow the ClaimResult union on `!claimed.ok`,
|
|
2596
|
-
// though the loser branch always carries `reason`.
|
|
2597
|
-
return /** @type {{reason: string}} */ (claimed).reason === "missing"
|
|
2598
|
-
? sessionUnavailableResult(
|
|
2599
|
-
"session_not_found",
|
|
2600
|
-
`Codex session ${resumeSessionId} is not live; cannot resume`,
|
|
2601
|
-
"codex_session_not_found",
|
|
2602
|
-
)
|
|
2603
|
-
: sessionUnavailableResult(
|
|
2604
|
-
"session_busy",
|
|
2605
|
-
`Codex session ${resumeSessionId} is already executing a turn`,
|
|
2606
|
-
"codex_session_busy",
|
|
2607
|
-
);
|
|
2608
|
-
}
|
|
2609
|
-
resumeEntry = claimed.entry;
|
|
2610
|
-
if (resumeEntry.mcpConfigFingerprint !== mcpConfigFingerprint) {
|
|
2611
|
-
await codexSessions.dispose(resumeSessionId);
|
|
2612
|
-
return sessionUnavailableResult(
|
|
2613
|
-
"session_not_found",
|
|
2614
|
-
`Codex session ${resumeSessionId} was invalidated because its MCP configuration changed; retry without the provider session to replay history`,
|
|
2615
|
-
"codex_session_config_mismatch",
|
|
2616
|
-
);
|
|
2617
|
-
}
|
|
2618
|
-
// A retained thread keeps the MCP configuration sent with thread/start.
|
|
2619
|
-
sentMcpServerNames = noToolsProbe ? [] : configuredMcpServerNameList;
|
|
2620
|
-
}
|
|
2621
|
-
|
|
2622
|
-
try {
|
|
2623
|
-
if (resumeEntry) {
|
|
2624
|
-
client = resumeEntry.client;
|
|
2625
|
-
threadId = resumeEntry.threadId;
|
|
2626
|
-
resumeEntry.notificationTarget.handler = handleNotification;
|
|
2627
|
-
resumeEntry.serverRequestTarget.handler = handleServerRequest;
|
|
2628
|
-
// Keep the idle TTL from firing while the turn is in flight.
|
|
2629
|
-
codexSessions.touch(resumeSessionId, { idleTimeoutMs: sessionTtlMs });
|
|
2630
|
-
} else {
|
|
2631
|
-
client = createClient();
|
|
2632
|
-
}
|
|
2633
|
-
if (options.abortSignal) {
|
|
2634
|
-
if (options.abortSignal.aborted) abortHandler();
|
|
2635
|
-
else options.abortSignal.addEventListener("abort", abortHandler, { once: true });
|
|
2636
|
-
}
|
|
2637
|
-
if (!resumeEntry) await initializeClient(client);
|
|
2638
|
-
const fastMode = codexModelSupportsFastMode(resolved.model) && normalizeFastMode(options.fastMode, true);
|
|
2639
|
-
if (!resumeEntry) {
|
|
2640
|
-
const mcpServers = noToolsProbe ? {} : configuredMcpServers;
|
|
2641
|
-
// Incrementally assembled config handed across the codex app-server
|
|
2642
|
-
// boundary; the reasoning fields below are attached conditionally.
|
|
2643
|
-
const config = /** @type {any} */ ({
|
|
2644
|
-
...(fastMode ? { service_tier: "fast" } : {}),
|
|
2645
|
-
features: { fast_mode: fastMode },
|
|
2646
|
-
...(noToolsProbe
|
|
2647
|
-
? { mcp_servers: {} }
|
|
2648
|
-
: Object.keys(mcpServers).length
|
|
2649
|
-
? { mcp_servers: mcpServers }
|
|
2650
|
-
: {}),
|
|
2651
|
-
});
|
|
2652
|
-
if (normalizedEffort) {
|
|
2653
|
-
config.model_reasoning_effort = normalizedEffort;
|
|
2654
|
-
if (normalizedEffort !== "none") config.model_reasoning_summary = "auto";
|
|
2655
|
-
}
|
|
2656
|
-
sentMcpServerNames = Object.keys(mcpServers);
|
|
2657
|
-
// The codex app-server protocol exposes thread/start but no thread/load
|
|
2658
|
-
// primitive, so cold continuations always start a fresh thread; a
|
|
2659
|
-
// thread is only resumable while its subprocess stays live in
|
|
2660
|
-
// codexSessions (options.sessionKeepAlive + options.sessionId).
|
|
2661
|
-
const thread = await requestThreadStart({
|
|
2662
|
-
model: resolved.model,
|
|
2663
|
-
modelProvider: "openai",
|
|
2664
|
-
...(fastMode ? { serviceTier: "fast" } : {}),
|
|
2665
|
-
cwd: options.cwd || process.cwd(),
|
|
2666
|
-
approvalPolicy: approvalPolicyForRun(options),
|
|
2667
|
-
sandbox: sandboxForRun(options),
|
|
2668
|
-
config,
|
|
2669
|
-
serviceName: (options.toolContext?.runtimeBrand ?? readRuntimeBrand()).serviceName,
|
|
2670
|
-
developerInstructions: systemPrompt,
|
|
2671
|
-
ephemeral: true,
|
|
2672
|
-
sessionStartSource: "startup",
|
|
2673
|
-
...(noToolsProbe ? { environments: [], dynamicTools: [], selectedCapabilityRoots: [] } : {}),
|
|
2674
|
-
experimentalRawEvents: false,
|
|
2675
|
-
persistExtendedHistory: false,
|
|
2676
|
-
});
|
|
2677
|
-
threadId = thread?.thread?.id;
|
|
2678
|
-
if (!threadId) throw new Error("Codex app-server did not return a thread id");
|
|
2679
|
-
}
|
|
2680
|
-
|
|
2681
|
-
const steerTask = steerLiveInput();
|
|
2682
|
-
steerTask.catch((err) => {
|
|
2683
|
-
emitEvent({
|
|
2684
|
-
type: "runtime_warning",
|
|
2685
|
-
warning_kind: "live_input_failed",
|
|
2686
|
-
message: safeDiagnostic(err?.message || err),
|
|
2687
|
-
});
|
|
2688
|
-
});
|
|
2689
|
-
const turnParams = {
|
|
2690
|
-
threadId,
|
|
2691
|
-
input: userTextInput(prompt),
|
|
2692
|
-
cwd: options.cwd || process.cwd(),
|
|
2693
|
-
approvalPolicy: approvalPolicyForRun(options),
|
|
2694
|
-
sandboxPolicy: sandboxPolicyForRun(options),
|
|
2695
|
-
model: resolved.model,
|
|
2696
|
-
...(fastMode ? { serviceTier: "fast" } : {}),
|
|
2697
|
-
effort: normalizedEffort,
|
|
2698
|
-
summary: normalizedEffort && normalizedEffort !== "none" ? "auto" : "none",
|
|
2699
|
-
outputSchema: options.outputSchema,
|
|
2700
|
-
};
|
|
2701
|
-
const turn = await client.request("turn/start", turnParams);
|
|
2702
|
-
assertNoUnsupportedServerRequest();
|
|
2703
|
-
setActiveTurnId(turn?.turn?.id);
|
|
2704
|
-
|
|
2705
|
-
let prematureClose = false;
|
|
2706
|
-
// Resumed runs watch subprocess death through the entry's mutable hook
|
|
2707
|
-
// instead of client.closed.then: a long-lived thread would otherwise
|
|
2708
|
-
// accumulate one permanent .then closure per turn.
|
|
2709
|
-
const closedSignal = resumeEntry
|
|
2710
|
-
? new Promise((resolve) => { resumeEntry.closedTarget.handler = resolve; })
|
|
2711
|
-
: client.closed;
|
|
2712
|
-
// Resumed runs never close the client on abort, so the wait must also
|
|
2713
|
-
// resolve on the abort signal or an interrupted turn could hang forever.
|
|
2714
|
-
let abortRaceCleanup = () => {};
|
|
2715
|
-
const abortedSignal = resumeEntry && options.abortSignal
|
|
2716
|
-
? new Promise((resolve) => {
|
|
2717
|
-
if (options.abortSignal.aborted) {
|
|
2718
|
-
resolve(null);
|
|
2719
|
-
return;
|
|
2720
|
-
}
|
|
2721
|
-
const onAbort = () => resolve(null);
|
|
2722
|
-
options.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
2723
|
-
abortRaceCleanup = () => options.abortSignal.removeEventListener?.("abort", onAbort);
|
|
2724
|
-
})
|
|
2725
|
-
: null;
|
|
2726
|
-
try {
|
|
2727
|
-
await Promise.race([
|
|
2728
|
-
turnDone,
|
|
2729
|
-
...(abortedSignal === null ? [] : [abortedSignal]),
|
|
2730
|
-
closedSignal.then((err) => {
|
|
2731
|
-
if (!turnCompleted) {
|
|
2732
|
-
prematureClose = true;
|
|
2733
|
-
stopLiveInput();
|
|
2734
|
-
throw err || new Error("codex app-server closed");
|
|
2735
|
-
}
|
|
2736
|
-
return null;
|
|
2737
|
-
}),
|
|
2738
|
-
]);
|
|
2739
|
-
} catch (err) {
|
|
2740
|
-
if (prematureClose && !errorMessage) {
|
|
2741
|
-
errorMessage = safeDiagnostic(err?.message || "codex app-server stream closed before turn completed");
|
|
2742
|
-
failureKind = "provider_unavailable";
|
|
2743
|
-
} else if (!prematureClose) {
|
|
2744
|
-
throw err;
|
|
2745
|
-
}
|
|
2746
|
-
} finally {
|
|
2747
|
-
abortRaceCleanup();
|
|
2748
|
-
stopLiveInput();
|
|
2749
|
-
}
|
|
2750
|
-
turnCompleted = true;
|
|
2751
|
-
await steerTask;
|
|
2752
|
-
|
|
2753
|
-
const text = texts[texts.length - 1] || "";
|
|
2754
|
-
let codexErrorCode = prematureClose ? "codex_app_server_closed" : null;
|
|
2755
|
-
if (!errorMessage && !text.trim()) {
|
|
2756
|
-
errorMessage = "codex app-server completed without final output";
|
|
2757
|
-
failureKind = "provider_unavailable";
|
|
2758
|
-
codexErrorCode = codexErrorCode || "codex_app_server_no_output";
|
|
2759
|
-
}
|
|
2760
|
-
// The app-server owns native descendants. If a known descendant remains
|
|
2761
|
-
// open, or an unowned/stale descendant requests work, retaining this
|
|
2762
|
-
// process would let old execution cross into another logical turn. Close
|
|
2763
|
-
// the provider session after the unaffected root finishes; the host can
|
|
2764
|
-
// replay history on a fresh process.
|
|
2765
|
-
const hasUnfinishedProviderDescendants = [...subagentGroupsBySpawnId.values()]
|
|
2766
|
-
.some((group) => !group.finished);
|
|
2767
|
-
const providerSessionHasUnsafeDescendants = hasUnfinishedProviderDescendants
|
|
2768
|
-
|| unownedChildRequestObserved;
|
|
2769
|
-
if (resumeEntry) {
|
|
2770
|
-
// A failed turn or a closed transport leaves the thread untrustworthy,
|
|
2771
|
-
// but an interrupt is normal steering only when no provider descendant
|
|
2772
|
-
// remains live.
|
|
2773
|
-
const aborted = !!options.abortSignal?.aborted;
|
|
2774
|
-
sessionRetained = !providerSessionHasUnsafeDescendants
|
|
2775
|
-
&& ((aborted && !prematureClose) || (!errorMessage && !failureKind));
|
|
2776
|
-
if (sessionRetained) codexSessions.touch(resumeSessionId, { idleTimeoutMs: sessionTtlMs });
|
|
2777
|
-
else codexSessions.delete(resumeSessionId);
|
|
2778
|
-
} else if (
|
|
2779
|
-
keepAlive
|
|
2780
|
-
&& threadId
|
|
2781
|
-
&& !errorMessage
|
|
2782
|
-
&& !failureKind
|
|
2783
|
-
&& !options.abortSignal?.aborted
|
|
2784
|
-
&& !providerSessionHasUnsafeDescendants
|
|
2785
|
-
) {
|
|
2786
|
-
sessionRetained = true;
|
|
2787
|
-
notificationTarget.handler = noopNotificationHandler;
|
|
2788
|
-
serverRequestTarget.handler = rejectIdleServerRequest;
|
|
2789
|
-
const entry = {
|
|
2790
|
-
client,
|
|
2791
|
-
threadId,
|
|
2792
|
-
busy: false,
|
|
2793
|
-
notificationTarget,
|
|
2794
|
-
serverRequestTarget,
|
|
2795
|
-
mcpConfigFingerprint,
|
|
2796
|
-
closedTarget: { handler: null },
|
|
2797
|
-
};
|
|
2798
|
-
codexSessions.set(threadId, entry, { idleTimeoutMs: sessionTtlMs });
|
|
2799
|
-
client.closed.then(() => {
|
|
2800
|
-
codexSessions.delete(threadId);
|
|
2801
|
-
entry.closedTarget.handler?.(new Error("codex app-server closed"));
|
|
2802
|
-
});
|
|
2803
|
-
}
|
|
2804
|
-
const hadPartialProgress = events.length > 0 || texts.length > 0;
|
|
2805
|
-
const reference = requestedReference;
|
|
2806
|
-
const inputTokens = usage?.input_tokens ?? usage?.inputTokens ?? 0;
|
|
2807
|
-
const outputTokens = usage?.output_tokens ?? usage?.outputTokens ?? 0;
|
|
2808
|
-
const cachedTokens = usage?.cache_read_tokens ?? usage?.cachedInputTokens ?? 0;
|
|
2809
|
-
const cacheCreationTokens = usage?.cache_creation_tokens ?? usage?.cacheCreationTokens ?? 0;
|
|
2810
|
-
const billableInputTokens = Math.max(0, inputTokens - cachedTokens - cacheCreationTokens);
|
|
2811
|
-
const costUsd = estimateCost({
|
|
2812
|
-
resolveCustomPricing: options.resolveCustomPricing,
|
|
2813
|
-
model: reference,
|
|
2814
|
-
inputTokens: billableInputTokens,
|
|
2815
|
-
outputTokens,
|
|
2816
|
-
cachedTokens,
|
|
2817
|
-
cacheWriteTokens: cacheCreationTokens,
|
|
2818
|
-
});
|
|
2819
|
-
const enrichedUsage = {
|
|
2820
|
-
...usage,
|
|
2821
|
-
input_tokens: inputTokens || null,
|
|
2822
|
-
output_tokens: outputTokens || null,
|
|
2823
|
-
cache_read_tokens: cachedTokens || null,
|
|
2824
|
-
cache_creation_tokens: cacheCreationTokens || null,
|
|
2825
|
-
cost_usd: costUsd,
|
|
2826
|
-
};
|
|
2827
|
-
const subagentCapabilities = observedSubagentCapabilities();
|
|
2828
|
-
return {
|
|
2829
|
-
text,
|
|
2830
|
-
structuredResult: undefined,
|
|
2831
|
-
structuredResultSource: null,
|
|
2832
|
-
events,
|
|
2833
|
-
usage: enrichedUsage,
|
|
2834
|
-
durationMs: Date.now() - start,
|
|
2835
|
-
numTurns: 1,
|
|
2836
|
-
model: reference,
|
|
2837
|
-
effort: options.effort || null,
|
|
2838
|
-
sdk: "codex",
|
|
2839
|
-
providerSessionId: threadId || null,
|
|
2840
|
-
provider_session_id: threadId || null,
|
|
2841
|
-
cancelled: !!options.abortSignal?.aborted,
|
|
2842
|
-
error: errorMessage,
|
|
2843
|
-
failureKind,
|
|
2844
|
-
diagnostics: {
|
|
2845
|
-
...codexDiagnostics,
|
|
2846
|
-
...(codexErrorCode ? { codex_error_code: codexErrorCode } : {}),
|
|
2847
|
-
...(hadPartialProgress && failureKind === "provider_unavailable"
|
|
2848
|
-
? { had_partial_progress: true }
|
|
2849
|
-
: {}),
|
|
2850
|
-
},
|
|
2851
|
-
capabilitiesUsed: buildCapabilitiesUsed({
|
|
2852
|
-
promptCacheActive: (cachedTokens || 0) > 0 || (cacheCreationTokens || 0) > 0,
|
|
2853
|
-
thinkingEnabled: null,
|
|
2854
|
-
structuredOutputEnforced: !!options.outputSchema,
|
|
2855
|
-
subagentInvoked: subagentCapabilities.invoked,
|
|
2856
|
-
mcpServersUsed: sentMcpServerNames,
|
|
2857
|
-
nativeSubagentsUsed: subagentCapabilities.names,
|
|
2858
|
-
toolCompactionApplied: false,
|
|
2859
|
-
contextCompactionApplied: null,
|
|
2860
|
-
}),
|
|
2861
|
-
};
|
|
2862
|
-
} catch (err) {
|
|
2863
|
-
if (resumeEntry) codexSessions.delete(resumeSessionId);
|
|
2864
|
-
const subagentCapabilities = observedSubagentCapabilities();
|
|
2865
|
-
return {
|
|
2866
|
-
text: texts[texts.length - 1] || null,
|
|
2867
|
-
structuredResult: undefined,
|
|
2868
|
-
structuredResultSource: null,
|
|
2869
|
-
events,
|
|
2870
|
-
usage,
|
|
2871
|
-
durationMs: Date.now() - start,
|
|
2872
|
-
numTurns: texts.length || (events.length ? 1 : 0),
|
|
2873
|
-
model: resolved?.reference || `codex:${resolved?.model || ""}`,
|
|
2874
|
-
effort: options.effort || null,
|
|
2875
|
-
sdk: "codex",
|
|
2876
|
-
providerSessionId: threadId || null,
|
|
2877
|
-
provider_session_id: threadId || null,
|
|
2878
|
-
cancelled: !!options.abortSignal?.aborted,
|
|
2879
|
-
error: safeDiagnostic(err?.message || err),
|
|
2880
|
-
failureKind: failureKind || "provider_unavailable",
|
|
2881
|
-
diagnostics: {
|
|
2882
|
-
...codexDiagnostics,
|
|
2883
|
-
...codexErrorDiagnostics(err, sensitiveValues),
|
|
2884
|
-
...(events.length > 0 || texts.length > 0 ? { had_partial_progress: true } : {}),
|
|
2885
|
-
},
|
|
2886
|
-
capabilitiesUsed: buildCapabilitiesUsed({
|
|
2887
|
-
promptCacheActive: null,
|
|
2888
|
-
thinkingEnabled: null,
|
|
2889
|
-
structuredOutputEnforced: !!options.outputSchema,
|
|
2890
|
-
subagentInvoked: subagentCapabilities.invoked,
|
|
2891
|
-
mcpServersUsed: sentMcpServerNames,
|
|
2892
|
-
nativeSubagentsUsed: subagentCapabilities.names,
|
|
2893
|
-
toolCompactionApplied: false,
|
|
2894
|
-
contextCompactionApplied: null,
|
|
2895
|
-
}),
|
|
2896
|
-
};
|
|
2897
|
-
} finally {
|
|
2898
|
-
stopLiveInput();
|
|
2899
|
-
// The returned result is already being finalized. Park every transport
|
|
2900
|
-
// callback before lifecycle drains or close() so shutdown-time frames
|
|
2901
|
-
// cannot mutate its event array or stale per-run state.
|
|
2902
|
-
if (resumeEntry) {
|
|
2903
|
-
resumeEntry.notificationTarget.handler = noopNotificationHandler;
|
|
2904
|
-
resumeEntry.serverRequestTarget.handler = rejectIdleServerRequest;
|
|
2905
|
-
resumeEntry.closedTarget.handler = null;
|
|
2906
|
-
} else {
|
|
2907
|
-
notificationTarget.handler = noopNotificationHandler;
|
|
2908
|
-
serverRequestTarget.handler = rejectIdleServerRequest;
|
|
2909
|
-
}
|
|
2910
|
-
if ([...subagentGroupsBySpawnId.values()].some((group) => !group.finished)) {
|
|
2911
|
-
const cancelled = !!options.abortSignal?.aborted;
|
|
2912
|
-
const failed = Boolean(errorMessage || failureKind);
|
|
2913
|
-
finalizeOpenSubagents(
|
|
2914
|
-
cancelled ? "cancelled" : failed ? "failed" : "incomplete",
|
|
2915
|
-
cancelled
|
|
2916
|
-
? "Parent Codex turn was cancelled before the subagent completed."
|
|
2917
|
-
: failed
|
|
2918
|
-
? "Parent Codex turn failed before the subagent completed."
|
|
2919
|
-
: "Parent Codex turn ended before the subagent completed.",
|
|
2920
|
-
);
|
|
2921
|
-
}
|
|
2922
|
-
if (activeCompactions.size > 0) {
|
|
2923
|
-
const cancelled = !!options.abortSignal?.aborted;
|
|
2924
|
-
finalizeOpenCompactions(
|
|
2925
|
-
cancelled ? "cancelled" : "incomplete",
|
|
2926
|
-
cancelled ? "Compaction was interrupted." : "Compaction ended without a completion event.",
|
|
2927
|
-
);
|
|
2928
|
-
}
|
|
2929
|
-
options.abortSignal?.removeEventListener?.("abort", abortHandler);
|
|
2930
|
-
if (resumeEntry) {
|
|
2931
|
-
resumeEntry.busy = false;
|
|
2932
|
-
}
|
|
2933
|
-
if (!sessionRetained) await closeCodexClient(client);
|
|
2934
|
-
}
|
|
2935
|
-
}
|
|
2936
|
-
|
|
2937
|
-
// CLI bridge for sdk='codex' agents that opt into execution_mode='cli'. The
|
|
2938
|
-
// codex `app-server` is more capable than `codex exec` (better event
|
|
2939
|
-
// streaming, MCP support), so this is the default CLI path for Codex.
|
|
2940
|
-
export const codexAppRuntimeBridge = {
|
|
2941
|
-
id: "codex-app",
|
|
2942
|
-
kind: "codex-app",
|
|
2943
|
-
capabilities: CODEX_APP_CAPABILITIES,
|
|
2944
|
-
supports: (ref, options) => ref?.sdk === "codex" && options?.executionMode === "cli",
|
|
2945
|
-
execute: generateCodexAppResponse,
|
|
2946
|
-
};
|