@mono-agent/agent-runtime 0.20.10 → 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.
Files changed (101) hide show
  1. package/MIGRATION.md +264 -25
  2. package/README.md +139 -448
  3. package/package.json +7 -43
  4. package/src/agent/tools/agent-tool.js +5 -5
  5. package/src/agent/tools/bash.js +1 -24
  6. package/src/agent/tools/codex-subscription-search.js +1 -1
  7. package/src/agent/tools/index.js +7 -0
  8. package/src/agent/tools/monitor.js +140 -0
  9. package/src/agent/tools/pi-bridge.js +92 -7
  10. package/src/agent/tools/shared/bash-environment.js +31 -0
  11. package/src/agent/tools/shared/monitors.js +274 -0
  12. package/src/agent/tools/shared/process-runner.js +23 -5
  13. package/src/ai/cost.js +13 -68
  14. package/src/ai/index.js +4 -17
  15. package/src/ai/pi-interop.js +65 -1
  16. package/src/ai/pi-oauth-compat.js +1 -1
  17. package/src/ai/providers/codex/app-server-client.js +592 -0
  18. package/src/ai/providers/pi-models.js +18 -10
  19. package/src/ai/providers/pi-native/compaction-driver.js +49 -21
  20. package/src/ai/providers/pi-native/harness-adapter.js +338 -0
  21. package/src/ai/providers/pi-native/result-builder.js +10 -10
  22. package/src/ai/providers/pi-native/session-lifecycle.js +95 -40
  23. package/src/ai/providers/pi-native/stream-subscriber.js +23 -5
  24. package/src/ai/providers/pi-native/turn-runner.js +34 -15
  25. package/src/ai/providers/pi-native.js +48 -22
  26. package/src/ai/runtime/capabilities.js +11 -56
  27. package/src/ai/runtime/model-refs.js +118 -153
  28. package/src/ai/runtime/registry.js +22 -56
  29. package/src/ai/runtime/router.js +48 -408
  30. package/src/ai/runtime/session-liveness.js +3 -4
  31. package/src/ai/runtime/sessions.js +4 -5
  32. package/src/ai/runtime/tool-policy.js +0 -2
  33. package/src/ai/types.js +12 -108
  34. package/src/index.js +0 -6
  35. package/src/runtime.js +8 -14
  36. package/types/agent/tools/agent-tool.d.ts +1 -2
  37. package/types/agent/tools/codex-subscription-search.d.ts +1 -1
  38. package/types/agent/tools/index.d.ts +1 -0
  39. package/types/agent/tools/monitor.d.ts +44 -0
  40. package/types/agent/tools/pi-bridge.d.ts +3 -2
  41. package/types/agent/tools/shared/bash-environment.d.ts +4 -0
  42. package/types/agent/tools/shared/monitors.d.ts +83 -0
  43. package/types/agent/tools/shared/process-runner.d.ts +11 -2
  44. package/types/ai/cost.d.ts +1 -2
  45. package/types/ai/index.d.ts +1 -4
  46. package/types/ai/pi-interop.d.ts +20 -0
  47. package/types/ai/providers/codex/app-server-client.d.ts +37 -0
  48. package/types/ai/providers/pi-native/harness-adapter.d.ts +56 -0
  49. package/types/ai/providers/pi-native/result-builder.d.ts +3 -3
  50. package/types/ai/providers/pi-native/session-lifecycle.d.ts +2 -1
  51. package/types/ai/providers/pi-native/stream-subscriber.d.ts +2 -2
  52. package/types/ai/providers/pi-native/turn-runner.d.ts +33 -6
  53. package/types/ai/providers/pi-native.d.ts +21 -4
  54. package/types/ai/runtime/capabilities.d.ts +21 -70
  55. package/types/ai/runtime/model-refs.d.ts +0 -24
  56. package/types/ai/runtime/router.d.ts +3 -10
  57. package/types/ai/runtime/tool-policy.d.ts +0 -2
  58. package/types/ai/types.d.ts +28 -250
  59. package/types/index.d.ts +0 -1
  60. package/src/ai/providers/acp-client.js +0 -1149
  61. package/src/ai/providers/acp-privacy.js +0 -124
  62. package/src/ai/providers/acp-public.js +0 -21
  63. package/src/ai/providers/acp-session-tokens.js +0 -282
  64. package/src/ai/providers/acp-transport.js +0 -356
  65. package/src/ai/providers/acp.js +0 -543
  66. package/src/ai/providers/claude-cli.js +0 -883
  67. package/src/ai/providers/claude-sandbox.js +0 -71
  68. package/src/ai/providers/claude-sdk-discovery-worker.js +0 -53
  69. package/src/ai/providers/claude-sdk-discovery.js +0 -352
  70. package/src/ai/providers/claude-sdk.js +0 -1127
  71. package/src/ai/providers/claude-subagent-activity.js +0 -719
  72. package/src/ai/providers/claude-subagents.js +0 -88
  73. package/src/ai/providers/codex-app.js +0 -2946
  74. package/src/ai/providers/opencode-app.js +0 -1109
  75. package/src/ai/providers/opencode-discovery.js +0 -39
  76. package/src/ai/providers/opencode-server.js +0 -508
  77. package/src/ai/runtime/context-windows.js +0 -46
  78. package/src/ai/runtime/fast-mode.js +0 -8
  79. package/src/ai/streaming/codex-events.js +0 -146
  80. package/src/ai/streaming/opencode-events.js +0 -59
  81. package/types/ai/providers/acp-client.d.ts +0 -227
  82. package/types/ai/providers/acp-privacy.d.ts +0 -25
  83. package/types/ai/providers/acp-public.d.ts +0 -7
  84. package/types/ai/providers/acp-session-tokens.d.ts +0 -41
  85. package/types/ai/providers/acp-transport.d.ts +0 -45
  86. package/types/ai/providers/acp.d.ts +0 -93
  87. package/types/ai/providers/claude-cli.d.ts +0 -305
  88. package/types/ai/providers/claude-sandbox.d.ts +0 -79
  89. package/types/ai/providers/claude-sdk-discovery-worker.d.ts +0 -1
  90. package/types/ai/providers/claude-sdk-discovery.d.ts +0 -97
  91. package/types/ai/providers/claude-sdk.d.ts +0 -138
  92. package/types/ai/providers/claude-subagent-activity.d.ts +0 -53
  93. package/types/ai/providers/claude-subagents.d.ts +0 -18
  94. package/types/ai/providers/codex-app.d.ts +0 -151
  95. package/types/ai/providers/opencode-app.d.ts +0 -96
  96. package/types/ai/providers/opencode-discovery.d.ts +0 -4
  97. package/types/ai/providers/opencode-server.d.ts +0 -20
  98. package/types/ai/runtime/context-windows.d.ts +0 -9
  99. package/types/ai/runtime/fast-mode.d.ts +0 -2
  100. package/types/ai/streaming/codex-events.d.ts +0 -40
  101. 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
- };