@mono-agent/agent-runtime 0.18.0 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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)) continue;
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 = ["app-server", "--listen", "stdio://", "-c", "project_doc_max_bytes=0"],
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 configuredMcpServerNames = new Set(Object.keys(codexMcpConfig(options.mcpServers)));
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 handleNotification and the bridge restores a no-op
1449
- // once the session goes idle.
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: options.codexAppServerArgs,
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: null,
1640
- mcpServersUsed: Object.keys(options.mcpServers || {}),
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 ? {} : codexMcpConfig(options.mcpServers);
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
- let turn;
1789
- try {
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: the aborted session survives.
2758
+ // but an interrupt is normal steering only when no provider descendant
2759
+ // remains live.
1866
2760
  const aborted = !!options.abortSignal?.aborted;
1867
- sessionRetained = (aborted && !prematureClose) || (!errorMessage && !failureKind);
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 (keepAlive && threadId && !errorMessage && !failureKind && !options.abortSignal?.aborted) {
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
- const entry = { client, threadId, busy: false, notificationTarget, closedTarget: { handler: null } };
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: null,
1931
- mcpServersUsed: Object.keys(options.mcpServers || {}),
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: null,
1965
- mcpServersUsed: Object.keys(options.mcpServers || {}),
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
  }