@mono-agent/agent-runtime 0.18.1 → 0.18.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +32 -0
- package/README.md +50 -0
- package/package.json +1 -1
- package/src/agent/tools/agent-tool.js +16 -4
- package/src/agent/tools/web-controller.js +97 -7
- package/src/agent/tools/web-search.js +296 -60
- package/src/ai/providers/claude-cli.js +77 -22
- package/src/ai/providers/claude-sdk.js +54 -11
- package/src/ai/providers/claude-subagent-activity.js +719 -0
- package/src/ai/providers/codex-app.js +1039 -105
- package/src/ai/providers/pi-native/result-builder.js +11 -0
- package/src/ai/providers/pi-native.js +22 -2
- package/src/ai/providers/transport-errors.js +154 -0
- package/src/ai/runtime/router.js +42 -0
- package/src/ai/types.js +98 -10
- package/types/agent/tools/web-controller.d.ts +5 -1
- package/types/agent/tools/web-search.d.ts +13 -0
- package/types/ai/providers/claude-subagent-activity.d.ts +53 -0
- package/types/ai/providers/transport-errors.d.ts +44 -0
- package/types/ai/runtime/router.d.ts +9 -0
- package/types/ai/types.d.ts +231 -20
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { createInterface } from "node:readline";
|
|
3
4
|
import { normalizeCodexItemEvent } from "../streaming/codex-events.js";
|
|
4
5
|
import { createFileChangePayload } from "../file-change-stats.js";
|
|
@@ -26,6 +27,12 @@ const CODEX_DIAGNOSTIC_BYTES = 8 * 1024;
|
|
|
26
27
|
const CODEX_STDERR_TAIL_BYTES = 8 * 1024;
|
|
27
28
|
const CODEX_SHUTDOWN_GRACE_MS = 1_000;
|
|
28
29
|
const CODEX_KILL_GRACE_MS = 1_000;
|
|
30
|
+
const CODEX_APP_SERVER_ARGS = ["app-server", "--listen", "stdio://"];
|
|
31
|
+
const CODEX_APP_SERVER_ISOLATED_ARGS = [
|
|
32
|
+
...CODEX_APP_SERVER_ARGS,
|
|
33
|
+
"-c",
|
|
34
|
+
"project_doc_max_bytes=0",
|
|
35
|
+
];
|
|
29
36
|
|
|
30
37
|
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;
|
|
31
38
|
const SENSITIVE_HEADER_RE = /((?:(?:proxy-)?authorization|cookie|set-cookie)\s*[:=]\s*)[^\r\n]*/giu;
|
|
@@ -450,6 +457,9 @@ const CODEX_APP_CAPABILITIES = {
|
|
|
450
457
|
supports_skills: true,
|
|
451
458
|
supports_builtin_tools: true,
|
|
452
459
|
supports_live_input: true,
|
|
460
|
+
// Codex owns its collaboration agents and exposes their lifecycle through
|
|
461
|
+
// app-server. This does not mean the bridge can inject caller-defined
|
|
462
|
+
// nativeSubagents profiles; that richer request contract is rejected below.
|
|
453
463
|
supports_native_subagents: true,
|
|
454
464
|
supports_fast_mode: true,
|
|
455
465
|
tool_policy: TOOL_POLICY_ALLOW_ALL_ONLY,
|
|
@@ -574,6 +584,7 @@ const CODEX_NO_TOOL_ACTION_ITEMS = new Set([
|
|
|
574
584
|
"mcpToolCall",
|
|
575
585
|
"dynamicToolCall",
|
|
576
586
|
"collabAgentToolCall",
|
|
587
|
+
"collabToolCall",
|
|
577
588
|
"subAgentActivity",
|
|
578
589
|
"webSearch",
|
|
579
590
|
"imageView",
|
|
@@ -592,10 +603,30 @@ const CODEX_NO_TOOL_REQUEST_METHODS = new Set([
|
|
|
592
603
|
"execCommandApproval",
|
|
593
604
|
]);
|
|
594
605
|
|
|
606
|
+
// These protocol items are conversation or lifecycle records rather than
|
|
607
|
+
// actionable child work. Keep them out of the unknown-item fallback: message
|
|
608
|
+
// and reasoning content has dedicated handling, while the remaining records
|
|
609
|
+
// would otherwise create misleading tool rows.
|
|
610
|
+
const CODEX_PASSIVE_CHILD_ITEM_TYPES = new Set([
|
|
611
|
+
"userMessage",
|
|
612
|
+
"hookPrompt",
|
|
613
|
+
"agentMessage",
|
|
614
|
+
"plan",
|
|
615
|
+
"reasoning",
|
|
616
|
+
"enteredReviewMode",
|
|
617
|
+
"exitedReviewMode",
|
|
618
|
+
"contextCompaction",
|
|
619
|
+
"subAgentActivity",
|
|
620
|
+
]);
|
|
621
|
+
|
|
595
622
|
function codexMcpConfig(mcpServers = {}) {
|
|
596
623
|
const servers = {};
|
|
624
|
+
const invalidNames = [];
|
|
597
625
|
for (const [name, cfg] of Object.entries(mcpServers || {})) {
|
|
598
|
-
if (!/^[A-Za-z0-9_-]+$/.test(name))
|
|
626
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
627
|
+
invalidNames.push(name);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
599
630
|
if (cfg?.command) {
|
|
600
631
|
servers[name] = {
|
|
601
632
|
command: cfg.command,
|
|
@@ -614,7 +645,27 @@ function codexMcpConfig(mcpServers = {}) {
|
|
|
614
645
|
};
|
|
615
646
|
}
|
|
616
647
|
}
|
|
617
|
-
return servers;
|
|
648
|
+
return { servers, invalidNames };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function codexMcpServerNameProblem(invalidNames) {
|
|
652
|
+
if (!invalidNames.length) return null;
|
|
653
|
+
const names = invalidNames.map((name) => JSON.stringify(name)).join(", ");
|
|
654
|
+
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 "-".`;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function stableConfigJson(value) {
|
|
658
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
659
|
+
if (Array.isArray(value)) return `[${value.map((entry) => stableConfigJson(entry)).join(",")}]`;
|
|
660
|
+
return `{${Object.keys(value)
|
|
661
|
+
.filter((key) => value[key] !== undefined)
|
|
662
|
+
.sort()
|
|
663
|
+
.map((key) => `${JSON.stringify(key)}:${stableConfigJson(value[key])}`)
|
|
664
|
+
.join(",")}}`;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function codexMcpConfigFingerprint(config) {
|
|
668
|
+
return createHash("sha256").update(stableConfigJson(config)).digest("hex");
|
|
618
669
|
}
|
|
619
670
|
|
|
620
671
|
function codexConfiguredMcpApprovalResponse(request, configuredMcpServerNames) {
|
|
@@ -683,36 +734,6 @@ function withoutCodexRequestErrorDiagnostics(diagnostics) {
|
|
|
683
734
|
return rest;
|
|
684
735
|
}
|
|
685
736
|
|
|
686
|
-
function codexNativeTeammates(nativeSubagents) {
|
|
687
|
-
if (nativeSubagents?.provider !== "codex" || !Array.isArray(nativeSubagents.teammates)) return [];
|
|
688
|
-
return nativeSubagents.teammates.map((agent) => {
|
|
689
|
-
const name = String(agent?.name || "").trim();
|
|
690
|
-
if (!name) return null;
|
|
691
|
-
return {
|
|
692
|
-
name,
|
|
693
|
-
displayName: agent.displayName || name,
|
|
694
|
-
description: agent.description || "",
|
|
695
|
-
model: agent.model?.model || agent.modelRef || null,
|
|
696
|
-
reasoningEffort: agent.effort || null,
|
|
697
|
-
instructions: agent.helperSystemPrompt || agent.instructions || "",
|
|
698
|
-
};
|
|
699
|
-
}).filter(Boolean);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
function codexCollaborationModePayload(nativeSubagents, { model, effort, systemPrompt }) {
|
|
703
|
-
const teammates = codexNativeTeammates(nativeSubagents);
|
|
704
|
-
if (!teammates.length) return null;
|
|
705
|
-
return {
|
|
706
|
-
mode: "default",
|
|
707
|
-
teammates,
|
|
708
|
-
settings: {
|
|
709
|
-
model,
|
|
710
|
-
reasoningEffort: effort || null,
|
|
711
|
-
developerInstructions: systemPrompt,
|
|
712
|
-
},
|
|
713
|
-
};
|
|
714
|
-
}
|
|
715
|
-
|
|
716
737
|
/**
|
|
717
738
|
* @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]
|
|
718
739
|
*/
|
|
@@ -720,7 +741,7 @@ export function createCodexAppServerClient({
|
|
|
720
741
|
command = "codex",
|
|
721
742
|
// project_doc_max_bytes=0 keeps codex from injecting its own project docs;
|
|
722
743
|
// the host supplies the full context through developerInstructions.
|
|
723
|
-
args =
|
|
744
|
+
args = CODEX_APP_SERVER_ISOLATED_ARGS,
|
|
724
745
|
cwd,
|
|
725
746
|
env = {},
|
|
726
747
|
redactionValues = [],
|
|
@@ -1008,7 +1029,7 @@ function mapThreadItem(method, item) {
|
|
|
1008
1029
|
},
|
|
1009
1030
|
};
|
|
1010
1031
|
}
|
|
1011
|
-
if (item.type === "collabAgentToolCall") {
|
|
1032
|
+
if (item.type === "collabAgentToolCall" || item.type === "collabToolCall") {
|
|
1012
1033
|
const name = `codex_${item.tool || "subagent"}`;
|
|
1013
1034
|
if (method.endsWith("started")) {
|
|
1014
1035
|
return {
|
|
@@ -1022,7 +1043,8 @@ function mapThreadItem(method, item) {
|
|
|
1022
1043
|
prompt: item.prompt,
|
|
1023
1044
|
model: item.model,
|
|
1024
1045
|
reasoningEffort: item.reasoningEffort,
|
|
1025
|
-
receiverThreadIds: item.receiverThreadIds
|
|
1046
|
+
receiverThreadIds: item.receiverThreadIds
|
|
1047
|
+
|| [item.newThreadId, item.receiverThreadId].filter(Boolean),
|
|
1026
1048
|
},
|
|
1027
1049
|
}],
|
|
1028
1050
|
},
|
|
@@ -1036,7 +1058,8 @@ function mapThreadItem(method, item) {
|
|
|
1036
1058
|
tool_use_id: item.id,
|
|
1037
1059
|
content: {
|
|
1038
1060
|
status: item.status,
|
|
1039
|
-
receiverThreadIds: item.receiverThreadIds
|
|
1061
|
+
receiverThreadIds: item.receiverThreadIds
|
|
1062
|
+
|| [item.newThreadId, item.receiverThreadId].filter(Boolean),
|
|
1040
1063
|
agentsStates: item.agentsStates || [],
|
|
1041
1064
|
...(item.error ? { error: item.error } : {}),
|
|
1042
1065
|
},
|
|
@@ -1045,6 +1068,32 @@ function mapThreadItem(method, item) {
|
|
|
1045
1068
|
},
|
|
1046
1069
|
};
|
|
1047
1070
|
}
|
|
1071
|
+
if (item.type === "dynamicToolCall") {
|
|
1072
|
+
if (method.endsWith("started")) {
|
|
1073
|
+
return {
|
|
1074
|
+
type: "assistant",
|
|
1075
|
+
message: {
|
|
1076
|
+
content: [{
|
|
1077
|
+
type: "tool_use",
|
|
1078
|
+
id: item.id,
|
|
1079
|
+
name: item.namespace ? `${item.namespace}__${item.tool}` : item.tool,
|
|
1080
|
+
input: item.arguments || {},
|
|
1081
|
+
}],
|
|
1082
|
+
},
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
return {
|
|
1086
|
+
type: "user",
|
|
1087
|
+
message: {
|
|
1088
|
+
content: [{
|
|
1089
|
+
type: "tool_result",
|
|
1090
|
+
tool_use_id: item.id,
|
|
1091
|
+
content: item.contentItems || item.result || item.error || "",
|
|
1092
|
+
is_error: item.status === "failed" || item.success === false || Boolean(item.error),
|
|
1093
|
+
}],
|
|
1094
|
+
},
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1048
1097
|
if (item.type === "reasoning") {
|
|
1049
1098
|
const text = [...(item.summary || []), ...(item.content || [])].join("\n").trim();
|
|
1050
1099
|
return text ? { type: "assistant", message: { content: [{ type: "thinking", text }] } } : null;
|
|
@@ -1052,6 +1101,108 @@ function mapThreadItem(method, item) {
|
|
|
1052
1101
|
return null;
|
|
1053
1102
|
}
|
|
1054
1103
|
|
|
1104
|
+
function normalizedCollabTool(tool) {
|
|
1105
|
+
return String(tool || "")
|
|
1106
|
+
.replace(/[^A-Za-z0-9]+/gu, "")
|
|
1107
|
+
.toLowerCase();
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function isCodexCollabItem(item) {
|
|
1111
|
+
return item?.type === "collabAgentToolCall" || item?.type === "collabToolCall";
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function codexCollabReceiverEntries(item) {
|
|
1115
|
+
const entries = [];
|
|
1116
|
+
const seen = new Set();
|
|
1117
|
+
const add = (nativeId, source = {}) => {
|
|
1118
|
+
const id = typeof nativeId === "string" ? nativeId.trim() : "";
|
|
1119
|
+
if (!id || seen.has(id)) return;
|
|
1120
|
+
seen.add(id);
|
|
1121
|
+
const agentPath = typeof source.agentPath === "string" && source.agentPath.trim()
|
|
1122
|
+
? source.agentPath
|
|
1123
|
+
: undefined;
|
|
1124
|
+
const name = [source.name, source.nickname, source.agentNickname, source.agentRole]
|
|
1125
|
+
.find((value) => typeof value === "string" && value.trim());
|
|
1126
|
+
entries.push({
|
|
1127
|
+
nativeId: id,
|
|
1128
|
+
...(agentPath === undefined ? {} : { agentPath }),
|
|
1129
|
+
...(name === undefined ? {} : { name }),
|
|
1130
|
+
});
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
add(item?.newThreadId, item);
|
|
1134
|
+
add(item?.receiverThreadId, item);
|
|
1135
|
+
for (const id of item?.receiverThreadIds || []) add(id, item);
|
|
1136
|
+
for (const receiver of item?.receiverAgents || []) {
|
|
1137
|
+
add(receiver?.threadId ?? receiver?.id, receiver);
|
|
1138
|
+
}
|
|
1139
|
+
if (item?.agentStatus && typeof item.agentStatus === "object") {
|
|
1140
|
+
add(item.agentStatus.threadId ?? item.agentStatus.id, item.agentStatus);
|
|
1141
|
+
}
|
|
1142
|
+
for (const id of Object.keys(item?.agentsStates || {})) add(id, item?.agentsStates?.[id]);
|
|
1143
|
+
return entries;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function codexAgentName(agentPath, fallback = "codex") {
|
|
1147
|
+
const path = typeof agentPath === "string" ? agentPath.trim() : "";
|
|
1148
|
+
if (!path) return fallback;
|
|
1149
|
+
const segments = path.split("/").filter(Boolean);
|
|
1150
|
+
return segments.at(-1) || path;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function codexItemFailed(item) {
|
|
1154
|
+
const agentStatus = typeof item?.agentStatus === "string"
|
|
1155
|
+
? item.agentStatus
|
|
1156
|
+
: item?.agentStatus?.status;
|
|
1157
|
+
const status = String(item?.status || agentStatus || "").toLowerCase();
|
|
1158
|
+
const exitCode = item?.exitCode ?? item?.exit_code;
|
|
1159
|
+
return Boolean(
|
|
1160
|
+
item?.error
|
|
1161
|
+
|| item?.success === false
|
|
1162
|
+
|| status === "failed"
|
|
1163
|
+
|| status === "errored"
|
|
1164
|
+
|| status === "error"
|
|
1165
|
+
|| status === "interrupted"
|
|
1166
|
+
|| status === "cancelled"
|
|
1167
|
+
|| status === "notfound"
|
|
1168
|
+
|| (typeof exitCode === "number" && exitCode !== 0),
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function codexActivityContent(value) {
|
|
1173
|
+
if (typeof value === "string") return value;
|
|
1174
|
+
if (value === undefined || value === null) return "";
|
|
1175
|
+
try {
|
|
1176
|
+
return JSON.stringify(value);
|
|
1177
|
+
} catch {
|
|
1178
|
+
return String(value);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function codexChildToolName(item) {
|
|
1183
|
+
if (item?.type === "commandExecution") return "command_execution";
|
|
1184
|
+
if (item?.type === "mcpToolCall") {
|
|
1185
|
+
return item.server && item.tool ? `mcp__${item.server}__${item.tool}` : item.tool || "mcp_tool_call";
|
|
1186
|
+
}
|
|
1187
|
+
if (item?.type === "dynamicToolCall") {
|
|
1188
|
+
return item.namespace && item.tool ? `${item.namespace}__${item.tool}` : item.tool || "dynamic_tool_call";
|
|
1189
|
+
}
|
|
1190
|
+
return item?.tool || item?.type || "tool";
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function codexItemDurationMs(item) {
|
|
1194
|
+
const value = Number(item?.durationMs ?? item?.duration_ms);
|
|
1195
|
+
return Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function boundedCodexActivityItemId(value, limit = 256) {
|
|
1199
|
+
const id = String(value || "item");
|
|
1200
|
+
if (Buffer.byteLength(id) <= limit) return id;
|
|
1201
|
+
const digest = createHash("sha256").update(id).digest("hex");
|
|
1202
|
+
const suffix = `:${digest}`;
|
|
1203
|
+
return `${utf8Head(id, Math.max(0, limit - Buffer.byteLength(suffix)))}${suffix}`;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1055
1206
|
function usageFromTokenUsage(tokenUsage) {
|
|
1056
1207
|
const last = tokenUsage?.last || tokenUsage?.total || {};
|
|
1057
1208
|
return {
|
|
@@ -1083,6 +1234,11 @@ function contextUsageFromTokenUsage(tokenUsage) {
|
|
|
1083
1234
|
}
|
|
1084
1235
|
|
|
1085
1236
|
const noopNotificationHandler = () => {};
|
|
1237
|
+
const rejectIdleServerRequest = (request) => {
|
|
1238
|
+
throw new Error(
|
|
1239
|
+
`Codex app-server request arrived while its provider session was idle: ${String(request?.method || "unknown")}`,
|
|
1240
|
+
);
|
|
1241
|
+
};
|
|
1086
1242
|
|
|
1087
1243
|
async function closeCodexClient(client) {
|
|
1088
1244
|
if (!client?.close) return;
|
|
@@ -1122,7 +1278,13 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1122
1278
|
const makeClient = options.codexClientFactory || createCodexAppServerClient;
|
|
1123
1279
|
const keepAlive = options.sessionKeepAlive === true;
|
|
1124
1280
|
const noToolsProbe = options.codexNoToolsProbe === true;
|
|
1125
|
-
const
|
|
1281
|
+
const {
|
|
1282
|
+
servers: configuredMcpServers,
|
|
1283
|
+
invalidNames: invalidMcpServerNames,
|
|
1284
|
+
} = codexMcpConfig(options.mcpServers);
|
|
1285
|
+
const configuredMcpServerNameList = Object.keys(configuredMcpServers);
|
|
1286
|
+
const configuredMcpServerNames = new Set(configuredMcpServerNameList);
|
|
1287
|
+
const mcpConfigFingerprint = codexMcpConfigFingerprint(configuredMcpServers);
|
|
1126
1288
|
// The bridge TTL is a backstop behind the host's session policy; the grace
|
|
1127
1289
|
// keeps the host's lazy expiry firing first so eviction stays host-driven.
|
|
1128
1290
|
const sessionTtlMs = Number.isFinite(Number(options.sessionIdleTimeoutMs))
|
|
@@ -1141,6 +1303,13 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1141
1303
|
const events = [];
|
|
1142
1304
|
const texts = [];
|
|
1143
1305
|
const agentTextByItem = new Map();
|
|
1306
|
+
const childTextByItem = new Map();
|
|
1307
|
+
const childMessageDeltaCounts = new Map();
|
|
1308
|
+
const childReasoningDeltaCounts = new Map();
|
|
1309
|
+
const subagentGroupsBySpawnId = new Map();
|
|
1310
|
+
const subagentBindingsByThread = new Map();
|
|
1311
|
+
const pendingChildNotifications = new Map();
|
|
1312
|
+
const observedSubagentActivityItems = new Set();
|
|
1144
1313
|
const compactionStatuses = new Map();
|
|
1145
1314
|
const activeCompactions = new Map();
|
|
1146
1315
|
const nativeCompactionTurnKeys = new Set();
|
|
@@ -1155,11 +1324,13 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1155
1324
|
let codexDiagnostics = {};
|
|
1156
1325
|
let noToolsViolation = null;
|
|
1157
1326
|
let serverRequestViolation = null;
|
|
1327
|
+
let sentMcpServerNames = [];
|
|
1158
1328
|
let resolveTurn;
|
|
1159
1329
|
let resolveTurnReady;
|
|
1160
1330
|
let resolveLiveInputStop;
|
|
1161
1331
|
let turnReadyResolved = false;
|
|
1162
1332
|
let liveInputStopped = false;
|
|
1333
|
+
let subagentCallIndex = 0;
|
|
1163
1334
|
const fileChangeSnapshots = new Map();
|
|
1164
1335
|
const codexItemContext = {
|
|
1165
1336
|
fileChangePayload: (raw) => createFileChangePayload(raw, {
|
|
@@ -1290,6 +1461,642 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1290
1461
|
emitEvent({ type: "assistant", message: { content: [{ type: "text", text: safeText }] } });
|
|
1291
1462
|
}
|
|
1292
1463
|
|
|
1464
|
+
function notificationThreadId(params = {}) {
|
|
1465
|
+
if (typeof params.threadId === "string" && params.threadId) return params.threadId;
|
|
1466
|
+
if (typeof params.item?.senderThreadId === "string" && params.item.senderThreadId) {
|
|
1467
|
+
return params.item.senderThreadId;
|
|
1468
|
+
}
|
|
1469
|
+
return null;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function notificationTurnId(params = {}) {
|
|
1473
|
+
const candidate = params.turn?.id ?? params.turnId;
|
|
1474
|
+
return typeof candidate === "string" && candidate ? candidate : null;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function isRootThreadNotification(params = {}) {
|
|
1478
|
+
const sourceThreadId = notificationThreadId(params);
|
|
1479
|
+
return sourceThreadId === null || threadId === null || sourceThreadId === threadId;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function isRootActiveTurnNotification(params = {}) {
|
|
1483
|
+
if (!isRootThreadNotification(params)) return false;
|
|
1484
|
+
const sourceTurnId = notificationTurnId(params);
|
|
1485
|
+
return sourceTurnId === null || activeTurnId === null || sourceTurnId === activeTurnId;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function ensureSubagentGroup(spawnId, item = {}) {
|
|
1489
|
+
const id = typeof spawnId === "string" && spawnId ? spawnId : "codex-subagent";
|
|
1490
|
+
let group = subagentGroupsBySpawnId.get(id);
|
|
1491
|
+
if (!group) {
|
|
1492
|
+
group = {
|
|
1493
|
+
id,
|
|
1494
|
+
callIndex: subagentCallIndex++,
|
|
1495
|
+
name: "codex",
|
|
1496
|
+
prompt: typeof item.prompt === "string" ? safeDiagnostic(item.prompt, 2_000) : undefined,
|
|
1497
|
+
primaryThreadId: null,
|
|
1498
|
+
started: false,
|
|
1499
|
+
finished: false,
|
|
1500
|
+
startedAt: Date.now(),
|
|
1501
|
+
completedTurns: new Set(),
|
|
1502
|
+
bindings: new Set(),
|
|
1503
|
+
openActivities: new Map(),
|
|
1504
|
+
settledActivities: new Set(),
|
|
1505
|
+
primaryOutcome: null,
|
|
1506
|
+
};
|
|
1507
|
+
subagentGroupsBySpawnId.set(id, group);
|
|
1508
|
+
} else if (group.prompt === undefined && typeof item.prompt === "string") {
|
|
1509
|
+
group.prompt = safeDiagnostic(item.prompt, 2_000);
|
|
1510
|
+
}
|
|
1511
|
+
return group;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function metadataForSubagent(group, binding = null) {
|
|
1515
|
+
const agentPath = typeof binding?.agentPath === "string" && binding.agentPath.trim()
|
|
1516
|
+
? binding.agentPath
|
|
1517
|
+
: undefined;
|
|
1518
|
+
const name = binding?.name || codexAgentName(agentPath, group.name || "codex");
|
|
1519
|
+
return {
|
|
1520
|
+
id: group.id,
|
|
1521
|
+
...(binding?.nativeId ? { nativeId: binding.nativeId } : {}),
|
|
1522
|
+
name,
|
|
1523
|
+
callIndex: group.callIndex,
|
|
1524
|
+
...(agentPath === undefined ? {} : { label: agentPath, agentPath }),
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function observedSubagentCapabilities() {
|
|
1529
|
+
const names = [];
|
|
1530
|
+
const seenNames = new Set();
|
|
1531
|
+
for (const binding of subagentBindingsByThread.values()) {
|
|
1532
|
+
const name = metadataForSubagent(binding.group, binding).name;
|
|
1533
|
+
if (typeof name !== "string" || !name.trim() || seenNames.has(name)) continue;
|
|
1534
|
+
seenNames.add(name);
|
|
1535
|
+
names.push(name);
|
|
1536
|
+
}
|
|
1537
|
+
return {
|
|
1538
|
+
invoked: subagentGroupsBySpawnId.size > 0,
|
|
1539
|
+
names,
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
function emitSubagentActivity(group, binding, activity) {
|
|
1544
|
+
// Once the canonical terminal row is emitted, no late provider frame may
|
|
1545
|
+
// append more activity to that group.
|
|
1546
|
+
if (group.finished && activity.phase !== "agent_completed") return;
|
|
1547
|
+
if (activity.phase === "started" && typeof activity.id === "string") {
|
|
1548
|
+
if (group.openActivities.has(activity.id) || group.settledActivities.has(activity.id)) return;
|
|
1549
|
+
group.openActivities.set(activity.id, {
|
|
1550
|
+
binding,
|
|
1551
|
+
name: activity.name,
|
|
1552
|
+
startedAt: Date.now(),
|
|
1553
|
+
});
|
|
1554
|
+
} else if (activity.phase === "completed" && typeof activity.id === "string") {
|
|
1555
|
+
if (group.settledActivities.has(activity.id)) return;
|
|
1556
|
+
group.openActivities.delete(activity.id);
|
|
1557
|
+
group.settledActivities.add(activity.id);
|
|
1558
|
+
}
|
|
1559
|
+
emitEvent({
|
|
1560
|
+
type: "subagent_activity",
|
|
1561
|
+
subagent: metadataForSubagent(group, binding),
|
|
1562
|
+
...activity,
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
function drainOpenSubagentActivities(group, reason, onlyBinding = null) {
|
|
1567
|
+
for (const [id, activity] of [...group.openActivities]) {
|
|
1568
|
+
if (onlyBinding && activity.binding !== onlyBinding) continue;
|
|
1569
|
+
emitSubagentActivity(group, activity.binding, {
|
|
1570
|
+
phase: "completed",
|
|
1571
|
+
id,
|
|
1572
|
+
name: activity.name,
|
|
1573
|
+
isError: true,
|
|
1574
|
+
executionMs: Math.max(0, Date.now() - activity.startedAt),
|
|
1575
|
+
content: safeDiagnostic(reason, 2_000),
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
function ensureSubagentStarted(group, binding = null) {
|
|
1581
|
+
if (group.started) return;
|
|
1582
|
+
group.started = true;
|
|
1583
|
+
group.startedAt = Date.now();
|
|
1584
|
+
const subagent = metadataForSubagent(group, binding);
|
|
1585
|
+
group.name = subagent.name;
|
|
1586
|
+
emitSubagentActivity(group, binding, {
|
|
1587
|
+
phase: "agent_started",
|
|
1588
|
+
id: `agent:${group.id}`,
|
|
1589
|
+
name: `Agent(${subagent.name})`,
|
|
1590
|
+
arguments: {
|
|
1591
|
+
name: subagent.name,
|
|
1592
|
+
...(subagent.agentPath === undefined ? {} : { description: subagent.agentPath }),
|
|
1593
|
+
...(group.prompt === undefined ? {} : { prompt: group.prompt }),
|
|
1594
|
+
},
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/**
|
|
1599
|
+
* @param {any} group
|
|
1600
|
+
* @param {any} binding
|
|
1601
|
+
* @param {{status?: string, error?: any, content?: string, executionMs?: number}} [outcome]
|
|
1602
|
+
*/
|
|
1603
|
+
function finishSubagentGroup(group, binding, {
|
|
1604
|
+
status = "completed",
|
|
1605
|
+
error,
|
|
1606
|
+
content,
|
|
1607
|
+
executionMs,
|
|
1608
|
+
} = {}) {
|
|
1609
|
+
if (group.finished) return;
|
|
1610
|
+
ensureSubagentStarted(group, binding);
|
|
1611
|
+
const isError = Boolean(error) || codexItemFailed({ status });
|
|
1612
|
+
const subagent = metadataForSubagent(group, binding);
|
|
1613
|
+
const summary = content
|
|
1614
|
+
|| (error ? safeDiagnostic(error, 2_000) : `${status || "completed"}`);
|
|
1615
|
+
drainOpenSubagentActivities(
|
|
1616
|
+
group,
|
|
1617
|
+
error
|
|
1618
|
+
? safeDiagnostic(error, 2_000)
|
|
1619
|
+
: "Codex subagent ended before this activity completed.",
|
|
1620
|
+
);
|
|
1621
|
+
// Mark terminal before invoking host callbacks so even re-entrant provider
|
|
1622
|
+
// frames cannot append an event after this row.
|
|
1623
|
+
group.finished = true;
|
|
1624
|
+
emitSubagentActivity(group, binding, {
|
|
1625
|
+
phase: "agent_completed",
|
|
1626
|
+
id: `agent:${group.id}`,
|
|
1627
|
+
name: `Agent(${subagent.name})`,
|
|
1628
|
+
isError,
|
|
1629
|
+
...(Number.isFinite(executionMs) ? { executionMs } : {}),
|
|
1630
|
+
content: safeDiagnostic(summary, 2_000),
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
function maybeFinishSubagentGroup(group) {
|
|
1635
|
+
if (group.finished || !group.primaryOutcome) return;
|
|
1636
|
+
if ([...group.bindings].some((binding) => !binding.terminal)) return;
|
|
1637
|
+
finishSubagentGroup(
|
|
1638
|
+
group,
|
|
1639
|
+
group.primaryOutcome.binding,
|
|
1640
|
+
group.primaryOutcome.outcome,
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
function recordPrimarySubagentOutcome(group, binding, outcome) {
|
|
1645
|
+
if (!group.primaryOutcome) group.primaryOutcome = { binding, outcome };
|
|
1646
|
+
maybeFinishSubagentGroup(group);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function finalizeOpenSubagents(status, content) {
|
|
1650
|
+
for (const group of subagentGroupsBySpawnId.values()) {
|
|
1651
|
+
if (group.finished) continue;
|
|
1652
|
+
const binding = group.primaryThreadId
|
|
1653
|
+
? subagentBindingsByThread.get(group.primaryThreadId)
|
|
1654
|
+
: null;
|
|
1655
|
+
finishSubagentGroup(group, binding, { status, error: content, content });
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
function subagentActivityId(group, binding, itemId, suffix = "") {
|
|
1660
|
+
const nativeId = binding?.nativeId || "child";
|
|
1661
|
+
return `agent:${group.id}:${nativeId}:${itemId || "item"}${suffix}`;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
function flushPendingChildNotifications(nativeId) {
|
|
1665
|
+
const queued = pendingChildNotifications.get(nativeId);
|
|
1666
|
+
if (!queued?.length) return;
|
|
1667
|
+
pendingChildNotifications.delete(nativeId);
|
|
1668
|
+
for (const notification of queued) handleChildNotification(notification);
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
function bindSubagentThread(group, receiver, { primary = false, flush = true } = {}) {
|
|
1672
|
+
const nativeId = typeof receiver?.nativeId === "string" ? receiver.nativeId.trim() : "";
|
|
1673
|
+
if (!nativeId) return null;
|
|
1674
|
+
let binding = subagentBindingsByThread.get(nativeId);
|
|
1675
|
+
if (binding && binding.group !== group) return binding;
|
|
1676
|
+
if (!binding) {
|
|
1677
|
+
binding = {
|
|
1678
|
+
group,
|
|
1679
|
+
nativeId,
|
|
1680
|
+
agentPath: undefined,
|
|
1681
|
+
name: undefined,
|
|
1682
|
+
lastText: "",
|
|
1683
|
+
turnStartedAt: new Map(),
|
|
1684
|
+
turnStates: new Map(),
|
|
1685
|
+
terminal: false,
|
|
1686
|
+
usage: null,
|
|
1687
|
+
};
|
|
1688
|
+
subagentBindingsByThread.set(nativeId, binding);
|
|
1689
|
+
}
|
|
1690
|
+
group.bindings.add(binding);
|
|
1691
|
+
if (typeof receiver.agentPath === "string" && receiver.agentPath.trim()) {
|
|
1692
|
+
binding.agentPath = receiver.agentPath;
|
|
1693
|
+
binding.name = codexAgentName(receiver.agentPath, binding.name || group.name);
|
|
1694
|
+
}
|
|
1695
|
+
if (typeof receiver.name === "string" && receiver.name.trim()) binding.name = receiver.name.trim();
|
|
1696
|
+
// A root spawn can report multiple receiver ids. The first is the primary
|
|
1697
|
+
// result source; every additional binding remains part of the same group
|
|
1698
|
+
// and must terminate before the group terminal row is emitted.
|
|
1699
|
+
if (group.primaryThreadId === null && (primary || group.bindings.size === 1)) {
|
|
1700
|
+
group.primaryThreadId = nativeId;
|
|
1701
|
+
}
|
|
1702
|
+
ensureSubagentStarted(group, binding);
|
|
1703
|
+
if (flush) flushPendingChildNotifications(nativeId);
|
|
1704
|
+
return binding;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
function handleCollabSpawn(method, params, item, sourceBinding = null) {
|
|
1708
|
+
const isRootSpawn = sourceBinding === null;
|
|
1709
|
+
const group = sourceBinding?.group || ensureSubagentGroup(item.id, item);
|
|
1710
|
+
const receivers = codexCollabReceiverEntries(item);
|
|
1711
|
+
const bindings = [];
|
|
1712
|
+
for (const receiver of receivers) {
|
|
1713
|
+
const binding = bindSubagentThread(group, receiver, {
|
|
1714
|
+
primary: isRootSpawn,
|
|
1715
|
+
// Bind the complete receiver set before replaying out-of-order child
|
|
1716
|
+
// frames, otherwise the first queued completion can close the group
|
|
1717
|
+
// before later receivers are known to it.
|
|
1718
|
+
flush: false,
|
|
1719
|
+
});
|
|
1720
|
+
if (binding) bindings.push(binding);
|
|
1721
|
+
}
|
|
1722
|
+
for (const binding of bindings) flushPendingChildNotifications(binding.nativeId);
|
|
1723
|
+
if (sourceBinding) {
|
|
1724
|
+
const phase = method === "item/started" ? "started" : "completed";
|
|
1725
|
+
emitSubagentActivity(group, sourceBinding, {
|
|
1726
|
+
phase,
|
|
1727
|
+
id: subagentActivityId(group, sourceBinding, item.id),
|
|
1728
|
+
name: `${metadataForSubagent(group, sourceBinding).name}▸spawn_agent`,
|
|
1729
|
+
...(phase === "started"
|
|
1730
|
+
? { arguments: { prompt: item.prompt || "", receivers: receivers.map(({ nativeId }) => nativeId) } }
|
|
1731
|
+
: {
|
|
1732
|
+
isError: codexItemFailed(item),
|
|
1733
|
+
content: safeDiagnostic(codexActivityContent({
|
|
1734
|
+
status: item.status,
|
|
1735
|
+
receivers: receivers.map(({ nativeId }) => nativeId),
|
|
1736
|
+
...(item.error ? { error: item.error } : {}),
|
|
1737
|
+
}), 2_000),
|
|
1738
|
+
}),
|
|
1739
|
+
});
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
if (method === "item/completed" && receivers.length === 0 && codexItemFailed(item)) {
|
|
1743
|
+
finishSubagentGroup(group, null, {
|
|
1744
|
+
status: item.status || "failed",
|
|
1745
|
+
error: item.error || "Codex failed to spawn the subagent",
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
function handleSubAgentActivityItem(params, item, sourceBinding = null) {
|
|
1751
|
+
const sourceThreadId = notificationThreadId(params) || threadId || "root";
|
|
1752
|
+
const signalKey = `${sourceThreadId}:${item.id}:${item.kind}`;
|
|
1753
|
+
if (observedSubagentActivityItems.has(signalKey)) return;
|
|
1754
|
+
observedSubagentActivityItems.add(signalKey);
|
|
1755
|
+
|
|
1756
|
+
const targetNativeId = typeof item.agentThreadId === "string" ? item.agentThreadId : null;
|
|
1757
|
+
const targetBinding = targetNativeId ? subagentBindingsByThread.get(targetNativeId) : null;
|
|
1758
|
+
const group = sourceBinding?.group
|
|
1759
|
+
|| targetBinding?.group
|
|
1760
|
+
|| ensureSubagentGroup(item.id, item);
|
|
1761
|
+
const binding = targetNativeId
|
|
1762
|
+
? bindSubagentThread(group, {
|
|
1763
|
+
nativeId: targetNativeId,
|
|
1764
|
+
agentPath: item.agentPath,
|
|
1765
|
+
}, { primary: sourceBinding === null && targetBinding === undefined })
|
|
1766
|
+
: sourceBinding;
|
|
1767
|
+
|
|
1768
|
+
const kind = String(item.kind || "started");
|
|
1769
|
+
if (kind === "interrupted") {
|
|
1770
|
+
if (binding) {
|
|
1771
|
+
for (const turnId of binding.turnStates.keys()) binding.turnStates.set(turnId, "completed");
|
|
1772
|
+
binding.terminal = true;
|
|
1773
|
+
drainOpenSubagentActivities(
|
|
1774
|
+
group,
|
|
1775
|
+
`Codex subagent ${binding.agentPath || binding.nativeId} was interrupted before this activity completed.`,
|
|
1776
|
+
binding,
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
if (binding?.nativeId === group.primaryThreadId) {
|
|
1780
|
+
recordPrimarySubagentOutcome(group, binding, {
|
|
1781
|
+
status: "interrupted",
|
|
1782
|
+
error: `Codex subagent ${binding.agentPath || binding.nativeId} was interrupted`,
|
|
1783
|
+
});
|
|
1784
|
+
} else {
|
|
1785
|
+
emitSubagentActivity(group, binding || sourceBinding, {
|
|
1786
|
+
phase: "message",
|
|
1787
|
+
id: subagentActivityId(group, binding || sourceBinding, item.id, ":interrupted"),
|
|
1788
|
+
name: `${metadataForSubagent(group, binding || sourceBinding).name}▸status`,
|
|
1789
|
+
kind: "status",
|
|
1790
|
+
role: "assistant",
|
|
1791
|
+
content: `interrupted ${item.agentPath || targetNativeId || "subagent"}`,
|
|
1792
|
+
});
|
|
1793
|
+
maybeFinishSubagentGroup(group);
|
|
1794
|
+
}
|
|
1795
|
+
} else if (kind === "interacted" || sourceBinding) {
|
|
1796
|
+
emitSubagentActivity(group, binding || sourceBinding, {
|
|
1797
|
+
phase: "message",
|
|
1798
|
+
id: subagentActivityId(group, binding || sourceBinding, item.id, `:${kind}`),
|
|
1799
|
+
name: `${metadataForSubagent(group, binding || sourceBinding).name}▸status`,
|
|
1800
|
+
kind: "status",
|
|
1801
|
+
role: "assistant",
|
|
1802
|
+
content: `${kind} ${item.agentPath || targetNativeId || "subagent"}`,
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
function emitChildMessage(binding, {
|
|
1808
|
+
itemId,
|
|
1809
|
+
kind,
|
|
1810
|
+
content,
|
|
1811
|
+
index = 0,
|
|
1812
|
+
}) {
|
|
1813
|
+
const text = safeDiagnostic(content, 2_000);
|
|
1814
|
+
if (!text) return;
|
|
1815
|
+
const { group } = binding;
|
|
1816
|
+
emitSubagentActivity(group, binding, {
|
|
1817
|
+
phase: "message",
|
|
1818
|
+
id: subagentActivityId(group, binding, itemId, `:${kind}:${index}`),
|
|
1819
|
+
name: `${metadataForSubagent(group, binding).name}▸${kind}`,
|
|
1820
|
+
kind,
|
|
1821
|
+
role: "assistant",
|
|
1822
|
+
content: text,
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
function handleChildItem(method, params, binding) {
|
|
1827
|
+
const item = params.item;
|
|
1828
|
+
if (!item || typeof item !== "object") return;
|
|
1829
|
+
const { group } = binding;
|
|
1830
|
+
if (isCodexCollabItem(item) && normalizedCollabTool(item.tool) === "spawnagent") {
|
|
1831
|
+
handleCollabSpawn(method, params, item, binding);
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
if (item.type === "subAgentActivity") {
|
|
1835
|
+
handleSubAgentActivityItem(params, item, binding);
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
const messageKey = `${binding.nativeId}:${item.id}`;
|
|
1839
|
+
if (item.type === "agentMessage") {
|
|
1840
|
+
if (method !== "item/completed") return;
|
|
1841
|
+
const text = item.text || childTextByItem.get(messageKey) || "";
|
|
1842
|
+
binding.lastText = safeDiagnostic(text, 2_000);
|
|
1843
|
+
if (!childMessageDeltaCounts.has(messageKey)) {
|
|
1844
|
+
emitChildMessage(binding, { itemId: item.id, kind: "text", content: text });
|
|
1845
|
+
}
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
if (item.type === "reasoning") {
|
|
1849
|
+
if (method !== "item/completed" || childReasoningDeltaCounts.has(messageKey)) return;
|
|
1850
|
+
const text = [...(item.summary || []), ...(item.content || [])].join("\n").trim();
|
|
1851
|
+
emitChildMessage(binding, { itemId: item.id, kind: "thinking", content: text });
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
const raw = mapThreadItem(method, item);
|
|
1856
|
+
const normalized = /** @type {any} */ (normalizeCodexItemEvent(raw, codexItemContext) || raw);
|
|
1857
|
+
if (normalized?.type === "file_change") {
|
|
1858
|
+
const phase = method === "item/started" ? "started" : "completed";
|
|
1859
|
+
const executionMs = codexItemDurationMs(item);
|
|
1860
|
+
emitSubagentActivity(group, binding, {
|
|
1861
|
+
phase,
|
|
1862
|
+
id: subagentActivityId(group, binding, item.id),
|
|
1863
|
+
name: `${metadataForSubagent(group, binding).name}▸file_change`,
|
|
1864
|
+
...(phase === "started"
|
|
1865
|
+
? { arguments: { changes: normalized.changes || [], status: normalized.status } }
|
|
1866
|
+
: {
|
|
1867
|
+
isError: normalized.is_error === true,
|
|
1868
|
+
...(executionMs === undefined ? {} : { executionMs }),
|
|
1869
|
+
content: safeDiagnostic(codexActivityContent({
|
|
1870
|
+
changes: normalized.changes || [],
|
|
1871
|
+
status: normalized.status,
|
|
1872
|
+
...(normalized.error ? { error: normalized.error } : {}),
|
|
1873
|
+
}), 2_000),
|
|
1874
|
+
}),
|
|
1875
|
+
});
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
const blocks = normalized?.message?.content;
|
|
1879
|
+
if (Array.isArray(blocks)) {
|
|
1880
|
+
for (const block of blocks) {
|
|
1881
|
+
if (block?.type === "tool_use") {
|
|
1882
|
+
emitSubagentActivity(group, binding, {
|
|
1883
|
+
phase: "started",
|
|
1884
|
+
id: subagentActivityId(group, binding, block.id || item.id),
|
|
1885
|
+
name: `${metadataForSubagent(group, binding).name}▸${block.name || item.type || "tool"}`,
|
|
1886
|
+
arguments: block.input || {},
|
|
1887
|
+
});
|
|
1888
|
+
} else if (block?.type === "tool_result") {
|
|
1889
|
+
const executionMs = codexItemDurationMs(item);
|
|
1890
|
+
emitSubagentActivity(group, binding, {
|
|
1891
|
+
phase: "completed",
|
|
1892
|
+
id: subagentActivityId(group, binding, block.tool_use_id || item.id),
|
|
1893
|
+
name: `${metadataForSubagent(group, binding).name}▸${codexChildToolName(item)}`,
|
|
1894
|
+
isError: block.is_error === true,
|
|
1895
|
+
...(executionMs === undefined ? {} : { executionMs }),
|
|
1896
|
+
content: safeDiagnostic(codexActivityContent(block.content), 2_000),
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
if (item.type === "webSearch" || item.type === "imageView") {
|
|
1903
|
+
const phase = method === "item/started" ? "started" : "completed";
|
|
1904
|
+
emitSubagentActivity(group, binding, {
|
|
1905
|
+
phase,
|
|
1906
|
+
id: subagentActivityId(group, binding, item.id),
|
|
1907
|
+
name: `${metadataForSubagent(group, binding).name}▸${item.type}`,
|
|
1908
|
+
...(phase === "started"
|
|
1909
|
+
? { arguments: item.type === "webSearch" ? { query: item.query } : { path: item.path } }
|
|
1910
|
+
: {
|
|
1911
|
+
isError: codexItemFailed(item),
|
|
1912
|
+
content: safeDiagnostic(codexActivityContent(item.results || item.result || item.status), 2_000),
|
|
1913
|
+
}),
|
|
1914
|
+
});
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
if (item.type === "sleep") {
|
|
1918
|
+
const phase = method === "item/started" ? "started" : "completed";
|
|
1919
|
+
const durationMs = codexItemDurationMs(item);
|
|
1920
|
+
emitSubagentActivity(group, binding, {
|
|
1921
|
+
phase,
|
|
1922
|
+
id: subagentActivityId(group, binding, item.id),
|
|
1923
|
+
name: `${metadataForSubagent(group, binding).name}▸sleep`,
|
|
1924
|
+
...(phase === "started"
|
|
1925
|
+
? { arguments: durationMs === undefined ? {} : { durationMs } }
|
|
1926
|
+
: {
|
|
1927
|
+
isError: codexItemFailed(item),
|
|
1928
|
+
content: safeDiagnostic(codexActivityContent({
|
|
1929
|
+
...(durationMs === undefined ? {} : { durationMs }),
|
|
1930
|
+
...(item.status === undefined ? {} : { status: item.status }),
|
|
1931
|
+
...(item.error === undefined ? {} : { error: item.error }),
|
|
1932
|
+
}), 2_000),
|
|
1933
|
+
}),
|
|
1934
|
+
});
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
if (item.type === "imageGeneration") {
|
|
1938
|
+
const phase = method === "item/started" ? "started" : "completed";
|
|
1939
|
+
const startedDetails = {
|
|
1940
|
+
...(item.status === undefined ? {} : { status: safeDiagnostic(item.status, 128) }),
|
|
1941
|
+
...(item.revisedPrompt === undefined
|
|
1942
|
+
? {}
|
|
1943
|
+
: {
|
|
1944
|
+
revisedPrompt: item.revisedPrompt === null
|
|
1945
|
+
? null
|
|
1946
|
+
: safeDiagnostic(item.revisedPrompt, 1_000),
|
|
1947
|
+
}),
|
|
1948
|
+
};
|
|
1949
|
+
const details = {
|
|
1950
|
+
...(item.status === undefined ? {} : { status: safeDiagnostic(item.status, 128) }),
|
|
1951
|
+
...(item.savedPath === undefined ? {} : { savedPath: safeDiagnostic(item.savedPath, 512) }),
|
|
1952
|
+
...(item.revisedPrompt === undefined
|
|
1953
|
+
? {}
|
|
1954
|
+
: {
|
|
1955
|
+
revisedPrompt: item.revisedPrompt === null
|
|
1956
|
+
? null
|
|
1957
|
+
: safeDiagnostic(item.revisedPrompt, 1_000),
|
|
1958
|
+
}),
|
|
1959
|
+
...(item.result === undefined
|
|
1960
|
+
? {}
|
|
1961
|
+
: { resultBytes: Buffer.byteLength(String(item.result)) }),
|
|
1962
|
+
...(item.error === undefined ? {} : { error: safeDiagnostic(item.error, 512) }),
|
|
1963
|
+
};
|
|
1964
|
+
emitSubagentActivity(group, binding, {
|
|
1965
|
+
phase,
|
|
1966
|
+
id: subagentActivityId(group, binding, item.id),
|
|
1967
|
+
name: `${metadataForSubagent(group, binding).name}▸imageGeneration`,
|
|
1968
|
+
...(phase === "started"
|
|
1969
|
+
? { arguments: startedDetails }
|
|
1970
|
+
: {
|
|
1971
|
+
isError: codexItemFailed(item),
|
|
1972
|
+
content: safeDiagnostic(codexActivityContent(details), 2_000),
|
|
1973
|
+
}),
|
|
1974
|
+
});
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
if (
|
|
1978
|
+
CODEX_PASSIVE_CHILD_ITEM_TYPES.has(item.type)
|
|
1979
|
+
|| typeof item.type !== "string"
|
|
1980
|
+
|| !item.type.trim()
|
|
1981
|
+
|| typeof item.id !== "string"
|
|
1982
|
+
|| !item.id.trim()
|
|
1983
|
+
) {
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
// The protocol has no action/category discriminator. Favor visibility for
|
|
1987
|
+
// unknown non-passive items until the bridge gains a first-class mapper;
|
|
1988
|
+
// bound opaque details so a new provider payload cannot inflate the stream.
|
|
1989
|
+
const phase = method === "item/started" ? "started" : "completed";
|
|
1990
|
+
const executionMs = codexItemDurationMs(item);
|
|
1991
|
+
const itemId = boundedCodexActivityItemId(item.id);
|
|
1992
|
+
const itemType = safeDiagnostic(item.type, 128);
|
|
1993
|
+
emitSubagentActivity(group, binding, {
|
|
1994
|
+
phase,
|
|
1995
|
+
id: subagentActivityId(group, binding, itemId),
|
|
1996
|
+
name: `${metadataForSubagent(group, binding).name}▸${itemType}`,
|
|
1997
|
+
...(phase === "started"
|
|
1998
|
+
? { arguments: { item: safeDiagnostic(item, 1_000) } }
|
|
1999
|
+
: {
|
|
2000
|
+
isError: codexItemFailed(item),
|
|
2001
|
+
...(executionMs === undefined ? {} : { executionMs }),
|
|
2002
|
+
content: safeDiagnostic(item, 2_000),
|
|
2003
|
+
}),
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
function handleChildNotification(notification) {
|
|
2008
|
+
const { method, params = {} } = notification;
|
|
2009
|
+
const sourceThreadId = notificationThreadId(params);
|
|
2010
|
+
const binding = sourceThreadId ? subagentBindingsByThread.get(sourceThreadId) : null;
|
|
2011
|
+
if (!binding) {
|
|
2012
|
+
if (!sourceThreadId) return;
|
|
2013
|
+
const queued = pendingChildNotifications.get(sourceThreadId) || [];
|
|
2014
|
+
if (queued.length >= 1_000) queued.shift();
|
|
2015
|
+
queued.push(notification);
|
|
2016
|
+
pendingChildNotifications.set(sourceThreadId, queued);
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
const { group } = binding;
|
|
2020
|
+
if (method === "turn/started") {
|
|
2021
|
+
const childTurnId = notificationTurnId(params) || "turn";
|
|
2022
|
+
binding.turnStartedAt.set(childTurnId, Date.now());
|
|
2023
|
+
binding.turnStates.set(childTurnId, "running");
|
|
2024
|
+
binding.terminal = false;
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
if (method === "turn/completed") {
|
|
2028
|
+
const childTurnId = notificationTurnId(params) || "turn";
|
|
2029
|
+
const terminalKey = `${binding.nativeId}:${childTurnId}`;
|
|
2030
|
+
if (group.completedTurns.has(terminalKey)) return;
|
|
2031
|
+
group.completedTurns.add(terminalKey);
|
|
2032
|
+
const status = params.turn?.status || "completed";
|
|
2033
|
+
const error = params.turn?.error?.message || params.turn?.error;
|
|
2034
|
+
const startedAt = binding.turnStartedAt.get(childTurnId);
|
|
2035
|
+
const executionMs = startedAt === undefined ? undefined : Date.now() - startedAt;
|
|
2036
|
+
binding.turnStates.set(childTurnId, "completed");
|
|
2037
|
+
binding.terminal = [...binding.turnStates.values()].every((state) => state === "completed");
|
|
2038
|
+
drainOpenSubagentActivities(
|
|
2039
|
+
group,
|
|
2040
|
+
error
|
|
2041
|
+
? safeDiagnostic(error, 2_000)
|
|
2042
|
+
: `Codex subagent turn ${status} before this activity completed.`,
|
|
2043
|
+
binding,
|
|
2044
|
+
);
|
|
2045
|
+
if (binding.nativeId === group.primaryThreadId) {
|
|
2046
|
+
recordPrimarySubagentOutcome(group, binding, {
|
|
2047
|
+
status,
|
|
2048
|
+
error,
|
|
2049
|
+
content: error || binding.lastText || status,
|
|
2050
|
+
executionMs,
|
|
2051
|
+
});
|
|
2052
|
+
} else {
|
|
2053
|
+
emitSubagentActivity(group, binding, {
|
|
2054
|
+
phase: "message",
|
|
2055
|
+
id: subagentActivityId(group, binding, childTurnId, ":completed"),
|
|
2056
|
+
name: `${metadataForSubagent(group, binding).name}▸status`,
|
|
2057
|
+
kind: "status",
|
|
2058
|
+
role: "assistant",
|
|
2059
|
+
content: safeDiagnostic(error || `${status} ${binding.agentPath || binding.nativeId}`, 2_000),
|
|
2060
|
+
});
|
|
2061
|
+
maybeFinishSubagentGroup(group);
|
|
2062
|
+
}
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
if (method === "item/agentMessage/delta") {
|
|
2066
|
+
const itemId = params.itemId || "message";
|
|
2067
|
+
const key = `${binding.nativeId}:${itemId}`;
|
|
2068
|
+
const current = childTextByItem.get(key) || "";
|
|
2069
|
+
childTextByItem.set(key, `${current}${params.delta || ""}`);
|
|
2070
|
+
const index = childMessageDeltaCounts.get(key) || 0;
|
|
2071
|
+
childMessageDeltaCounts.set(key, index + 1);
|
|
2072
|
+
emitChildMessage(binding, { itemId, kind: "text", content: params.delta || "", index });
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
if (method === "item/reasoning/summaryTextDelta" || method === "item/reasoning/textDelta") {
|
|
2076
|
+
const itemId = params.itemId || "reasoning";
|
|
2077
|
+
const key = `${binding.nativeId}:${itemId}`;
|
|
2078
|
+
const index = childReasoningDeltaCounts.get(key) || 0;
|
|
2079
|
+
childReasoningDeltaCounts.set(key, index + 1);
|
|
2080
|
+
emitChildMessage(binding, { itemId, kind: "thinking", content: params.delta || "", index });
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
if (method === "item/started" || method === "item/completed") {
|
|
2084
|
+
handleChildItem(method, params, binding);
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
if (method === "thread/tokenUsage/updated") {
|
|
2088
|
+
binding.usage = usageFromTokenUsage(params.tokenUsage);
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
if (method === "warning" || method === "error" || method === "configWarning" || method === "guardianWarning") {
|
|
2092
|
+
emitChildMessage(binding, {
|
|
2093
|
+
itemId: notificationTurnId(params) || "warning",
|
|
2094
|
+
kind: method === "error" ? "error" : "warning",
|
|
2095
|
+
content: params.message || params.error || params,
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
|
|
1293
2100
|
function failNoToolsProbe(action) {
|
|
1294
2101
|
if (!noToolsProbe || noToolsViolation) return;
|
|
1295
2102
|
const safeAction = safeDiagnostic(action, 512);
|
|
@@ -1342,6 +2149,21 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1342
2149
|
}
|
|
1343
2150
|
}
|
|
1344
2151
|
|
|
2152
|
+
function rejectUnsupportedChildServerRequest(request, method, binding) {
|
|
2153
|
+
if (!binding) return false;
|
|
2154
|
+
const params = request?.params || {};
|
|
2155
|
+
const sourceThreadId = notificationThreadId(params);
|
|
2156
|
+
handleChildNotification({
|
|
2157
|
+
method: "error",
|
|
2158
|
+
params: {
|
|
2159
|
+
threadId: sourceThreadId,
|
|
2160
|
+
...(notificationTurnId(params) ? { turnId: notificationTurnId(params) } : {}),
|
|
2161
|
+
message: `Codex subagent requested unsupported client interaction (${safeDiagnostic(method, 512)}).`,
|
|
2162
|
+
},
|
|
2163
|
+
});
|
|
2164
|
+
return true;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
1345
2167
|
function handleNotification(notification) {
|
|
1346
2168
|
const safeNotification = sanitizeCodexNotification(notification, sensitiveValues);
|
|
1347
2169
|
const { method, params = {} } = safeNotification;
|
|
@@ -1355,12 +2177,19 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1355
2177
|
return;
|
|
1356
2178
|
}
|
|
1357
2179
|
}
|
|
2180
|
+
const sourceThreadId = notificationThreadId(params);
|
|
2181
|
+
if (threadId && sourceThreadId && sourceThreadId !== threadId) {
|
|
2182
|
+
handleChildNotification(safeNotification);
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
1358
2185
|
if (method === "turn/started") {
|
|
2186
|
+
if (!isRootThreadNotification(params)) return;
|
|
1359
2187
|
setActiveTurnId(params.turn?.id, { steerReady: true });
|
|
1360
2188
|
emitEvent({ type: "cli_event", raw: { type: "turn_started", turn: params.turn } });
|
|
1361
2189
|
return;
|
|
1362
2190
|
}
|
|
1363
2191
|
if (method === "turn/completed") {
|
|
2192
|
+
if (!isRootActiveTurnNotification(params)) return;
|
|
1364
2193
|
setActiveTurnId(params.turn?.id);
|
|
1365
2194
|
turnCompleted = true;
|
|
1366
2195
|
stopLiveInput();
|
|
@@ -1383,6 +2212,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1383
2212
|
return;
|
|
1384
2213
|
}
|
|
1385
2214
|
if (method === "thread/tokenUsage/updated") {
|
|
2215
|
+
if (!isRootActiveTurnNotification(params)) return;
|
|
1386
2216
|
usage = usageFromTokenUsage(params.tokenUsage);
|
|
1387
2217
|
const contextUsage = contextUsageFromTokenUsage(params.tokenUsage);
|
|
1388
2218
|
if (contextUsage) {
|
|
@@ -1400,19 +2230,23 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1400
2230
|
return;
|
|
1401
2231
|
}
|
|
1402
2232
|
if (method === "model/rerouted") {
|
|
2233
|
+
if (!isRootActiveTurnNotification(params)) return;
|
|
1403
2234
|
if (typeof params.toModel === "string" && params.toModel.trim().length > 0) actualModel = params.toModel;
|
|
1404
2235
|
return;
|
|
1405
2236
|
}
|
|
1406
2237
|
if (method === "thread/compacted") {
|
|
2238
|
+
if (!isRootActiveTurnNotification(params)) return;
|
|
1407
2239
|
handleLegacyCompaction(params);
|
|
1408
2240
|
return;
|
|
1409
2241
|
}
|
|
1410
2242
|
if (method === "item/agentMessage/delta") {
|
|
2243
|
+
if (!isRootActiveTurnNotification(params)) return;
|
|
1411
2244
|
const current = agentTextByItem.get(params.itemId) || "";
|
|
1412
2245
|
agentTextByItem.set(params.itemId, `${current}${params.delta || ""}`);
|
|
1413
2246
|
return;
|
|
1414
2247
|
}
|
|
1415
2248
|
if (method === "item/reasoning/summaryTextDelta" || method === "item/reasoning/textDelta") {
|
|
2249
|
+
if (!isRootActiveTurnNotification(params)) return;
|
|
1416
2250
|
emitEvent({ type: "assistant", message: { content: [{ type: "thinking", text: params.delta || "" }] } });
|
|
1417
2251
|
return;
|
|
1418
2252
|
}
|
|
@@ -1425,6 +2259,15 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1425
2259
|
return;
|
|
1426
2260
|
}
|
|
1427
2261
|
if (method === "item/started" || method === "item/completed") {
|
|
2262
|
+
if (!isRootActiveTurnNotification(params)) return;
|
|
2263
|
+
if (isCodexCollabItem(params.item) && normalizedCollabTool(params.item.tool) === "spawnagent") {
|
|
2264
|
+
handleCollabSpawn(method, params, params.item);
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
if (params.item?.type === "subAgentActivity") {
|
|
2268
|
+
handleSubAgentActivityItem(params, params.item);
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
1428
2271
|
if (params.item?.type === "contextCompaction") {
|
|
1429
2272
|
handleContextCompactionItem(method, params);
|
|
1430
2273
|
return;
|
|
@@ -1444,29 +2287,75 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1444
2287
|
let sessionRetained = false;
|
|
1445
2288
|
let abortRequested = false;
|
|
1446
2289
|
let interruptSent = false;
|
|
2290
|
+
let unownedChildRequestObserved = false;
|
|
1447
2291
|
// Mutable holder so keep-alive clients can outlive this run: each run
|
|
1448
|
-
// installs its own
|
|
1449
|
-
// once the session
|
|
2292
|
+
// installs its own notification and server-request handlers. The bridge
|
|
2293
|
+
// restores idle handlers once the session is no longer executing a turn.
|
|
1450
2294
|
const notificationTarget = { handler: handleNotification };
|
|
2295
|
+
function handleServerRequest(request) {
|
|
2296
|
+
const method = typeof request?.method === "string" ? request.method : "unknown";
|
|
2297
|
+
// A same-batch request can arrive after turn/completed but before the
|
|
2298
|
+
// generate promise reaches finally. Never let that frame approve work or
|
|
2299
|
+
// mutate the already-final provider result.
|
|
2300
|
+
if (turnCompleted || abortRequested || !activeTurnId) return rejectIdleServerRequest(request);
|
|
2301
|
+
const params = request?.params || {};
|
|
2302
|
+
const sourceThreadId = notificationThreadId(params);
|
|
2303
|
+
const sourceTurnId = notificationTurnId(params);
|
|
2304
|
+
const isChildRequest = Boolean(
|
|
2305
|
+
threadId && sourceThreadId && sourceThreadId !== threadId,
|
|
2306
|
+
);
|
|
2307
|
+
if (!isChildRequest && sourceTurnId !== activeTurnId) {
|
|
2308
|
+
throw new Error(
|
|
2309
|
+
`Codex app-server request does not match the active turn: ${method}`,
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
// A foreign thread id is not enough to establish child ownership. Retained
|
|
2313
|
+
// app-server processes can still deliver work from an earlier logical run;
|
|
2314
|
+
// only a live binding created by this run's root/descendant spawn records
|
|
2315
|
+
// may inherit configured-MCP approval.
|
|
2316
|
+
const currentChildBinding = isChildRequest
|
|
2317
|
+
? subagentBindingsByThread.get(sourceThreadId)
|
|
2318
|
+
: null;
|
|
2319
|
+
if (isChildRequest && (
|
|
2320
|
+
!currentChildBinding
|
|
2321
|
+
|| currentChildBinding.terminal
|
|
2322
|
+
|| currentChildBinding.group.finished
|
|
2323
|
+
|| !sourceTurnId
|
|
2324
|
+
|| currentChildBinding.turnStates.get(sourceTurnId) !== "running"
|
|
2325
|
+
)) {
|
|
2326
|
+
unownedChildRequestObserved = true;
|
|
2327
|
+
throw new Error(`Unsupported Codex app-server request: ${method}`);
|
|
2328
|
+
}
|
|
2329
|
+
const approval = noToolsProbe
|
|
2330
|
+
? null
|
|
2331
|
+
: codexConfiguredMcpApprovalResponse(request, configuredMcpServerNames);
|
|
2332
|
+
if (approval) return approval;
|
|
2333
|
+
// A child turn can fail an interaction it cannot service without
|
|
2334
|
+
// terminating the still-active root turn. The app-server receives the
|
|
2335
|
+
// JSON-RPC error and reports the child lifecycle separately.
|
|
2336
|
+
if (rejectUnsupportedChildServerRequest(request, method, currentChildBinding)) {
|
|
2337
|
+
throw new Error(`Unsupported Codex app-server request: ${method}`);
|
|
2338
|
+
}
|
|
2339
|
+
failUnsupportedServerRequest(method);
|
|
2340
|
+
throw new Error(`Unsupported Codex app-server request: ${method}`);
|
|
2341
|
+
}
|
|
2342
|
+
const serverRequestTarget = { handler: handleServerRequest };
|
|
1451
2343
|
function createClient() {
|
|
2344
|
+
const args = options.codexAppServerArgs !== undefined
|
|
2345
|
+
? options.codexAppServerArgs
|
|
2346
|
+
: options.codexLoadProjectDocs === true
|
|
2347
|
+
? CODEX_APP_SERVER_ARGS
|
|
2348
|
+
: CODEX_APP_SERVER_ISOLATED_ARGS;
|
|
1452
2349
|
return makeClient({
|
|
1453
2350
|
command: options.codexAppServerCommand,
|
|
1454
|
-
args
|
|
2351
|
+
args,
|
|
1455
2352
|
cwd: options.cwd,
|
|
1456
2353
|
env: options.codexAppServerEnv,
|
|
1457
2354
|
redactionValues: sensitiveValues,
|
|
1458
2355
|
onNotification: (notification) => notificationTarget.handler(
|
|
1459
2356
|
sanitizeCodexNotification(notification, sensitiveValues),
|
|
1460
2357
|
),
|
|
1461
|
-
onServerRequest: (request) =>
|
|
1462
|
-
const method = typeof request?.method === "string" ? request.method : "unknown";
|
|
1463
|
-
const approval = noToolsProbe
|
|
1464
|
-
? null
|
|
1465
|
-
: codexConfiguredMcpApprovalResponse(request, configuredMcpServerNames);
|
|
1466
|
-
if (approval) return approval;
|
|
1467
|
-
failUnsupportedServerRequest(method);
|
|
1468
|
-
throw new Error(`Unsupported Codex app-server request: ${method}`);
|
|
1469
|
-
},
|
|
2358
|
+
onServerRequest: (request) => serverRequestTarget.handler(request),
|
|
1470
2359
|
});
|
|
1471
2360
|
}
|
|
1472
2361
|
|
|
@@ -1615,6 +2504,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1615
2504
|
}
|
|
1616
2505
|
|
|
1617
2506
|
function sessionUnavailableResult(kind, error, codexErrorCode) {
|
|
2507
|
+
const subagentCapabilities = observedSubagentCapabilities();
|
|
1618
2508
|
return {
|
|
1619
2509
|
text: null,
|
|
1620
2510
|
structuredResult: undefined,
|
|
@@ -1636,15 +2526,35 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1636
2526
|
promptCacheActive: null,
|
|
1637
2527
|
thinkingEnabled: null,
|
|
1638
2528
|
structuredOutputEnforced: !!options.outputSchema,
|
|
1639
|
-
subagentInvoked:
|
|
1640
|
-
mcpServersUsed:
|
|
1641
|
-
nativeSubagentsUsed:
|
|
2529
|
+
subagentInvoked: subagentCapabilities.invoked,
|
|
2530
|
+
mcpServersUsed: sentMcpServerNames,
|
|
2531
|
+
nativeSubagentsUsed: subagentCapabilities.names,
|
|
1642
2532
|
toolCompactionApplied: false,
|
|
1643
2533
|
contextCompactionApplied: null,
|
|
1644
2534
|
}),
|
|
1645
2535
|
};
|
|
1646
2536
|
}
|
|
1647
2537
|
|
|
2538
|
+
if (
|
|
2539
|
+
Array.isArray(options.nativeSubagents?.teammates)
|
|
2540
|
+
&& options.nativeSubagents.teammates.length > 0
|
|
2541
|
+
) {
|
|
2542
|
+
return sessionUnavailableResult(
|
|
2543
|
+
"skipped_capability_mismatch",
|
|
2544
|
+
"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.",
|
|
2545
|
+
"codex_native_subagent_definitions_unsupported",
|
|
2546
|
+
);
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
const mcpServerNameProblem = codexMcpServerNameProblem(invalidMcpServerNames);
|
|
2550
|
+
if (mcpServerNameProblem) {
|
|
2551
|
+
return sessionUnavailableResult(
|
|
2552
|
+
"skipped_capability_mismatch",
|
|
2553
|
+
mcpServerNameProblem,
|
|
2554
|
+
"codex_mcp_server_name_invalid",
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2557
|
+
|
|
1648
2558
|
if (resolveSandboxPolicy(options.toolContext, options.sandboxPolicy) !== undefined) {
|
|
1649
2559
|
return sessionUnavailableResult(
|
|
1650
2560
|
"skipped_capability_mismatch",
|
|
@@ -1684,6 +2594,16 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1684
2594
|
);
|
|
1685
2595
|
}
|
|
1686
2596
|
resumeEntry = claimed.entry;
|
|
2597
|
+
if (resumeEntry.mcpConfigFingerprint !== mcpConfigFingerprint) {
|
|
2598
|
+
await codexSessions.dispose(resumeSessionId);
|
|
2599
|
+
return sessionUnavailableResult(
|
|
2600
|
+
"session_not_found",
|
|
2601
|
+
`Codex session ${resumeSessionId} was invalidated because its MCP configuration changed; retry without the provider session to replay history`,
|
|
2602
|
+
"codex_session_config_mismatch",
|
|
2603
|
+
);
|
|
2604
|
+
}
|
|
2605
|
+
// A retained thread keeps the MCP configuration sent with thread/start.
|
|
2606
|
+
sentMcpServerNames = noToolsProbe ? [] : configuredMcpServerNameList;
|
|
1687
2607
|
}
|
|
1688
2608
|
|
|
1689
2609
|
try {
|
|
@@ -1691,6 +2611,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1691
2611
|
client = resumeEntry.client;
|
|
1692
2612
|
threadId = resumeEntry.threadId;
|
|
1693
2613
|
resumeEntry.notificationTarget.handler = handleNotification;
|
|
2614
|
+
resumeEntry.serverRequestTarget.handler = handleServerRequest;
|
|
1694
2615
|
// Keep the idle TTL from firing while the turn is in flight.
|
|
1695
2616
|
codexSessions.touch(resumeSessionId, { idleTimeoutMs: sessionTtlMs });
|
|
1696
2617
|
} else {
|
|
@@ -1701,30 +2622,9 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1701
2622
|
else options.abortSignal.addEventListener("abort", abortHandler, { once: true });
|
|
1702
2623
|
}
|
|
1703
2624
|
if (!resumeEntry) await initializeClient(client);
|
|
1704
|
-
let collaborationMode = noToolsProbe
|
|
1705
|
-
? null
|
|
1706
|
-
: codexCollaborationModePayload(options.nativeSubagents, {
|
|
1707
|
-
model: resolved.model,
|
|
1708
|
-
effort: normalizedEffort,
|
|
1709
|
-
systemPrompt,
|
|
1710
|
-
});
|
|
1711
|
-
if (collaborationMode) {
|
|
1712
|
-
try {
|
|
1713
|
-
await client.request("collaborationMode/list", {}, { timeoutMs: 5_000 });
|
|
1714
|
-
} catch (err) {
|
|
1715
|
-
emitEvent({
|
|
1716
|
-
type: "runtime_warning",
|
|
1717
|
-
warning_kind: "codex_collaboration_mode_unavailable",
|
|
1718
|
-
message: safeDiagnostic(codexErrorMessage(
|
|
1719
|
-
err?.responseError ? safeResponseError(err.responseError) : err,
|
|
1720
|
-
)),
|
|
1721
|
-
});
|
|
1722
|
-
collaborationMode = null;
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
2625
|
const fastMode = codexModelSupportsFastMode(resolved.model) && normalizeFastMode(options.fastMode, true);
|
|
1726
2626
|
if (!resumeEntry) {
|
|
1727
|
-
const mcpServers = noToolsProbe ? {} :
|
|
2627
|
+
const mcpServers = noToolsProbe ? {} : configuredMcpServers;
|
|
1728
2628
|
// Incrementally assembled config handed across the codex app-server
|
|
1729
2629
|
// boundary; the reasoning fields below are attached conditionally.
|
|
1730
2630
|
const config = /** @type {any} */ ({
|
|
@@ -1740,6 +2640,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1740
2640
|
config.model_reasoning_effort = normalizedEffort;
|
|
1741
2641
|
if (normalizedEffort !== "none") config.model_reasoning_summary = "auto";
|
|
1742
2642
|
}
|
|
2643
|
+
sentMcpServerNames = Object.keys(mcpServers);
|
|
1743
2644
|
// The codex app-server protocol exposes thread/start but no thread/load
|
|
1744
2645
|
// primitive, so cold continuations always start a fresh thread; a
|
|
1745
2646
|
// thread is only resumable while its subprocess stays live in
|
|
@@ -1783,26 +2684,9 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1783
2684
|
effort: normalizedEffort,
|
|
1784
2685
|
summary: normalizedEffort && normalizedEffort !== "none" ? "auto" : "none",
|
|
1785
2686
|
outputSchema: options.outputSchema,
|
|
1786
|
-
...(collaborationMode ? { collaborationMode } : {}),
|
|
1787
2687
|
};
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
turn = await client.request("turn/start", turnParams);
|
|
1791
|
-
assertNoUnsupportedServerRequest();
|
|
1792
|
-
} catch (err) {
|
|
1793
|
-
if (!collaborationMode) throw err;
|
|
1794
|
-
emitEvent({
|
|
1795
|
-
type: "runtime_warning",
|
|
1796
|
-
warning_kind: "codex_collaboration_mode_rejected",
|
|
1797
|
-
message: safeDiagnostic(codexErrorMessage(
|
|
1798
|
-
err?.responseError ? safeResponseError(err.responseError) : err,
|
|
1799
|
-
)),
|
|
1800
|
-
});
|
|
1801
|
-
const fallbackParams = { ...turnParams };
|
|
1802
|
-
delete fallbackParams.collaborationMode;
|
|
1803
|
-
turn = await client.request("turn/start", fallbackParams);
|
|
1804
|
-
assertNoUnsupportedServerRequest();
|
|
1805
|
-
}
|
|
2688
|
+
const turn = await client.request("turn/start", turnParams);
|
|
2689
|
+
assertNoUnsupportedServerRequest();
|
|
1806
2690
|
setActiveTurnId(turn?.turn?.id);
|
|
1807
2691
|
|
|
1808
2692
|
let prematureClose = false;
|
|
@@ -1860,17 +2744,44 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1860
2744
|
failureKind = "provider_unavailable";
|
|
1861
2745
|
codexErrorCode = codexErrorCode || "codex_app_server_no_output";
|
|
1862
2746
|
}
|
|
2747
|
+
// The app-server owns native descendants. If a known descendant remains
|
|
2748
|
+
// open, or an unowned/stale descendant requests work, retaining this
|
|
2749
|
+
// process would let old execution cross into another logical turn. Close
|
|
2750
|
+
// the provider session after the unaffected root finishes; the host can
|
|
2751
|
+
// replay history on a fresh process.
|
|
2752
|
+
const hasUnfinishedProviderDescendants = [...subagentGroupsBySpawnId.values()]
|
|
2753
|
+
.some((group) => !group.finished);
|
|
2754
|
+
const providerSessionHasUnsafeDescendants = hasUnfinishedProviderDescendants
|
|
2755
|
+
|| unownedChildRequestObserved;
|
|
1863
2756
|
if (resumeEntry) {
|
|
1864
2757
|
// A failed turn or a closed transport leaves the thread untrustworthy,
|
|
1865
|
-
// but an interrupt is normal steering
|
|
2758
|
+
// but an interrupt is normal steering only when no provider descendant
|
|
2759
|
+
// remains live.
|
|
1866
2760
|
const aborted = !!options.abortSignal?.aborted;
|
|
1867
|
-
sessionRetained =
|
|
2761
|
+
sessionRetained = !providerSessionHasUnsafeDescendants
|
|
2762
|
+
&& ((aborted && !prematureClose) || (!errorMessage && !failureKind));
|
|
1868
2763
|
if (sessionRetained) codexSessions.touch(resumeSessionId, { idleTimeoutMs: sessionTtlMs });
|
|
1869
2764
|
else codexSessions.delete(resumeSessionId);
|
|
1870
|
-
} else if (
|
|
2765
|
+
} else if (
|
|
2766
|
+
keepAlive
|
|
2767
|
+
&& threadId
|
|
2768
|
+
&& !errorMessage
|
|
2769
|
+
&& !failureKind
|
|
2770
|
+
&& !options.abortSignal?.aborted
|
|
2771
|
+
&& !providerSessionHasUnsafeDescendants
|
|
2772
|
+
) {
|
|
1871
2773
|
sessionRetained = true;
|
|
1872
2774
|
notificationTarget.handler = noopNotificationHandler;
|
|
1873
|
-
|
|
2775
|
+
serverRequestTarget.handler = rejectIdleServerRequest;
|
|
2776
|
+
const entry = {
|
|
2777
|
+
client,
|
|
2778
|
+
threadId,
|
|
2779
|
+
busy: false,
|
|
2780
|
+
notificationTarget,
|
|
2781
|
+
serverRequestTarget,
|
|
2782
|
+
mcpConfigFingerprint,
|
|
2783
|
+
closedTarget: { handler: null },
|
|
2784
|
+
};
|
|
1874
2785
|
codexSessions.set(threadId, entry, { idleTimeoutMs: sessionTtlMs });
|
|
1875
2786
|
client.closed.then(() => {
|
|
1876
2787
|
codexSessions.delete(threadId);
|
|
@@ -1900,6 +2811,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1900
2811
|
cache_creation_tokens: cacheCreationTokens || null,
|
|
1901
2812
|
cost_usd: costUsd,
|
|
1902
2813
|
};
|
|
2814
|
+
const subagentCapabilities = observedSubagentCapabilities();
|
|
1903
2815
|
return {
|
|
1904
2816
|
text,
|
|
1905
2817
|
structuredResult: undefined,
|
|
@@ -1927,15 +2839,16 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1927
2839
|
promptCacheActive: (cachedTokens || 0) > 0 || (cacheCreationTokens || 0) > 0,
|
|
1928
2840
|
thinkingEnabled: null,
|
|
1929
2841
|
structuredOutputEnforced: !!options.outputSchema,
|
|
1930
|
-
subagentInvoked:
|
|
1931
|
-
mcpServersUsed:
|
|
1932
|
-
nativeSubagentsUsed:
|
|
2842
|
+
subagentInvoked: subagentCapabilities.invoked,
|
|
2843
|
+
mcpServersUsed: sentMcpServerNames,
|
|
2844
|
+
nativeSubagentsUsed: subagentCapabilities.names,
|
|
1933
2845
|
toolCompactionApplied: false,
|
|
1934
2846
|
contextCompactionApplied: null,
|
|
1935
2847
|
}),
|
|
1936
2848
|
};
|
|
1937
2849
|
} catch (err) {
|
|
1938
2850
|
if (resumeEntry) codexSessions.delete(resumeSessionId);
|
|
2851
|
+
const subagentCapabilities = observedSubagentCapabilities();
|
|
1939
2852
|
return {
|
|
1940
2853
|
text: texts[texts.length - 1] || null,
|
|
1941
2854
|
structuredResult: undefined,
|
|
@@ -1961,15 +2874,38 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1961
2874
|
promptCacheActive: null,
|
|
1962
2875
|
thinkingEnabled: null,
|
|
1963
2876
|
structuredOutputEnforced: !!options.outputSchema,
|
|
1964
|
-
subagentInvoked:
|
|
1965
|
-
mcpServersUsed:
|
|
1966
|
-
nativeSubagentsUsed:
|
|
2877
|
+
subagentInvoked: subagentCapabilities.invoked,
|
|
2878
|
+
mcpServersUsed: sentMcpServerNames,
|
|
2879
|
+
nativeSubagentsUsed: subagentCapabilities.names,
|
|
1967
2880
|
toolCompactionApplied: false,
|
|
1968
2881
|
contextCompactionApplied: null,
|
|
1969
2882
|
}),
|
|
1970
2883
|
};
|
|
1971
2884
|
} finally {
|
|
1972
2885
|
stopLiveInput();
|
|
2886
|
+
// The returned result is already being finalized. Park every transport
|
|
2887
|
+
// callback before lifecycle drains or close() so shutdown-time frames
|
|
2888
|
+
// cannot mutate its event array or stale per-run state.
|
|
2889
|
+
if (resumeEntry) {
|
|
2890
|
+
resumeEntry.notificationTarget.handler = noopNotificationHandler;
|
|
2891
|
+
resumeEntry.serverRequestTarget.handler = rejectIdleServerRequest;
|
|
2892
|
+
resumeEntry.closedTarget.handler = null;
|
|
2893
|
+
} else {
|
|
2894
|
+
notificationTarget.handler = noopNotificationHandler;
|
|
2895
|
+
serverRequestTarget.handler = rejectIdleServerRequest;
|
|
2896
|
+
}
|
|
2897
|
+
if ([...subagentGroupsBySpawnId.values()].some((group) => !group.finished)) {
|
|
2898
|
+
const cancelled = !!options.abortSignal?.aborted;
|
|
2899
|
+
const failed = Boolean(errorMessage || failureKind);
|
|
2900
|
+
finalizeOpenSubagents(
|
|
2901
|
+
cancelled ? "cancelled" : failed ? "failed" : "incomplete",
|
|
2902
|
+
cancelled
|
|
2903
|
+
? "Parent Codex turn was cancelled before the subagent completed."
|
|
2904
|
+
: failed
|
|
2905
|
+
? "Parent Codex turn failed before the subagent completed."
|
|
2906
|
+
: "Parent Codex turn ended before the subagent completed.",
|
|
2907
|
+
);
|
|
2908
|
+
}
|
|
1973
2909
|
if (activeCompactions.size > 0) {
|
|
1974
2910
|
const cancelled = !!options.abortSignal?.aborted;
|
|
1975
2911
|
finalizeOpenCompactions(
|
|
@@ -1980,8 +2916,6 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
|
|
|
1980
2916
|
options.abortSignal?.removeEventListener?.("abort", abortHandler);
|
|
1981
2917
|
if (resumeEntry) {
|
|
1982
2918
|
resumeEntry.busy = false;
|
|
1983
|
-
resumeEntry.notificationTarget.handler = noopNotificationHandler;
|
|
1984
|
-
resumeEntry.closedTarget.handler = null;
|
|
1985
2919
|
}
|
|
1986
2920
|
if (!sessionRetained) await closeCodexClient(client);
|
|
1987
2921
|
}
|