1import { codexScriptProgressConfig } from "./codexScriptProgress.ts";
2import * as ScriptProgress from "@t3tools/provider-core/server/ScriptProgress";
3import { revertCodexThread } from "../../provider/CodexThreadRevert.ts";
4import { historyResponseItems } from "@t3tools/provider-core/server/handoffBudget";
5import { makeProviderTextDeltaCoalescer } from "./ProviderTextDeltaCoalescer.ts";
6import {
7 mcpToolPresentation,
8 type McpToolPresentation,
9} from "../../provider/CodexToolPresentation.ts";
10import {
11 makeCodexTurnTokenUsageState,
12 getCodexTurnAccumulator,
13 accumulateCodexTurnTokenUsage,
14 completeCodexTurnTokenUsage,
15 type CodexTurnTokenUsageState,
16} from "../../provider/CodexTurnTokenUsage.ts";
17import type { ManagedServerProvider } from "@t3tools/provider-core/server/snapshot";
18import type { CodexEffectiveRuntime } from "../../provider/CodexManagedRuntime.ts";
19import {
20 buildCodexInitializeParams,
21 codexAppServerEnvironment,
22} from "../../provider/CodexProvider.ts";
23import {
24 codexRateLimitsToUpdate,
25 mergeCodexRateLimits,
26 codexUsageLimitResetAt,
27 type CodexRateLimitSnapshot,
28} from "../../provider/codexUsageLimits.ts";
29import {
30 CodexSettings,
31 defaultInstanceIdForDriver,
32 isOrchestrationV2WorkActive,
33 OrchestrationV2ProviderGoal,
34 ProviderDriverKind,
35 type ProviderSetupError,
36} from "@t3tools/contracts";
37import { SKILL_MENTION_PATTERN } from "@t3tools/shared/composerInlineTokens";
38import { AgentScope } from "@t3tools/shared/AgentScope";
39import * as HostProcess from "@t3tools/shared/HostProcess";
40import {
41 COMPUTER_USE_TOOL_SOURCE,
42 dynamicToolSurface,
43 dynamicToolTitle,
44} from "@t3tools/shared/toolActivity";
45import { getModelSelectionStringOptionValue, modelSelectionsEqual } from "@t3tools/shared/model";
46import { resolveSpawnCommand } from "@t3tools/shared/shell";
47import type {
48 ChatAttachment,
49 OrchestrationV2AppThread,
50 OrchestrationV2ConversationMessage,
51 OrchestrationV2ExecutionNode,
52 ModelSelection,
53 OrchestrationV2PlanArtifact,
54 OrchestrationV2ProviderCapabilities,
55 OrchestrationV2ProviderFailure,
56 OrchestrationV2ProviderRetry,
57 OrchestrationV2ProviderSession,
58 OrchestrationV2ProviderThread,
59 OrchestrationV2ProviderTurn,
60 OrchestrationV2PlanStep,
61 OrchestrationV2RuntimeRequest,
62 OrchestrationV2Subagent,
63 OrchestrationV2TurnItem,
64 ProviderUserInputAnswers,
65 ProviderApprovalDecision,
66 ProviderApprovalOption,
67 ProviderRequestKind,
68 ProviderThreadId,
69 ProviderTurnId,
70 ProviderInstanceId,
71 RuntimeMode,
72 RuntimeRequestId,
73 ThreadId,
74} from "@t3tools/contracts";
75import * as CodexClient from "effect-codex-app-server/client";
76import * as CodexErrors from "effect-codex-app-server/errors";
77import * as CodexSchema from "effect-codex-app-server/schema";
78import * as Cause from "effect/Cause";
79import * as Context from "effect/Context";
80import * as Crypto from "effect/Crypto";
81import * as DateTime from "effect/DateTime";
82import * as Deferred from "effect/Deferred";
83import * as Effect from "effect/Effect";
84import * as Fiber from "effect/Fiber";
85import * as FileSystem from "effect/FileSystem";
86import * as Layer from "effect/Layer";
87import * as Option from "effect/Option";
88import * as Path from "effect/Path";
89import * as Queue from "effect/Queue";
90import * as Ref from "effect/Ref";
91import * as Schema from "effect/Schema";
92import * as Scope from "effect/Scope";
93import * as Semaphore from "effect/Semaphore";
94import * as Stream from "effect/Stream";
95import { ChildProcess, ChildProcessSpawner } from "effect/process";
96
97import { resolveAttachmentPath, resolveAttachmentPathById } from "../../attachmentStore.ts";
98import { getCodexServiceTierOptionValue } from "../../codexModelOptions.ts";
99import { ServerConfig } from "../../config.ts";
100import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
101import {
102 buildCodexAdditionalContext,
103 buildCodexDeveloperInstructions,
104} from "../../provider/CodexDeveloperInstructions.ts";
105import {
106 describeMcpElicitation,
107 toMcpElicitationResponse,
108} from "../../provider/CodexMcpElicitation.ts";
109import {
110 materializeCodexShadowHome,
111 resolveCodexHomeLayout,
112} from "../../provider/Drivers/CodexHomeLayout.ts";
113import {
114 boundProviderEventForLogging,
115 shouldPersistProviderEvent,
116} from "../../provider/EventNdjsonLogger.ts";
117import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
118import { codexAppServerArgs, resolveCodexLaunchArgs } from "../../provider/codexLaunchArgs.ts";
119import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment";
120import * as McpProviderSession from "@t3tools/provider-core/server/mcpSession";
121import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
122import {
123 MCP_APP_EXTENSION_ID,
124 MCP_APP_MIME_TYPE,
125 MCP_APP_OUTPUT_KEY,
126 MCP_APP_RESOURCE_SCHEME,
127} from "@t3tools/shared/mcpApp";
128import { snapshotMcpApp } from "../../mcpApps/McpAppSnapshot.ts";
129import {
130 ProviderAdapterDriverCreateError,
131 type ProviderAdapterDriver,
132 type ProviderAdapterDriverCreateInput,
133} from "@t3tools/provider-core/server/adapterDriver";
134import * as ProviderContinuationRequests from "@t3tools/provider-core/server/ProviderContinuationRequests";
135import { backgroundWorkNotification } from "@t3tools/provider-core/server/notification";
136import {
137 makeProviderFailure,
138 makeProviderFailureTurnItem,
139 makeProviderRetryTurnItem,
140} from "@t3tools/provider-core/server/failure";
141import { turnScopedSelectionTransition } from "@t3tools/provider-core/server/selectionTransition";
142import {
143 isProviderNativeImageAttachment,
144 providerMessageTextWithAttachmentPaths,
145} from "@t3tools/provider-core/server/attachmentPrompt";
146import {
147 makeSubagentChildThread,
148 makeSubagentConversationArtifacts,
149 subagentThreadTitle,
150} from "@t3tools/provider-core/server/subagentProjection";
151import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
152import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
153
154const CODEX_PROVIDER = ProviderDriverKind.make("codex");
155export const CODEX_DRIVER_KIND = CODEX_PROVIDER;
156export const CODEX_DEFAULT_INSTANCE_ID = defaultInstanceIdForDriver(CODEX_DRIVER_KIND);
157
158/** Describe approval scope even when Codex omits or blanks the optional reason. */
159export function codexFileChangeApprovalPrompt(input: {
160 readonly reason?: string | null;
161 readonly grantRoot?: string | null;
162 readonly fileChanges?: CodexSchema.ServerRequest__ApplyPatchApprovalParams["fileChanges"];
163}): string | undefined {
164 const reason = input.reason?.trim();
165 if (reason) return reason;
166 const entries = Object.entries(input.fileChanges ?? {}).toSorted(([left], [right]) =>
167 left.localeCompare(right),
168 );
169 if (entries.length > 0) {
170 const described = entries.slice(0, 20).map(([path, change]) => {
171 const movePath = change.type === "update" ? change.move_path : undefined;
172 return movePath ? `${change.type} ${path} -> ${movePath}` : `${change.type} ${path}`;
173 });
174 const remaining = entries.length - described.length;
175 return remaining > 0 ? `${described.join("\n")}\n+${remaining} more` : described.join("\n");
176 }
177 return input.grantRoot?.trim() || undefined;
178}
179
180// Reasoning effort changes future generation, not the existing context's tokenizer
181// or model window. All other option changes remain untrusted until new telemetry.
182export function canReuseCodexContextUsage(previous: ModelSelection, next: ModelSelection): boolean {
183 return modelSelectionsEqual(
184 {
185 ...previous,
186 options: (previous.options ?? []).filter((option) => option.id !== "reasoningEffort"),
187 },
188 { ...next, options: (next.options ?? []).filter((option) => option.id !== "reasoningEffort") },
189 );
190}
191
192export function codexProviderTurnTokenUsage(
193 tokenUsage: CodexSchema.V2ThreadTokenUsageUpdatedNotification["tokenUsage"],
194 updatedAt: string,
195) {
196 return {
197 usedTokens: Math.max(0, tokenUsage.last.totalTokens),
198 maxTokens: tokenUsage.modelContextWindow ?? null,
199 inputTokens: Math.max(0, tokenUsage.last.inputTokens),
200 cachedInputTokens: Math.max(0, tokenUsage.last.cachedInputTokens),
201 outputTokens: Math.max(0, tokenUsage.last.outputTokens),
202 reasoningOutputTokens: Math.max(0, tokenUsage.last.reasoningOutputTokens),
203 updatedAt,
204 };
205}
206const DEFAULT_CODEX_SETTINGS = Schema.decodeSync(CodexSettings)({});
207const CODEX_ASSISTANT_DELTA_FLUSH_INTERVAL_MS = 50;
208// Each plan flush stores the whole plan three times (node, plan, turn item), so
209// flush at the cadence clients already see for assistant text.
210const CODEX_PLAN_DELTA_FLUSH_INTERVAL_MS = 400;
211const CodexBackgroundTerminalTerminateResponse = Schema.Struct({
212 terminated: Schema.Boolean,
213});
214const CodexBackgroundTerminalsListResponse = Schema.Struct({
215 data: Schema.Array(
216 Schema.Struct({
217 processId: Schema.String,
218 }),
219 ),
220 nextCursor: Schema.NullOr(Schema.String),
221});
222type CodexBackgroundTerminalsListPage = typeof CodexBackgroundTerminalsListResponse.Type;
223const decodeCodexBackgroundTerminalTerminateResponse = Schema.decodeUnknownEffect(
224 CodexBackgroundTerminalTerminateResponse,
225);
226const decodeCodexBackgroundTerminalsListResponse = Schema.decodeUnknownEffect(
227 CodexBackgroundTerminalsListResponse,
228);
229const CODEX_CLIENT_CAPABILITIES = {
230 experimentalApi: true,
231 optOutNotificationMethods: ["turn/diff/updated"],
232 // Declares MCP Apps support (SEP-1724 extension negotiation), so servers
233 // attach their UI resources and Codex reports them on tool call items.
234 extensions: { [MCP_APP_EXTENSION_ID]: { mimeTypes: [MCP_APP_MIME_TYPE] } },
235} as const;
236
237export const CodexProviderCapabilitiesV2 = {
238 sessions: {
239 supportsMultipleProviderThreadsPerSession: true,
240 supportsModelSwitchInSession: true,
241 supportsProviderSwitchingViaHandoff: true,
242 supportsRuntimeModeSwitchInSession: true,
243 pendingRequestsSurviveRestart: false,
244 },
245 threads: {
246 canCreateEmptyThread: true,
247 canReadThreadSnapshot: true,
248 canRollbackThread: true,
249 canForkThread: true,
250 canForkFromTurn: true,
251 canForkFromSubagentThread: true,
252 exposesNativeThreadId: true,
253 },
254 turns: {
255 exposesNativeTurnId: true,
256 emitsTurnStarted: true,
257 emitsTurnCompleted: true,
258 supportsInterrupt: true,
259 supportsActiveSteering: true,
260 supportsSteeringByInterruptRestart: true,
261 supportsQueuedMessages: true,
262 terminalStatusQuality: "strong",
263 },
264 streaming: {
265 streamsAssistantText: true,
266 streamsReasoning: true,
267 streamsToolOutput: true,
268 streamsPlanText: true,
269 emitsMessageCompleted: true,
270 },
271 tools: {
272 exposesToolItemIds: true,
273 emitsToolStarted: true,
274 emitsToolCompleted: true,
275 emitsToolOutput: true,
276 supportsMcpTools: true,
277 supportsDynamicToolCallbacks: true,
278 },
279 approvals: {
280 supportsCommandApproval: true,
281 supportsFileReadApproval: true,
282 supportsFileChangeApproval: true,
283 supportsApplyPatchApproval: true,
284 approvalsHaveNativeRequestIds: true,
285 approvalCallbacksAreLiveOnly: true,
286 approvalsCanOriginateFromSubagents: true,
287 },
288 planning: {
289 emitsPlanUpdated: true,
290 emitsTodoList: true,
291 emitsProposedPlan: true,
292 supportsStructuredQuestions: true,
293 planDeltasHaveItemIds: true,
294 },
295 subagents: {
296 supportsSubagents: true,
297 exposesSubagentThreadIds: true,
298 emitsSubagentLifecycle: true,
299 canWaitForSubagents: true,
300 canCloseSubagents: true,
301 canForkSubagentThread: true,
302 },
303 context: {
304 acceptsSystemContext: true,
305 acceptsDeveloperContext: true,
306 acceptsSyntheticUserContext: true,
307 canGenerateSummaries: true,
308 canConsumeHandoffSummaries: true,
309 supportsDeltaHandoff: true,
310 supportsFullThreadHandoff: true,
311 maxRecommendedHandoffChars: null,
312 },
313 checkpointing: {
314 appCanCheckpointFilesystem: true,
315 supportsNestedCheckpointScopes: true,
316 providerCanRollbackConversation: true,
317 providerRollbackReturnsSnapshot: true,
318 providerCanReadConversationSnapshot: true,
319 },
320 identity: {
321 nativeThreadIds: "strong",
322 nativeTurnIds: "strong",
323 nativeItemIds: "strong",
324 nativeRequestIds: "strong",
325 },
326 runtimePolicy: {
327 enforcement: "native",
328 },
329} satisfies OrchestrationV2ProviderCapabilities;
330
331function toProtocolError(
332 detail: string,
333 payload?: unknown,
334): ProviderAdapter.ProviderAdapterProtocolError {
335 return new ProviderAdapter.ProviderAdapterProtocolError({
336 driver: CODEX_PROVIDER,
337 detail,
338 ...(payload === undefined ? {} : { payload }),
339 });
340}
341
342function normalizeCodexCause(error: unknown): unknown {
343 return error;
344}
345
346function codexTimestamp(seconds: number | null | undefined): DateTime.Utc {
347 return seconds === null || seconds === undefined
348 ? DateTime.nowUnsafe()
349 : DateTime.makeUnsafe(seconds * 1000);
350}
351
352function codexUserMessageText(
353 content: ReadonlyArray<CodexSchema.V2ItemCompletedNotification__UserInput>,
354): string {
355 return content
356 .flatMap((item) => (item.type === "text" ? [item.text] : []))
357 .join("\n")
358 .trim();
359}
360
361function mapCodexTurnStatus(
362 status: CodexSchema.V2TurnCompletedNotification__TurnStatus,
363): OrchestrationV2ProviderTurn["status"] {
364 switch (status) {
365 case "completed":
366 return "completed";
367 case "interrupted":
368 return "interrupted";
369 case "failed":
370 return "failed";
371 case "inProgress":
372 return "running";
373 }
374}
375
376function providerTurnStatusToTerminal(
377 status: OrchestrationV2ProviderTurn["status"],
378): Extract<ProviderAdapter.ProviderAdapterV2Event, { type: "turn.terminal" }>["status"] {
379 switch (status) {
380 case "completed":
381 return "completed";
382 case "interrupted":
383 return "interrupted";
384 case "failed":
385 return "failed";
386 case "cancelled":
387 return "cancelled";
388 case "pending":
389 case "running":
390 return "failed";
391 }
392}
393
394function codexItemStatus(status: "inProgress" | "completed" | "failed" | "declined"): {
395 readonly node: OrchestrationV2ExecutionNode["status"];
396 readonly turnItem: OrchestrationV2TurnItem["status"];
397 readonly completed: boolean;
398} {
399 switch (status) {
400 case "inProgress":
401 return { node: "running", turnItem: "running", completed: false };
402 case "completed":
403 return {
404 node: "completed",
405 turnItem: "completed",
406 completed: true,
407 };
408 case "failed":
409 return { node: "failed", turnItem: "failed", completed: true };
410 case "declined":
411 return {
412 node: "cancelled",
413 turnItem: "cancelled",
414 completed: true,
415 };
416 }
417}
418
419/**
420 * The composers let a skill be typed with any currency sigil (`€review`), but
421 * Codex only parses `$name` as a skill mention. Rewrite the sigil so the skill
422 * runs; currency amounts like `€20` do not match and stay prose.
423 */
424export function codexSkillMentionText(text: string): string {
425 return text.replace(SKILL_MENTION_PATTERN, "$1$$$2");
426}
427
428const BACKGROUND_COMMAND_DETAIL_COMMAND_MAX_LENGTH = 200;
429const BACKGROUND_COMMAND_DETAIL_OUTPUT_TAIL_MAX_LENGTH = 1_000;
430
431export function codexBackgroundCommandDetail(item: {
432 readonly command: string;
433 readonly exitCode?: number | null | undefined;
434 readonly aggregatedOutput?: string | null | undefined;
435}): string {
436 const command =
437 item.command.length > BACKGROUND_COMMAND_DETAIL_COMMAND_MAX_LENGTH
438 ? `${item.command.slice(0, BACKGROUND_COMMAND_DETAIL_COMMAND_MAX_LENGTH)}...`
439 : item.command;
440 const exit =
441 item.exitCode === null || item.exitCode === undefined ? "" : ` (exit ${item.exitCode})`;
442 const output = (item.aggregatedOutput ?? "").trimEnd();
443 const outputTail =
444 output.length > BACKGROUND_COMMAND_DETAIL_OUTPUT_TAIL_MAX_LENGTH
445 ? `...${output.slice(-BACKGROUND_COMMAND_DETAIL_OUTPUT_TAIL_MAX_LENGTH)}`
446 : output;
447 const header = `Background command completed${exit}: ${command}`;
448 return outputTail.length === 0 ? header : `${header}\n\nOutput tail:\n${outputTail}`;
449}
450
451export interface CodexDynamicToolProjection extends McpToolPresentation {
452 readonly toolName: string;
453 readonly title?: string;
454 readonly input: unknown;
455 readonly output?: unknown;
456 readonly status: OrchestrationV2TurnItem["status"];
457}
458
459function codexMcpToolOutput(
460 item: Extract<CodexDynamicToolItem, { readonly type: "mcpToolCall" }>,
461): unknown | undefined {
462 const resultOutput =
463 item.result === null || item.result === undefined
464 ? undefined
465 : item.result.structuredContent !== null && item.result.structuredContent !== undefined
466 ? item.result.structuredContent
467 : item.result.content;
468
469 if (item.error === null || item.error === undefined) {
470 return resultOutput;
471 }
472 return resultOutput === undefined
473 ? { error: item.error.message }
474 : { error: item.error.message, result: resultOutput };
475}
476
477// Reading an app's resource is one MCP round trip; a server that hangs must not
478// hold the item (and the run's ingestion) open.
479const MCP_APP_CAPTURE_TIMEOUT = "20 seconds";
480
481/** The UI resource a completed Codex MCP tool call names, under any field Codex reports it in. */
482function codexMcpAppResourceUri(item: CodexDynamicToolItem): string | undefined {
483 if (item.type !== "mcpToolCall") return undefined;
484 const uri = item.mcpAppUi?.resourceUri ?? item.mcpAppResourceUri ?? item.appContext?.resourceUri;
485 return typeof uri === "string" && uri.startsWith(MCP_APP_RESOURCE_SCHEME) ? uri : undefined;
486}
487
488function codexDynamicToolOutput(
489 item: Extract<CodexDynamicToolItem, { readonly type: "dynamicToolCall" }>,
490): unknown | undefined {
491 if (item.contentItems !== null && item.contentItems !== undefined) {
492 return item.contentItems;
493 }
494 return item.success === false ? { success: false } : undefined;
495}
496
497export const projectCodexDynamicToolItem = Effect.fn("CodexAdapterV2.projectDynamicToolItem")(
498 function* (
499 item: CodexDynamicToolItem,
500 ): Effect.fn.Return<CodexDynamicToolProjection, never, Crypto.Crypto> {
501 const output =
502 item.type === "mcpToolCall" ? codexMcpToolOutput(item) : codexDynamicToolOutput(item);
503 const toolName =
504 item.type === "mcpToolCall"
505 ? `${item.server}.${item.tool}`
506 : [trimText(item.namespace), item.tool].filter(Boolean).join(".");
507 const presentation: McpToolPresentation =
508 item.type === "mcpToolCall" ? yield* mcpToolPresentation(item) : {};
509 const title =
510 dynamicToolTitle(toolName, item.arguments, presentation.title) ?? presentation.title;
511 const toolSurface = presentation.toolSurface ?? dynamicToolSurface(toolName, item.arguments);
512 const toolSource =
513 toolSurface === "computer" && presentation.toolSource?.kind !== "computer"
514 ? COMPUTER_USE_TOOL_SOURCE
515 : presentation.toolSource;
516 const projection: CodexDynamicToolProjection = {
517 ...presentation,
518 ...(toolSurface ? { toolSurface } : {}),
519 ...(toolSource ? { toolSource } : {}),
520 toolName,
521 ...(title ? { title } : {}),
522 input: item.arguments,
523 status: codexItemStatus(item.status).turnItem,
524 };
525 return output === undefined ? projection : { ...projection, output };
526 },
527);
528
529function codexNativeItemRef(nativeItemId: string) {
530 return {
531 driver: CODEX_PROVIDER,
532 nativeId: nativeItemId,
533 strength: "strong" as const,
534 };
535}
536
537function trimText(value: string | null | undefined): string | undefined {
538 const trimmed = value?.trim();
539 return trimmed && trimmed.length > 0 ? trimmed : undefined;
540}
541
542function nonEmptyText(value: string | null | undefined, fallback: string): string {
543 return trimText(value) ?? fallback;
544}
545
546function codexPlanStepStatus(
547 status: CodexSchema.V2TurnPlanUpdatedNotification__TurnPlanStepStatus,
548): OrchestrationV2PlanStep["status"] {
549 switch (status) {
550 case "completed":
551 return "completed";
552 case "inProgress":
553 return "running";
554 case "pending":
555 return "pending";
556 }
557}
558
559function approvalDecisionToLegacyReviewDecision(
560 decision: ProviderApprovalDecision,
561): CodexSchema.ExecCommandApprovalResponse__ReviewDecision {
562 switch (decision) {
563 case "accept":
564 return "approved";
565 case "acceptForSession":
566 case "acceptAlways":
567 return "approved_for_session";
568 case "decline":
569 return { denied: { rejection: "User declined the request." } };
570 case "cancel":
571 return "abort";
572 }
573}
574
575function providerRequestKindFromPermissions(
576 permissions: CodexSchema.PermissionsRequestApprovalParams["permissions"],
577): ProviderRequestKind {
578 if ((permissions.fileSystem?.write?.length ?? 0) > 0) {
579 return "file-change";
580 }
581 if ((permissions.fileSystem?.read?.length ?? 0) > 0) {
582 return "file-read";
583 }
584 return "command";
585}
586
587function permissionsResponseFromDecision(input: {
588 readonly decision: ProviderApprovalDecision;
589 readonly permissions: CodexSchema.PermissionsRequestApprovalParams["permissions"];
590}): CodexSchema.PermissionsRequestApprovalResponse {
591 if (input.decision !== "accept" && input.decision !== "acceptForSession") {
592 return { permissions: {}, scope: "turn" };
593 }
594
595 return {
596 permissions: input.permissions,
597 scope: input.decision === "acceptForSession" ? "session" : "turn",
598 };
599}
600
601function answerValueToStrings(value: unknown): ReadonlyArray<string> {
602 if (Array.isArray(value)) {
603 return value.map((item) => String(item));
604 }
605 if (typeof value === "string") {
606 return [value];
607 }
608 if (value === null || value === undefined) {
609 return [];
610 }
611 if (typeof value === "number" || typeof value === "boolean") {
612 return [String(value)];
613 }
614 return [JSON.stringify(value)];
615}
616
617function toCodexUserInputAnswers(
618 answers: ProviderUserInputAnswers,
619 allowedQuestionIds: ReadonlySet<string>,
620): CodexSchema.ToolRequestUserInputResponse["answers"] {
621 return Object.fromEntries(
622 Object.entries(answers).flatMap(([questionId, value]) =>
623 allowedQuestionIds.has(questionId)
624 ? [[questionId, { answers: [...answerValueToStrings(value)] }]]
625 : [],
626 ),
627 );
628}
629
630function compactStrings(values: ReadonlyArray<string | null | undefined>): ReadonlyArray<string> {
631 return values.flatMap((value) => {
632 const trimmed = trimText(value);
633 return trimmed === undefined ? [] : [trimmed];
634 });
635}
636
637function webSearchPatterns(item: CodexWebSearchItem): ReadonlyArray<string> {
638 if (item.action === null || item.action === undefined) {
639 return compactStrings([item.query]);
640 }
641
642 switch (item.action.type) {
643 case "search":
644 return compactStrings([...(item.action.queries ?? []), item.action.query, item.query]);
645 case "openPage":
646 return compactStrings([item.action.url, item.query]);
647 case "findInPage":
648 return compactStrings([item.action.pattern, item.action.url, item.query]);
649 case "other":
650 return compactStrings([item.query]);
651 }
652}
653
654const decodeTurnApprovalPolicy = Schema.decodeUnknownEffect(
655 Schema.Union([CodexSchema.V2TurnStartParams__AskForApproval, Schema.Null]),
656);
657const decodeTurnSandboxPolicy = Schema.decodeUnknownEffect(
658 Schema.Union([CodexSchema.V2TurnStartParams__SandboxPolicy, Schema.Null]),
659);
660const decodeTurnReasoningEffort = Schema.decodeUnknownEffect(
661 Schema.Union([CodexSchema.V2TurnStartParams__ReasoningEffort, Schema.Null]),
662);
663
664const CodexTurnStartParamsWithCollaborationMode = CodexSchema.V2TurnStartParams.pipe(
665 Schema.fieldsAssign({
666 collaborationMode: Schema.optionalKey(CodexSchema.ClientRequest__CollaborationMode),
667 additionalContext: Schema.optionalKey(
668 Schema.Record(Schema.String, CodexSchema.V2TurnStartParams__AdditionalContextEntry),
669 ),
670 }),
671);
672type CodexTurnStartParamsWithCollaborationMode =
673 typeof CodexTurnStartParamsWithCollaborationMode.Type;
674const decodeCodexTurnStartParamsWithCollaborationMode = Schema.decodeUnknownEffect(
675 CodexTurnStartParamsWithCollaborationMode,
676);
677const isProviderAdapterRuntimeRequestResponseError = Schema.is(
678 ProviderAdapter.ProviderAdapterRuntimeRequestResponseError,
679);
680
681function codexRuntimeModeTurnDefaults(runtimeMode: RuntimeMode): {
682 readonly approvalPolicy: CodexSchema.V2TurnStartParams__AskForApproval;
683 readonly approvalsReviewer: CodexSchema.V2TurnStartParams__ApprovalsReviewer;
684 readonly sandboxPolicy: CodexSchema.V2TurnStartParams__SandboxPolicy;
685} {
686 switch (runtimeMode) {
687 case "approval-required":
688 return {
689 approvalPolicy: "untrusted",
690 approvalsReviewer: "user",
691 sandboxPolicy: {
692 type: "readOnly",
693 },
694 };
695 case "auto-accept-edits":
696 return {
697 approvalPolicy: "on-request",
698 approvalsReviewer: "user",
699 sandboxPolicy: {
700 type: "workspaceWrite",
701 },
702 };
703 case "auto":
704 return {
705 approvalPolicy: "on-request",
706 approvalsReviewer: "auto_review",
707 sandboxPolicy: {
708 type: "workspaceWrite",
709 },
710 };
711 case "full-access":
712 return {
713 approvalPolicy: "never",
714 approvalsReviewer: "user",
715 sandboxPolicy: {
716 type: "dangerFullAccess",
717 },
718 };
719 }
720}
721
722export function buildCodexTurnStartParams(input: {
723 readonly nativeThreadId: string;
724 readonly codexInput: ReadonlyArray<CodexSchema.V2TurnStartParams__UserInput>;
725 readonly runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy;
726 readonly modelSelection: ModelSelection;
727 readonly hasT3Mcp?: boolean;
728 readonly browserToolsAvailable?: boolean;
729 readonly deviceToolsAvailable?: boolean;
730 /** ChatGPT token sharing does not accept service tiers. */
731 readonly omitServiceTier?: boolean;
732 /** What the thread's MCP Apps want the agent to know (`ui/update-model-context`). */
733 readonly appContext?: ProviderAdapter.ProviderAdapterV2TurnInput["appContext"];
734}) {
735 return Effect.gen(function* () {
736 const runtimeModeDefaults = codexRuntimeModeTurnDefaults(input.runtimePolicy.runtimeMode);
737 const approvalPolicy =
738 input.runtimePolicy.approvalPolicy === undefined
739 ? runtimeModeDefaults.approvalPolicy
740 : yield* decodeTurnApprovalPolicy(input.runtimePolicy.approvalPolicy);
741 const sandboxPolicy =
742 input.runtimePolicy.sandboxPolicy === undefined
743 ? runtimeModeDefaults.sandboxPolicy
744 : yield* decodeTurnSandboxPolicy(input.runtimePolicy.sandboxPolicy);
745 const selectedEffort = getModelSelectionStringOptionValue(
746 input.modelSelection,
747 "reasoningEffort",
748 );
749 const effort =
750 selectedEffort === undefined ? undefined : yield* decodeTurnReasoningEffort(selectedEffort);
751 const serviceTier =
752 input.omitServiceTier === true
753 ? undefined
754 : getCodexServiceTierOptionValue(input.modelSelection);
755 const developerInstructions =
756 input.hasT3Mcp !== true
757 ? undefined
758 : buildCodexDeveloperInstructions(input.runtimePolicy.interactionMode);
759 // An app's context is text an MCP server wrote, so it goes in as untrusted
760 // context: Codex renders it as quoted user-side input, never as developer
761 // instructions. Codex resends it only when it changes.
762 const appContext = Object.fromEntries(
763 (input.appContext ?? []).map((entry) => [
764 entry.key,
765 { kind: "untrusted" as const, value: entry.text },
766 ]),
767 );
768 const t3Context =
769 input.hasT3Mcp === true
770 ? buildCodexAdditionalContext(
771 { model: input.modelSelection.model, reasoningEffort: effort ?? "medium" },
772 {
773 browser: input.browserToolsAvailable ?? true,
774 device: input.deviceToolsAvailable ?? false,
775 },
776 )
777 : undefined;
778 const additionalContext =
779 t3Context === undefined && Object.keys(appContext).length === 0
780 ? undefined
781 : { ...t3Context, ...appContext };
782 const collaborationMode: CodexSchema.ClientRequest__CollaborationMode | undefined =
783 input.runtimePolicy.interactionMode !== "plan" && developerInstructions === undefined
784 ? undefined
785 : {
786 mode: input.runtimePolicy.interactionMode === "plan" ? "plan" : "default",
787 settings: {
788 model: input.modelSelection.model,
789 reasoning_effort: effort ?? "medium",
790 ...(developerInstructions === undefined
791 ? {}
792 : { developer_instructions: developerInstructions }),
793 },
794 };
795
796 return yield* decodeCodexTurnStartParamsWithCollaborationMode({
797 threadId: input.nativeThreadId,
798 input: input.codexInput,
799 ...(additionalContext ? { additionalContext } : {}),
800 cwd: input.runtimePolicy.cwd,
801 model: input.modelSelection.model,
802 // Model catalogues can default summaries to "none". Request them on every
803 // turn, including resumed threads, for T3's reasoning timeline.
804 summary: "detailed",
805 // Always explicit: omitting this on resume leaves Codex's previous
806 // reviewer sticky after switching away from Auto mode.
807 approvalsReviewer: runtimeModeDefaults.approvalsReviewer,
808 ...(approvalPolicy === undefined ? {} : { approvalPolicy }),
809 ...(sandboxPolicy === undefined ? {} : { sandboxPolicy }),
810 ...(effort === undefined ? {} : { effort }),
811 ...(serviceTier === undefined ? {} : { serviceTier }),
812 ...(collaborationMode === undefined ? {} : { collaborationMode }),
813 });
814 });
815}
816
817function providerSession(input: {
818 readonly providerSessionId: OrchestrationV2ProviderSession["id"];
819 readonly providerInstanceId: ProviderInstanceId;
820 readonly cwd: string | null;
821 readonly model: string;
822 readonly now: DateTime.Utc;
823}): OrchestrationV2ProviderSession {
824 return {
825 id: input.providerSessionId,
826 driver: CODEX_PROVIDER,
827 providerInstanceId: input.providerInstanceId,
828 status: "ready",
829 cwd: input.cwd ?? process.cwd(),
830 model: input.model,
831 capabilities: CodexProviderCapabilitiesV2,
832 createdAt: input.now,
833 updatedAt: input.now,
834 lastError: null,
835 };
836}
837
838function getNativeThreadId(providerThread: OrchestrationV2ProviderThread) {
839 return Effect.gen(function* () {
840 const nativeThreadId = providerThread.nativeThreadRef?.nativeId;
841 if (nativeThreadId === undefined || nativeThreadId === null) {
842 return yield* toProtocolError(
843 `Provider thread ${providerThread.id} is missing a native Codex thread id.`,
844 );
845 }
846 return nativeThreadId;
847 });
848}
849
850function providerThreadFromCodexThread(input: {
851 readonly appThreadId: ThreadId | null;
852 readonly idAllocator: IdAllocator.IdAllocatorV2["Service"];
853 readonly ownerNodeId: OrchestrationV2ProviderThread["ownerNodeId"];
854 readonly providerSessionId: OrchestrationV2ProviderThread["providerSessionId"];
855 readonly providerInstanceId: ProviderInstanceId;
856 readonly thread: {
857 readonly createdAt: number;
858 readonly forkedFromId?: string | null;
859 readonly id: string;
860 readonly updatedAt: number;
861 };
862 readonly forkedFrom?: OrchestrationV2ProviderThread["forkedFrom"];
863}): OrchestrationV2ProviderThread {
864 return {
865 id: input.idAllocator.derive.providerThread({
866 driver: CODEX_PROVIDER,
867 nativeThreadId: input.thread.id,
868 }),
869 driver: CODEX_PROVIDER,
870 providerInstanceId: input.providerInstanceId,
871 providerSessionId: input.providerSessionId,
872 appThreadId: input.appThreadId,
873 ownerNodeId: input.ownerNodeId,
874 nativeThreadRef: {
875 driver: CODEX_PROVIDER,
876 nativeId: input.thread.id,
877 strength: "strong" as const,
878 },
879 nativeConversationHeadRef: null,
880 status: "idle",
881 firstRunOrdinal: null,
882 lastRunOrdinal: null,
883 handoffIds: [],
884 forkedFrom: input.forkedFrom ?? null,
885 createdAt: codexTimestamp(input.thread.createdAt),
886 updatedAt: codexTimestamp(input.thread.updatedAt),
887 };
888}
889
890// Counts toward native rollback. `/goal` control turns never reach Codex and
891// carry no native ref, so they must not consume a native turn on revert.
892const isTerminalProviderTurn = (turn: OrchestrationV2ProviderTurn): boolean =>
893 turn.nativeTurnRef !== null &&
894 (turn.status === "completed" ||
895 turn.status === "interrupted" ||
896 turn.status === "failed" ||
897 turn.status === "cancelled");
898
899export type CodexGoalCommand =
900 | { readonly type: "show" }
901 | { readonly type: "clear" }
902 | { readonly type: "pause" }
903 | { readonly type: "resume" }
904 | { readonly type: "set"; readonly objective: string };
905
906/**
907 * Parses a `/goal` message the way the Codex TUI does: `clear`, `pause` and
908 * `resume` control the current goal, a bare `/goal` shows it, and any other
909 * text becomes the new objective. Returns null for every other message.
910 */
911export function parseCodexGoalCommand(text: string): CodexGoalCommand | null {
912 const match = /^\/goal(?:\s+([\s\S]*))?$/u.exec(text.trim());
913 if (match === null) return null;
914 const argument = (match[1] ?? "").trim();
915 switch (argument.toLowerCase()) {
916 case "":
917 case "edit":
918 return { type: "show" };
919 case "clear":
920 return { type: "clear" };
921 case "pause":
922 return { type: "pause" };
923 case "resume":
924 return { type: "resume" };
925 default:
926 return { type: "set", objective: argument };
927 }
928}
929
930type CodexThreadGoal = CodexSchema.V2ThreadGoalUpdatedNotification["goal"];
931
932const CODEX_GOAL_STATUSES = {
933 active: "active",
934 paused: "paused",
935 blocked: "blocked",
936 usageLimited: "usage_limited",
937 budgetLimited: "budget_limited",
938 complete: "complete",
939} as const satisfies Record<CodexThreadGoal["status"], OrchestrationV2ProviderGoal["status"]>;
940
941function providerGoalFromCodex(goal: CodexThreadGoal): OrchestrationV2ProviderGoal | null {
942 const objective = goal.objective.trim();
943 if (objective.length === 0) return null;
944 return {
945 objective,
946 status: CODEX_GOAL_STATUSES[goal.status],
947 tokensUsed: Math.max(0, goal.tokensUsed),
948 tokenBudget: goal.tokenBudget ?? null,
949 timeUsedSeconds: Math.max(0, goal.timeUsedSeconds),
950 };
951}
952
953const providerGoalsEqual = Schema.toEquivalence(Schema.NullOr(OrchestrationV2ProviderGoal));
954
955function describeCodexGoal(goal: OrchestrationV2ProviderGoal): string {
956 return `Goal ${goal.status.replace("_", " ")}: ${goal.objective}`;
957}
958
959// Codex starts the next goal turn milliseconds after the last one completes.
960// A run waits this long for it before settling, in case Codex declines.
961const CODEX_GOAL_CONTINUATION_GRACE = "5 seconds";
962
963const providerTurnsForThread = (
964 providerTurns: ReadonlyArray<OrchestrationV2ProviderTurn>,
965 providerThread: OrchestrationV2ProviderThread,
966): ReadonlyArray<OrchestrationV2ProviderTurn> =>
967 providerTurns.filter((turn) => turn.providerThreadId === providerThread.id);
968
969const countTerminalTurnsAfterBoundary = (
970 providerTurns: ReadonlyArray<OrchestrationV2ProviderTurn>,
971 providerTurnId: ProviderTurnId,
972): number | null => {
973 const boundaryTurn = providerTurns.find((turn) => turn.id === providerTurnId);
974 if (boundaryTurn === undefined) {
975 return null;
976 }
977
978 return providerTurns.filter(
979 (turn) => turn.ordinal > boundaryTurn.ordinal && isTerminalProviderTurn(turn),
980 ).length;
981};
982
983const resolveCodexForkRollbackTurnCount = Effect.fn("CodexAdapterV2.resolveForkRollbackTurnCount")(
984 function* (input: ProviderAdapter.ProviderAdapterV2ForkThreadInput) {
985 if (input.providerTurnId === undefined || input.sourceProviderTurns === undefined) {
986 return 0;
987 }
988
989 const rollbackTurnCount = countTerminalTurnsAfterBoundary(
990 providerTurnsForThread(input.sourceProviderTurns, input.sourceProviderThread),
991 input.providerTurnId,
992 );
993 if (rollbackTurnCount === null) {
994 return yield* new ProviderAdapter.ProviderAdapterForkThreadError({
995 driver: CODEX_PROVIDER,
996 providerThreadId: input.sourceProviderThread.id,
997 cause: `Cannot fork Codex thread from provider turn ${input.providerTurnId}: source turn was not found in provider thread ${input.sourceProviderThread.id}.`,
998 });
999 }
1000
1001 return rollbackTurnCount;
1002 },
1003);
1004
1005/**
1006 * Prefer a native `thread/fork` turn boundary over the fork-then-rollback
1007 * fallback. `lastTurnId` is inclusive on the Codex side, so a fork requested at
1008 * the selected turn omits every later turn atomically. That matters for
1009 * paginated threads. The count fallback resolves a `thread/revert` boundary
1010 * only for source turns that predate native turn references.
1011 */
1012export const resolveCodexForkBoundary = Effect.fn("CodexAdapterV2.resolveForkBoundary")(function* (
1013 input: ProviderAdapter.ProviderAdapterV2ForkThreadInput,
1014) {
1015 const rollbackTurnCount = yield* resolveCodexForkRollbackTurnCount(input);
1016 if (input.providerTurnId === undefined || input.sourceProviderTurns === undefined) {
1017 return { lastTurnId: undefined, rollbackTurnCount };
1018 }
1019
1020 const boundaryTurn = providerTurnsForThread(
1021 input.sourceProviderTurns,
1022 input.sourceProviderThread,
1023 ).find((turn) => turn.id === input.providerTurnId);
1024 const nativeTurnId = boundaryTurn?.nativeTurnRef?.nativeId;
1025 if (nativeTurnId === null || nativeTurnId === undefined) {
1026 return { lastTurnId: undefined, rollbackTurnCount };
1027 }
1028
1029 return { lastTurnId: nativeTurnId, rollbackTurnCount: 0 };
1030});
1031
1032/**
1033 * The generated `thread/read` response schema does not surface `historyMode`,
1034 * so the probe goes through the raw request channel with a permissive decode.
1035 */
1036const CodexThreadHistoryMetadata = Schema.Struct({
1037 thread: Schema.Struct({
1038 historyMode: Schema.optionalKey(Schema.Literals(["legacy", "paginated"])),
1039 status: Schema.optionalKey(Schema.Struct({ type: Schema.String })),
1040 }),
1041});
1042const decodeCodexThreadHistoryMetadata = Schema.decodeUnknownEffect(CodexThreadHistoryMetadata);
1043
1044const readCodexThreadHistoryMetadata = Effect.fn("CodexAdapterV2.readThreadHistoryMetadata")(
1045 function* (
1046 raw: Pick<CodexClient.CodexAppServerClient["Service"]["raw"], "request">,
1047 threadId: string,
1048 ) {
1049 const response = yield* raw.request("thread/read", { threadId, includeTurns: false });
1050 const metadata = yield* decodeCodexThreadHistoryMetadata(response).pipe(
1051 Effect.mapError((error) =>
1052 CodexErrors.CodexAppServerRequestError.invalidPayload(
1053 "thread/read",
1054 "decode-payload",
1055 error,
1056 ),
1057 ),
1058 );
1059 return {
1060 historyMode: metadata.thread.historyMode,
1061 loaded: metadata.thread.status?.type !== "notLoaded",
1062 };
1063 },
1064);
1065
1066export const resolveCodexRollbackTurnCount = Effect.fn("CodexAdapterV2.resolveRollbackTurnCount")(
1067 function* (input: ProviderAdapter.ProviderAdapterV2RollbackThreadInput) {
1068 const providerTurns = input.providerThreadTurns;
1069 switch (input.target.type) {
1070 case "thread_start":
1071 return providerTurns.filter(isTerminalProviderTurn).length;
1072 case "provider_turn": {
1073 if (input.target.providerTurn.providerThreadId !== input.providerThread.id) {
1074 return yield* new ProviderAdapter.ProviderAdapterRollbackThreadError({
1075 driver: CODEX_PROVIDER,
1076 providerThreadId: input.providerThread.id,
1077 cause: `Cannot roll back Codex thread ${input.providerThread.id} to provider turn ${input.target.providerTurn.id}: target turn belongs to provider thread ${input.target.providerTurn.providerThreadId}.`,
1078 });
1079 }
1080
1081 const rollbackTurnCount = countTerminalTurnsAfterBoundary(
1082 providerTurns,
1083 input.target.providerTurn.id,
1084 );
1085 if (rollbackTurnCount === null) {
1086 return yield* new ProviderAdapter.ProviderAdapterRollbackThreadError({
1087 driver: CODEX_PROVIDER,
1088 providerThreadId: input.providerThread.id,
1089 cause: `Cannot roll back Codex thread ${input.providerThread.id} to provider turn ${input.target.providerTurn.id}: target turn was not found in durable provider turn history.`,
1090 });
1091 }
1092
1093 return rollbackTurnCount;
1094 }
1095 }
1096 },
1097);
1098
1099function parseCodexRetryProgress(
1100 message: string,
1101): Pick<OrchestrationV2ProviderRetry, "attempt" | "maxAttempts"> | null {
1102 const match = /\b(\d+)\s*\/\s*(\d+)\b/u.exec(message);
1103 if (match?.[1] === undefined || match[2] === undefined) {
1104 return null;
1105 }
1106 const attempt = Number.parseInt(match[1], 10);
1107 const maxAttempts = Number.parseInt(match[2], 10);
1108 if (attempt < 1 || maxAttempts < 1) {
1109 return null;
1110 }
1111 return { attempt, maxAttempts };
1112}
1113
1114function codexErrorInfoCode(value: unknown): string | null {
1115 if (typeof value === "string") {
1116 return value;
1117 }
1118 if (typeof value !== "object" || value === null) {
1119 return null;
1120 }
1121 return Object.keys(value)[0] ?? null;
1122}
1123
1124interface ActiveCodexTurnContext {
1125 latestProviderFailure?: {
1126 readonly nativeMessage: string;
1127 readonly failure: OrchestrationV2ProviderFailure;
1128 };
1129 readonly nativeStartReady?: Deferred.Deferred<void>;
1130 readonly input: ProviderAdapter.ProviderAdapterV2TurnInput;
1131 readonly projectionAppThread: OrchestrationV2AppThread;
1132 readonly projectionThreadId: ThreadId;
1133 readonly projectionRunId: ProviderAdapter.ProviderAdapterV2TurnInput["runId"] | null;
1134 readonly nativeTurnId: string;
1135 readonly providerThread: OrchestrationV2ProviderThread;
1136 readonly providerTurnId: ProviderTurnId;
1137 readonly providerTurnOrdinal: number;
1138 readonly providerNodeId: OrchestrationV2ExecutionNode["id"];
1139 readonly providerNodeKind: OrchestrationV2ExecutionNode["kind"];
1140 readonly providerNodeStartedAt: DateTime.Utc | null;
1141 readonly itemParentNodeId: OrchestrationV2ExecutionNode["id"];
1142 readonly rootNodeId: OrchestrationV2ExecutionNode["id"];
1143 readonly subagent: CodexSubagentThreadContext | null;
1144 readonly startedAt: DateTime.Utc;
1145 // Item positions allocated in this turn. Later turns never look items up
1146 // here: late background items resolve their settled turn's context, and a
1147 // subagent's approvals allocate on the owning root turn.
1148 readonly itemPositions: Map<
1149 string,
1150 { readonly ordinal: number; readonly startedAt: DateTime.Utc }
1151 >;
1152}
1153
1154interface ActiveCodexProviderRetry {
1155 readonly nativeMessage: string;
1156 readonly retry: OrchestrationV2ProviderRetry;
1157 readonly failure: OrchestrationV2ProviderFailure;
1158 readonly startedAt: DateTime.Utc;
1159 readonly itemOrdinal: number;
1160}
1161
1162/** Snapshot of a still-running commandExecution item for interrupt/fail terminalization. */
1163interface TrackedRunningCommandItem {
1164 readonly id: string;
1165 readonly command: string;
1166 readonly aggregatedOutput?: string;
1167 readonly processId?: string;
1168}
1169
1170function isPersistentCodexDynamicTool(item: CodexDynamicToolItem): boolean {
1171 const input = item.arguments;
1172 if (input === null || typeof input !== "object" || Array.isArray(input)) {
1173 return false;
1174 }
1175 return Reflect.get(input, "persistent") === true;
1176}
1177
1178type CodexRootTerminalEvent = Extract<
1179 ProviderAdapter.ProviderAdapterV2Event,
1180 { readonly type: "turn.terminal" }
1181>;
1182
1183/** A completed root turn whose run stays open for the goal turn Codex starts next. */
1184interface CodexGoalHold {
1185 readonly context: ActiveCodexTurnContext;
1186 readonly event: Extract<CodexRootTerminalEvent, { readonly failure: null }>;
1187 /** Sent when the hold resolves, so until then Stop and steering still target this turn. */
1188 readonly completedTurn: OrchestrationV2ProviderTurn;
1189 /** The turn Codex continued with, or undefined when the run settled instead. */
1190 readonly next: Deferred.Deferred<ActiveCodexTurnContext | undefined>;
1191}
1192
1193interface DeferredCodexRootTerminal {
1194 readonly context: ActiveCodexTurnContext;
1195 readonly event: CodexRootTerminalEvent;
1196}
1197
1198interface CodexSubagentThreadContext {
1199 parentContext: ActiveCodexTurnContext;
1200 readonly providerThread: OrchestrationV2ProviderThread;
1201 readonly childThread: OrchestrationV2AppThread;
1202 readonly subagentNodeId: OrchestrationV2ExecutionNode["id"];
1203 readonly childRootNodeId: OrchestrationV2ExecutionNode["id"];
1204 readonly childThreadId: ThreadId;
1205 readonly nativeToolCallId: string;
1206 readonly ordinal: number;
1207 readonly nativeTurnIds: Set<string>;
1208 startedAt: DateTime.Utc;
1209 readonly turnItemId: OrchestrationV2TurnItem["id"];
1210 readonly turnItemOrdinal: number;
1211 task: OrchestrationV2Subagent;
1212}
1213
1214/**
1215 * The top-level turn a (possibly nested) subagent turn runs under, and the
1216 * subagent on that turn's thread that leads to it. Native subagent threads are
1217 * hidden from the sidebar, so their approvals are asked there instead.
1218 */
1219const approvalOwnerCodexTurn = (
1220 context: ActiveCodexTurnContext,
1221): {
1222 readonly owner: ActiveCodexTurnContext;
1223 readonly subagent: CodexSubagentThreadContext | null;
1224} => {
1225 let owner = context;
1226 let subagent: CodexSubagentThreadContext | null = null;
1227 while (owner.subagent !== null) {
1228 subagent = owner.subagent;
1229 owner = owner.subagent.parentContext;
1230 }
1231 return { owner, subagent };
1232};
1233
1234const isDescendantCodexTurn = (
1235 candidate: ActiveCodexTurnContext,
1236 ancestor: ActiveCodexTurnContext,
1237): boolean => {
1238 let parent = candidate.subagent?.parentContext;
1239 while (parent !== undefined) {
1240 if (parent === ancestor) {
1241 return true;
1242 }
1243 parent = parent.subagent?.parentContext;
1244 }
1245 return false;
1246};
1247
1248interface PendingCodexSubagentTurnStarted {
1249 readonly nativeTurnId: string;
1250 readonly startedAt: DateTime.Utc;
1251}
1252
1253type PendingCodexRuntimeRequest =
1254 | {
1255 readonly type: "approval";
1256 readonly requestId: RuntimeRequestId;
1257 readonly requestKind: ProviderRequestKind;
1258 readonly decision: Deferred.Deferred<ProviderApprovalDecision, never>;
1259 }
1260 | {
1261 readonly type: "user_input";
1262 readonly requestId: RuntimeRequestId;
1263 readonly answers: Deferred.Deferred<ProviderUserInputAnswers, never>;
1264 };
1265
1266type CodexWebSearchItem = {
1267 readonly id: string;
1268 readonly type: "webSearch";
1269 readonly query?: string | null;
1270 readonly action?:
1271 | CodexSchema.V2ItemStartedNotification__WebSearchAction
1272 | CodexSchema.V2ItemCompletedNotification__WebSearchAction
1273 | null;
1274};
1275
1276export type CodexDynamicToolItem = Extract<
1277 | CodexSchema.V2ItemStartedNotification__ThreadItem
1278 | CodexSchema.V2ItemCompletedNotification__ThreadItem,
1279 { readonly type: "mcpToolCall" | "dynamicToolCall" }
1280>;
1281
1282type CodexCollabAgentToolCallItem = Extract<
1283 CodexSchema.V2ItemCompletedNotification__ThreadItem,
1284 { readonly type: "collabAgentToolCall" }
1285>;
1286
1287type CodexSubAgentActivityItem = Extract<
1288 | CodexSchema.V2ItemStartedNotification__ThreadItem
1289 | CodexSchema.V2ItemCompletedNotification__ThreadItem,
1290 { readonly type: "subAgentActivity" }
1291>;
1292
1293export interface CodexAppServerClientFactoryShape {
1294 readonly open: (input: {
1295 readonly instanceId: ProviderInstanceId;
1296 readonly threadId: ThreadId;
1297 readonly providerSessionId: OrchestrationV2ProviderSession["id"];
1298 readonly runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy;
1299 readonly settings: CodexSettings;
1300 readonly environment: NodeJS.ProcessEnv;
1301 }) => Effect.Effect<
1302 CodexClient.CodexAppServerClient["Service"],
1303 ProviderAdapter.ProviderAdapterOpenSessionError,
1304 Scope.Scope
1305 >;
1306}
1307
1308export class CodexAppServerClientFactory extends Context.Service<
1309 CodexAppServerClientFactory,
1310 CodexAppServerClientFactoryShape
1311>()("t3/orchestration-v2/Adapters/CodexAdapterV2/CodexAppServerClientFactory") {}
1312
1313/**
1314 * Config overrides sent with every `thread/start`, `thread/resume` and `thread/fork`.
1315 * Codex layers these above the user's and project's `config.toml`.
1316 *
1317 * - Codex 0.152 made the `update_plan` checklist tool opt-in; T3 renders it as
1318 * the todo list.
1319 * - Codex waits this long for MCP servers before the first model request and
1320 * leaves out any server still starting (default 1 s, too short for cold
1321 * `npx`/`uvx` servers). 10 s covers most slow starts, and a hung server
1322 * delays only the first turn of a session by 10 s, not its 30 s timeout.
1323 */
1324export const CODEX_THREAD_CONFIG = {
1325 "tools.update_plan.enabled": true,
1326 mcp_optional_startup_grace_ms: 10_000,
1327} as const;
1328
1329// T3 wait tools can block for up to an hour. Keep Codex's MCP timeout above
1330// that limit so the server's own wait timeout ends a long call.
1331const CODEX_T3_MCP_TOOL_TIMEOUT_SEC = 65 * 60;
1332
1333export function codexThreadRuntimeParams(input: {
1334 readonly mcpSession: McpProviderSession.McpProviderSessionConfig | undefined;
1335 readonly modelSelection?: { readonly model: string };
1336 readonly runtimePolicy?: ProviderAdapter.ProviderAdapterV2RuntimePolicy;
1337 readonly scriptProgressConfig?: Readonly<Record<string, Schema.Json>>;
1338}): {
1339 readonly cwd?: string;
1340 readonly model?: string;
1341 readonly config: Readonly<Record<string, Schema.Json>>;
1342} {
1343 const { mcpSession } = input;
1344 return {
1345 ...(input.runtimePolicy?.cwd == null ? {} : { cwd: input.runtimePolicy.cwd }),
1346 ...(input.modelSelection === undefined ? {} : { model: input.modelSelection.model }),
1347 config: {
1348 ...CODEX_THREAD_CONFIG,
1349 ...input.scriptProgressConfig,
1350 ...(mcpSession === undefined
1351 ? {}
1352 : {
1353 mcp_servers: {
1354 "t3-code": {
1355 url: mcpSession.endpoint,
1356 tool_timeout_sec: CODEX_T3_MCP_TOOL_TIMEOUT_SEC,
1357 http_headers: {
1358 Authorization: mcpSession.authorizationHeader,
1359 },
1360 },
1361 },
1362 }),
1363 },
1364 };
1365}
1366
1367const decodeCodexResumeMetadata = Schema.decodeUnknownEffect(
1368 Schema.Struct({ thread: Schema.Struct({ id: Schema.String, updatedAt: Schema.Number }) }),
1369);
1370
1371const CodexChildSelection = Schema.Struct({
1372 thread: Schema.Struct({ id: Schema.String }),
1373 model: Schema.NullOr(Schema.String),
1374 reasoningEffort: Schema.optional(Schema.NullOr(Schema.String)),
1375 serviceTier: Schema.optional(Schema.NullOr(Schema.String)),
1376});
1377const decodeCodexChildModel = Schema.decodeUnknownEffect(CodexChildSelection);
1378
1379const decodeCodexChildThread = Schema.decodeUnknownEffect(
1380 Schema.Struct({
1381 thread: Schema.Struct({
1382 id: Schema.String,
1383 model: Schema.optional(Schema.NullOr(Schema.String)),
1384 reasoningEffort: Schema.optional(Schema.NullOr(Schema.String)),
1385 }),
1386 }),
1387);
1388
1389export const makeCodexAppServerSpawnCommand = Effect.fn(
1390 "CodexAdapterV2.makeCodexAppServerSpawnCommand",
1391)(function* (input: {
1392 readonly command: string;
1393 readonly args: ReadonlyArray<string>;
1394 readonly cwd?: string | undefined;
1395 readonly env?: NodeJS.ProcessEnv | undefined;
1396 readonly extendEnv?: boolean | undefined;
1397 readonly threadId?: string | undefined;
1398}) {
1399 const launch = yield* (yield* AgentScope).wrap({
1400 command: input.command,
1401 args: input.args,
1402 name: "codex",
1403 threadId: input.threadId,
1404 env: input.env,
1405 });
1406 const spawnCommand = yield* resolveSpawnCommand(launch.command, launch.args, {
1407 ...(input.env === undefined ? {} : { env: input.env }),
1408 ...(input.extendEnv === undefined ? {} : { extendEnv: input.extendEnv }),
1409 });
1410 return ChildProcess.make(spawnCommand.command, spawnCommand.args, {
1411 ...(input.cwd === undefined ? {} : { cwd: input.cwd }),
1412 ...(input.env === undefined ? {} : { env: input.env }),
1413 ...(input.extendEnv === undefined ? {} : { extendEnv: input.extendEnv }),
1414 shell: spawnCommand.shell,
1415 });
1416});
1417
1418const layerCodexAppServerClientFactoryCommand = (
1419 options: CodexClient.CodexAppServerClientOptions & {
1420 readonly command: string;
1421 readonly args?: ReadonlyArray<string>;
1422 readonly cwd?: string;
1423 readonly env?: NodeJS.ProcessEnv;
1424 },
1425): Layer.Layer<CodexAppServerClientFactory, never, ChildProcessSpawner.ChildProcessSpawner> =>
1426 Layer.effect(
1427 CodexAppServerClientFactory,
1428 Effect.gen(function* () {
1429 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
1430 return CodexAppServerClientFactory.of({
1431 open: (input) =>
1432 Effect.gen(function* () {
1433 const scope = yield* Scope.Scope;
1434 const command = yield* makeCodexAppServerSpawnCommand({
1435 command: options.command,
1436 args: [...(options.args ?? [])],
1437 ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
1438 ...(options.env === undefined ? {} : { env: options.env, extendEnv: true }),
1439 });
1440 const handle = yield* spawner.spawn(command).pipe(
1441 Effect.provideService(Scope.Scope, scope),
1442 Effect.mapError(
1443 (cause) =>
1444 new ProviderAdapter.ProviderAdapterOpenSessionError({
1445 driver: CODEX_PROVIDER,
1446 providerSessionId: input.providerSessionId,
1447 cause,
1448 }),
1449 ),
1450 );
1451 const context = yield* Layer.build(CodexClient.layerChildProcess(handle, options));
1452 return yield* Effect.service(CodexClient.CodexAppServerClient).pipe(
1453 Effect.provide(context),
1454 );
1455 }),
1456 });
1457 }),
1458 );
1459
1460export function makeCodexAppServerProtocolLogger(input: {
1461 readonly nativeEventLogger: ProviderEventLoggers.EventNdjsonLogger | undefined;
1462 readonly threadId: ThreadId;
1463 readonly providerSessionId: OrchestrationV2ProviderSession["id"];
1464}): CodexClient.CodexAppServerClientOptions["logger"] | undefined {
1465 const { nativeEventLogger } = input;
1466 if (nativeEventLogger === undefined) {
1467 return undefined;
1468 }
1469
1470 return (event) => {
1471 if (!shouldPersistProviderEvent("native", event)) return Effect.void;
1472 return nativeEventLogger
1473 .write(
1474 {
1475 provider: CODEX_PROVIDER,
1476 protocol: "codex.app-server",
1477 kind: "protocol",
1478 providerSessionId: input.providerSessionId,
1479 event: redactCodexProtocolValue(boundProviderEventForLogging(event)),
1480 },
1481 input.threadId,
1482 )
1483 .pipe(Effect.ignore);
1484 };
1485}
1486
1487function redactCodexProtocolValue(value: unknown): unknown {
1488 if (Array.isArray(value)) {
1489 return value.map(redactCodexProtocolValue);
1490 }
1491 if (value === null || typeof value !== "object") {
1492 if (typeof value !== "string") {
1493 return value;
1494 }
1495 if (/^Bearer\s+/i.test(value)) {
1496 return "[REDACTED]";
1497 }
1498 const trimmed = value.trim();
1499 if (
1500 !(
1501 (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
1502 (trimmed.startsWith("[") && trimmed.endsWith("]"))
1503 )
1504 ) {
1505 return value;
1506 }
1507 try {
1508 return JSON.stringify(
1509 redactCodexProtocolValue(boundProviderEventForLogging(JSON.parse(trimmed) as unknown)),
1510 );
1511 } catch {
1512 return value;
1513 }
1514 }
1515 return Object.fromEntries(
1516 Object.entries(value).map(([key, nested]) => [
1517 key,
1518 isSensitiveCodexProtocolKey(key) ? "[REDACTED]" : redactCodexProtocolValue(nested),
1519 ]),
1520 );
1521}
1522
1523function isSensitiveCodexProtocolKey(key: string): boolean {
1524 const normalized = key.replace(/[^a-z0-9]/gi, "").toLowerCase();
1525 return (
1526 normalized.endsWith("authorization") ||
1527 normalized.endsWith("apikey") ||
1528 normalized.endsWith("token") ||
1529 normalized.endsWith("password") ||
1530 normalized.endsWith("secret")
1531 );
1532}
1533
1534export const layerAppServerClientFactory: Layer.Layer<
1535 CodexAppServerClientFactory,
1536 never,
1537 ChildProcessSpawner.ChildProcessSpawner | ProviderEventLoggers.ProviderEventLoggers
1538> = Layer.effect(
1539 CodexAppServerClientFactory,
1540 Effect.gen(function* () {
1541 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
1542 const { native: nativeEventLogger } = yield* ProviderEventLoggers.ProviderEventLoggers;
1543 const agentScope = yield* AgentScope;
1544
1545 return CodexAppServerClientFactory.of({
1546 open: (input) =>
1547 Effect.gen(function* () {
1548 const scope = yield* Scope.Scope;
1549 const environment = {
1550 ...codexAppServerEnvironment(input.environment),
1551 ...(input.settings.homePath ? { CODEX_HOME: input.settings.homePath } : {}),
1552 };
1553 const command = yield* makeCodexAppServerSpawnCommand({
1554 command: input.settings.binaryPath || "codex",
1555 args: codexAppServerArgs(
1556 resolveCodexLaunchArgs(input.settings.launchArgs, input.environment),
1557 ),
1558 env: environment,
1559 threadId: input.threadId,
1560 }).pipe(Effect.provideService(AgentScope, agentScope));
1561 const handle = yield* spawner.spawn(command).pipe(
1562 Effect.provideService(Scope.Scope, scope),
1563 Effect.mapError(
1564 (cause) =>
1565 new ProviderAdapter.ProviderAdapterOpenSessionError({
1566 driver: CODEX_PROVIDER,
1567 providerSessionId: input.providerSessionId,
1568 cause,
1569 }),
1570 ),
1571 );
1572 const protocolLogger = makeCodexAppServerProtocolLogger({
1573 nativeEventLogger,
1574 threadId: input.threadId,
1575 providerSessionId: input.providerSessionId,
1576 });
1577 const clientOptions: CodexClient.CodexAppServerClientOptions =
1578 protocolLogger === undefined
1579 ? {}
1580 : {
1581 logIncoming: true,
1582 logOutgoing: true,
1583 logger: protocolLogger,
1584 };
1585 const context = yield* Layer.build(CodexClient.layerChildProcess(handle, clientOptions));
1586 return yield* Effect.service(CodexClient.CodexAppServerClient).pipe(
1587 Effect.provide(context),
1588 );
1589 }),
1590 });
1591 }),
1592);
1593
1594export type CodexAdapterV2DriverEnv =
1595 | CodexAppServerClientFactory
1596 | Crypto.Crypto
1597 | FileSystem.FileSystem
1598 | IdAllocator.IdAllocatorV2
1599 | ScriptProgress.ScriptProgress
1600 | McpProviderSessions.McpProviderSessions
1601 | Path.Path
1602 | ServerConfig;
1603
1604export const createCodexAdapterV2 = (
1605 { instanceId, environment, enabled, config }: ProviderAdapterDriverCreateInput<CodexSettings>,
1606 hooks: Pick<CodexAdapterV2Options, "onUsageLimits" | "resolveRuntime" | "authenticate"> = {},
1607) =>
1608 Effect.gen(function* () {
1609 const clientFactory = yield* CodexAppServerClientFactory;
1610 const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
1611 const crypto = yield* Crypto.Crypto;
1612 const fileSystem = yield* FileSystem.FileSystem;
1613 const hostEnvironment = yield* HostProcess.Environment;
1614 const idAllocator = yield* IdAllocator.IdAllocatorV2;
1615 const serverConfig = yield* ServerConfig;
1616 const homeLayout = yield* resolveCodexHomeLayout(config);
1617
1618 yield* materializeCodexShadowHome(homeLayout).pipe(
1619 Effect.mapError(
1620 (cause) =>
1621 new ProviderAdapterDriverCreateError({
1622 driver: CODEX_DRIVER_KIND,
1623 instanceId,
1624 detail: "Failed to materialize the Codex shadow home.",
1625 cause,
1626 }),
1627 ),
1628 );
1629
1630 const settings = {
1631 ...config,
1632 enabled,
1633 binaryPath: expandHomePath(config.binaryPath, yield* HostProcess.HomeDirectory),
1634 homePath: homeLayout.effectiveHomePath ?? "",
1635 } satisfies CodexSettings;
1636
1637 return yield* makeCodexAdapterV2({
1638 instanceId,
1639 settings,
1640 environment: yield* mergeProviderInstanceEnvironment(environment, hostEnvironment),
1641 clientFactory,
1642 crypto,
1643 fileSystem,
1644 idAllocator,
1645 serverConfig,
1646 continuationRequests,
1647 ...hooks,
1648 });
1649 });
1650
1651export const CodexAdapterV2Driver: ProviderAdapterDriver<CodexSettings, CodexAdapterV2DriverEnv> = {
1652 driverKind: CODEX_DRIVER_KIND,
1653 configSchema: CodexSettings,
1654 defaultConfig: (): CodexSettings => DEFAULT_CODEX_SETTINGS,
1655 create: createCodexAdapterV2,
1656};
1657
1658const layer: Layer.Layer<
1659 ProviderAdapter.ProviderAdapterV2,
1660 never,
1661 | CodexAppServerClientFactory
1662 | Crypto.Crypto
1663 | FileSystem.FileSystem
1664 | IdAllocator.IdAllocatorV2
1665 | ScriptProgress.ScriptProgress
1666 | McpProviderSessions.McpProviderSessions
1667 | ServerConfig
1668> = Layer.effect(
1669 ProviderAdapter.ProviderAdapterV2,
1670 Effect.gen(function* () {
1671 const clientFactory = yield* CodexAppServerClientFactory;
1672 const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
1673 const crypto = yield* Crypto.Crypto;
1674 const fileSystem = yield* FileSystem.FileSystem;
1675 const hostEnvironment = yield* HostProcess.Environment;
1676 const idAllocator = yield* IdAllocator.IdAllocatorV2;
1677 const serverConfig = yield* ServerConfig;
1678
1679 return yield* makeCodexAdapterV2({
1680 instanceId: CODEX_DEFAULT_INSTANCE_ID,
1681 settings: DEFAULT_CODEX_SETTINGS,
1682 environment: hostEnvironment,
1683 clientFactory,
1684 crypto,
1685 fileSystem,
1686 idAllocator,
1687 serverConfig,
1688 continuationRequests,
1689 });
1690 }),
1691);
1692
1693export interface CodexAdapterV2Options {
1694 readonly instanceId: ProviderInstanceId;
1695 readonly settings: CodexSettings;
1696 readonly environment: NodeJS.ProcessEnv;
1697 readonly clientFactory: CodexAppServerClientFactoryShape;
1698 readonly onUsageLimits?: ManagedServerProvider["applyUsageLimits"];
1699 /**
1700 * Resolves launch settings when each session opens, replacing `settings` and
1701 * `environment`. Managed ChatGPT sign-in uses it to launch the T3-installed
1702 * Codex with a current access token.
1703 */
1704 readonly resolveRuntime?: Effect.Effect<CodexEffectiveRuntime, ProviderSetupError, Scope.Scope>;
1705 readonly authenticate?: (
1706 client: CodexClient.CodexAppServerClient["Service"],
1707 ) => Effect.Effect<void, CodexErrors.CodexAppServerError>;
1708 readonly crypto: Crypto.Crypto;
1709 readonly fileSystem: FileSystem.FileSystem;
1710 readonly idAllocator: IdAllocator.IdAllocatorV2["Service"];
1711 readonly serverConfig: ServerConfig["Service"];
1712 /**
1713 * Sink for post-settle background command completions so the orchestrator
1714 * can start a continuation run. Optional: adapters that omit it keep
1715 * projection-only handling for late item completions.
1716 */
1717 readonly continuationRequests?: {
1718 readonly offer: (
1719 request: ProviderContinuationRequests.ProviderContinuationRequest,
1720 ) => Effect.Effect<void>;
1721 };
1722}
1723
1724export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
1725 adapterOptions: CodexAdapterV2Options,
1726) {
1727 const mcpSessions = yield* McpProviderSessions.McpProviderSessions;
1728 const scriptProgress = yield* ScriptProgress.ScriptProgress;
1729 const readMcpSession = (threadId: ThreadId | null) =>
1730 threadId === null ? Effect.succeed(undefined) : mcpSessions.read(threadId);
1731 const { clientFactory, crypto, fileSystem, idAllocator, serverConfig } = adapterOptions;
1732 const continuationRequests = adapterOptions.continuationRequests;
1733
1734 return ProviderAdapter.ProviderAdapterV2.of({
1735 instanceId: adapterOptions.instanceId,
1736 driver: CODEX_PROVIDER,
1737 getCapabilities: () => Effect.succeed(CodexProviderCapabilitiesV2),
1738 planSelectionTransition: () => Effect.succeed(turnScopedSelectionTransition()),
1739 openSession: (input) =>
1740 Effect.gen(function* () {
1741 const scope = yield* Scope.Scope;
1742 const resolvedRuntime =
1743 adapterOptions.resolveRuntime === undefined
1744 ? undefined
1745 : yield* adapterOptions.resolveRuntime.pipe(
1746 Effect.mapError(
1747 (cause) =>
1748 new ProviderAdapter.ProviderAdapterOpenSessionError({
1749 driver: CODEX_PROVIDER,
1750 providerSessionId: input.providerSessionId,
1751 cause,
1752 }),
1753 ),
1754 );
1755 const client = yield* clientFactory.open({
1756 instanceId: adapterOptions.instanceId,
1757 threadId: input.threadId,
1758 providerSessionId: input.providerSessionId,
1759 runtimePolicy: input.runtimePolicy,
1760 settings: resolvedRuntime?.config ?? adapterOptions.settings,
1761 environment: resolvedRuntime?.environment ?? adapterOptions.environment,
1762 });
1763 const additionalContextByThread = yield* Ref.make(
1764 new Map<
1765 string,
1766 NonNullable<CodexTurnStartParamsWithCollaborationMode["additionalContext"]>
1767 >(),
1768 );
1769 // Codex drops client developer messages during compaction but only
1770 // resends additionalContext when it changes. Restore the current entries.
1771 const restoreAdditionalContext = (threadId: string) =>
1772 Effect.gen(function* () {
1773 const context = (yield* Ref.get(additionalContextByThread)).get(threadId);
1774 if (!context) return;
1775 yield* client.request("thread/inject_items", {
1776 threadId,
1777 // Restored as Codex sent them: application context as developer
1778 // input, untrusted context as quoted user-side input.
1779 items: Object.entries(context).map(([key, entry]) =>
1780 entry.kind === "untrusted"
1781 ? {
1782 type: "message",
1783 role: "user",
1784 content: [
1785 {
1786 type: "input_text",
1787 text: `<external_${key}>${entry.value}</external_${key}>`,
1788 },
1789 ],
1790 }
1791 : {
1792 type: "message",
1793 role: "developer",
1794 content: [{ type: "input_text", text: `<${key}>${entry.value}</${key}>` }],
1795 },
1796 ),
1797 });
1798 }).pipe(
1799 Effect.timeout("10 seconds"),
1800 Effect.catch((cause) =>
1801 Effect.logWarning("Failed to restore Codex additional context after compaction.", {
1802 cause,
1803 }),
1804 ),
1805 );
1806 const initialized = yield* Ref.make(false);
1807 // Threads share this app-server, and Codex rejects a second
1808 // `initialize`. Callers wait for an in-flight handshake instead of
1809 // starting their own; a failed handshake leaves the flag unset so the
1810 // next caller retries.
1811 const initializePermit = yield* Semaphore.make(1);
1812 const ensureInitialized = initializePermit.withPermit(
1813 Effect.gen(function* () {
1814 const alreadyInitialized = yield* Ref.get(initialized);
1815 if (alreadyInitialized) {
1816 return;
1817 }
1818
1819 yield* client
1820 .request("initialize", {
1821 // Codex uses the client name as the request originator, so sessions
1822 // identify themselves exactly like the provider probe.
1823 clientInfo: buildCodexInitializeParams().clientInfo,
1824 capabilities: CODEX_CLIENT_CAPABILITIES,
1825 })
1826 .pipe(
1827 Effect.catchTags({
1828 // A caller interrupted after its `initialize` reached Codex
1829 // leaves the app-server initialized but the flag unset.
1830 CodexAppServerRequestError: (error) =>
1831 error.code === -32600 && error.errorMessage === "Already initialized"
1832 ? Effect.void
1833 : Effect.fail(error),
1834 }),
1835 );
1836 yield* client.notify("initialized", undefined);
1837 if (adapterOptions.authenticate) yield* adapterOptions.authenticate(client);
1838 yield* Ref.set(initialized, true);
1839 }),
1840 );
1841 const readScriptProgressConfig = Effect.fnUntraced(function* (
1842 threadId: ThreadId | null,
1843 cwd: string | null | undefined,
1844 ) {
1845 if (threadId === null) return {};
1846 const environment = yield* scriptProgress.environmentForThread(threadId);
1847 if (Object.keys(environment).length === 0) return {};
1848 yield* ensureInitialized;
1849 const { config } = yield* client.request("config/read", {
1850 ...(cwd == null ? {} : { cwd }),
1851 includeLayers: false,
1852 });
1853 return codexScriptProgressConfig(
1854 config.shell_environment_policy,
1855 environment,
1856 (resolvedRuntime?.environment ?? adapterOptions.environment).NODE_OPTIONS,
1857 );
1858 });
1859 const now = yield* DateTime.now;
1860 const session = providerSession({
1861 providerSessionId: input.providerSessionId,
1862 providerInstanceId: adapterOptions.instanceId,
1863 cwd: input.runtimePolicy.cwd,
1864 model: input.modelSelection.model,
1865 now,
1866 });
1867 const events = yield* Queue.unbounded<ProviderAdapter.ProviderAdapterV2Event>();
1868 const rateLimitSnapshot = yield* Ref.make<CodexRateLimitSnapshot | undefined>(undefined);
1869 const limitedTurnItems = yield* Ref.make(
1870 new Map<ProviderThreadId, Extract<OrchestrationV2TurnItem, { type: "error" }>>(),
1871 );
1872 const activeTurns = yield* Ref.make(new Map<string, ActiveCodexTurnContext>());
1873 const turnTokenUsageByThread = new Map<string, CodexTurnTokenUsageState>();
1874 const usageStateForThread = (nativeThreadId: string) => {
1875 let state = turnTokenUsageByThread.get(nativeThreadId);
1876 if (!state) {
1877 state = makeCodexTurnTokenUsageState();
1878 turnTokenUsageByThread.set(nativeThreadId, state);
1879 }
1880 return state;
1881 };
1882 const beginTurnTokenUsage = (context: ActiveCodexTurnContext) => {
1883 const nativeThreadId = context.providerThread.nativeThreadRef?.nativeId;
1884 if (!nativeThreadId) return;
1885 const state = usageStateForThread(nativeThreadId);
1886 if (state.activeTurnId !== context.nativeTurnId) {
1887 state.byTurnId.clear();
1888 state.activeTurnId = context.nativeTurnId;
1889 getCodexTurnAccumulator(state, context.nativeTurnId);
1890 }
1891 };
1892 const markSubagentUsage = (context: ActiveCodexTurnContext) => {
1893 const nativeThreadId = context.providerThread.nativeThreadRef?.nativeId;
1894 if (!nativeThreadId) return;
1895 getCodexTurnAccumulator(
1896 usageStateForThread(nativeThreadId),
1897 context.nativeTurnId,
1898 ).hasSubagents = true;
1899 };
1900 const pendingRootTurns = yield* Ref.make(
1901 new Map<string, ProviderAdapter.ProviderAdapterV2TurnInput>(),
1902 );
1903 const turnWaiters = yield* Ref.make(new Map<string, Deferred.Deferred<void, never>>());
1904 const subagentThreads = yield* Ref.make(new Map<string, CodexSubagentThreadContext>());
1905 const subagentSelections = new Map<string, Omit<ModelSelection, "instanceId">>();
1906 const pendingSubagentTurns = yield* Ref.make(
1907 new Map<string, ReadonlyArray<PendingCodexSubagentTurnStarted>>(),
1908 );
1909 const nextProviderTurnOrdinals = yield* Ref.make(new Map<string, number>());
1910 const providerRetries = yield* Ref.make(
1911 new Map<ProviderTurnId, ActiveCodexProviderRetry>(),
1912 );
1913 const planIds = yield* Ref.make(new Map<string, OrchestrationV2PlanArtifact["id"]>());
1914 const pendingRuntimeRequests = yield* Ref.make(
1915 new Map<string, PendingCodexRuntimeRequest>(),
1916 );
1917 /**
1918 * Approval kinds the user chose "Always allow this session" for, by
1919 * native thread. Codex's own acceptForSession only skips the exact
1920 * command or patch it answered, so every new command would ask again.
1921 * A turn in another runtime mode ends the allowance, and so does the
1922 * thread unloading, like Codex's own session approvals.
1923 */
1924 const sessionApprovals = new Map<
1925 string,
1926 { readonly runtimeMode: RuntimeMode; readonly kinds: Set<ProviderRequestKind> }
1927 >();
1928 /**
1929 * The allowed kinds for an approval request's turn. Read it when the
1930 * request arrives and add to that same set when the user answers, so a
1931 * late answer from a turn whose mode has since ended is dropped.
1932 */
1933 /** Every native turn registers here. A turn in another mode ends the allowance. */
1934 const endSessionApprovalsOnModeChange = (context: ActiveCodexTurnContext) => {
1935 const nativeThreadId = context.providerThread.nativeThreadRef?.nativeId;
1936 if (
1937 nativeThreadId != null &&
1938 sessionApprovals.get(nativeThreadId)?.runtimeMode !==
1939 context.input.runtimePolicy.runtimeMode
1940 ) {
1941 sessionApprovals.delete(nativeThreadId);
1942 }
1943 };
1944 const sessionAllowance = (
1945 nativeThreadId: string,
1946 context: ActiveCodexTurnContext,
1947 ): Set<ProviderRequestKind> => {
1948 const runtimeMode = context.input.runtimePolicy.runtimeMode;
1949 const allowed = sessionApprovals.get(nativeThreadId);
1950 if (allowed?.runtimeMode === runtimeMode) return allowed.kinds;
1951 if (allowed !== undefined) return new Set();
1952 const kinds = new Set<ProviderRequestKind>();
1953 sessionApprovals.set(nativeThreadId, { runtimeMode, kinds });
1954 return kinds;
1955 };
1956 /**
1957 * Turn contexts retained past turn/completed while background command
1958 * items started in that turn are still running, so late item events
1959 * keep projecting instead of being dropped.
1960 */
1961 const settledTurns = yield* Ref.make(new Map<string, ActiveCodexTurnContext>());
1962 const runningCommandItemsByTurn = yield* Ref.make(
1963 new Map<string, Map<string, TrackedRunningCommandItem>>(),
1964 );
1965 const runningDynamicToolsByTurn = yield* Ref.make(
1966 new Map<string, Map<string, CodexDynamicToolItem>>(),
1967 );
1968 const interruptingNativeTurns = yield* Ref.make(new Set<string>());
1969 const terminalizedNonCompletedNativeTurns = yield* Ref.make(new Set<string>());
1970 // Keep the run event stream open until descendant provider state is
1971 // terminal, otherwise the root terminal can strand child projections.
1972 const deferredRootTerminals = yield* Ref.make(new Map<string, DeferredCodexRootTerminal>());
1973 const offeredContinuationItemsByTurn = yield* Ref.make(new Map<string, Set<string>>());
1974 const finalAnswerItemIdsByTurn = yield* Ref.make(new Map<string, Set<string>>());
1975 const completedFinalAnswerTextsByTurn = yield* Ref.make(new Map<string, Set<string>>());
1976 // Native completion and the interrupt timeout share one finalization
1977 // path. Serialize the race so only one can publish terminal events.
1978 const turnTerminalizationPermit = yield* Semaphore.make(1);
1979 // Native goals by Codex thread, and the root provider thread snapshot
1980 // each goal change is written onto.
1981 const goalsByNativeThread = new Map<string, OrchestrationV2ProviderGoal | null>();
1982 const rootProviderThreads = new Map<string, OrchestrationV2ProviderThread>();
1983 // A completed root turn whose goal is still active. Its run stays open
1984 // for the turn Codex starts next, so goal phases do not read as Done.
1985 const goalHolds = new Map<string, CodexGoalHold>();
1986 // The root turns of each continued goal run, keyed by each of its turns.
1987 // A Stop or steer that names an earlier turn reaches the newest one, and
1988 // Stop also reaches work an earlier turn left running. Dropped when the
1989 // run settles.
1990 const goalRuns = new Map<ProviderTurnId, Array<ActiveCodexTurnContext>>();
1991 // `/goal` runs whose goal turns active after the first turn starts.
1992 const goalActivations = new Map<string, { stopped: boolean }>();
1993 // Stops in progress by native thread. Until a Stop resolves its target,
1994 // a held run must not settle as completed.
1995 const goalStops = new Map<string, number>();
1996 const latestGoalTurnId = (providerTurnId: ProviderTurnId) =>
1997 goalRuns.get(providerTurnId)?.at(-1)?.providerTurnId ?? providerTurnId;
1998
1999 const emitProviderEvent = (event: ProviderAdapter.ProviderAdapterV2Event) =>
2000 Queue.offer(events, event).pipe(Effect.asVoid);
2001
2002 /** Writes the thread's current native goal onto its root provider thread. */
2003 const emitGoalUpdate = Effect.fnUntraced(function* (nativeThreadId: string) {
2004 const providerThread = rootProviderThreads.get(nativeThreadId);
2005 if (providerThread === undefined || !goalsByNativeThread.has(nativeThreadId)) return;
2006 const goal = goalsByNativeThread.get(nativeThreadId) ?? null;
2007 if (providerGoalsEqual(providerThread.goal ?? null, goal)) return;
2008 const updated = { ...providerThread, goal, updatedAt: yield* DateTime.now };
2009 rootProviderThreads.set(nativeThreadId, updated);
2010 yield* emitProviderEvent({
2011 type: "provider_thread.updated",
2012 driver: CODEX_PROVIDER,
2013 providerThread: updated,
2014 });
2015 });
2016
2017 const rememberRootProviderThread = Effect.fnUntraced(function* (
2018 providerThread: OrchestrationV2ProviderThread,
2019 ) {
2020 const nativeThreadId = providerThread.nativeThreadRef?.nativeId;
2021 if (nativeThreadId == null) return;
2022 rootProviderThreads.set(nativeThreadId, providerThread);
2023 yield* emitGoalUpdate(nativeThreadId);
2024 });
2025
2026 // Call only for new model-output activity. A local item/completed can
2027 // arrive while the upstream response stream is still retrying.
2028 const completeProviderRetry = Effect.fn("CodexAdapterV2.completeProviderRetry")(function* (
2029 context: ActiveCodexTurnContext,
2030 updatedAt: DateTime.Utc,
2031 ) {
2032 const providerRetry = yield* Ref.modify(providerRetries, (current) => {
2033 const retry = current.get(context.providerTurnId);
2034 if (retry === undefined) {
2035 return [undefined, current] as const;
2036 }
2037 const updated = new Map(current);
2038 updated.delete(context.providerTurnId);
2039 return [retry, updated] as const;
2040 });
2041 if (providerRetry === undefined) {
2042 return;
2043 }
2044 yield* emitProviderEvent({
2045 type: "turn_item.updated",
2046 driver: CODEX_PROVIDER,
2047 turnItem: makeProviderRetryTurnItem({
2048 driver: CODEX_PROVIDER,
2049 threadId: context.projectionThreadId,
2050 runId: context.projectionRunId,
2051 nodeId: context.providerNodeId,
2052 providerThreadId: context.providerThread.id,
2053 providerTurnId: context.providerTurnId,
2054 itemOrdinal: providerRetry.itemOrdinal,
2055 failure: providerRetry.failure,
2056 retry: providerRetry.retry,
2057 status: "completed",
2058 startedAt: providerRetry.startedAt,
2059 updatedAt,
2060 }),
2061 });
2062 });
2063
2064 const registerRootTurn = (input: {
2065 readonly turnInput: ProviderAdapter.ProviderAdapterV2TurnInput;
2066 readonly nativeTurnId: string;
2067 readonly startedAt: DateTime.Utc;
2068 readonly waitForNativeStart?: boolean;
2069 }) =>
2070 Effect.gen(function* () {
2071 const existing = (yield* Ref.get(activeTurns)).get(input.nativeTurnId);
2072 if (existing !== undefined) {
2073 return existing;
2074 }
2075 const providerTurnId = idAllocator.derive.providerTurn({
2076 driver: CODEX_PROVIDER,
2077 nativeTurnId: input.nativeTurnId,
2078 });
2079 const context: ActiveCodexTurnContext = {
2080 ...(input.waitForNativeStart
2081 ? { nativeStartReady: yield* Deferred.make<void>() }
2082 : {}),
2083 input: input.turnInput,
2084 projectionAppThread: input.turnInput.appThread,
2085 projectionThreadId: input.turnInput.threadId,
2086 projectionRunId: input.turnInput.runId,
2087 nativeTurnId: input.nativeTurnId,
2088 providerThread: input.turnInput.providerThread,
2089 providerTurnId,
2090 providerTurnOrdinal: input.turnInput.providerTurnOrdinal,
2091 providerNodeId: input.turnInput.rootNodeId,
2092 providerNodeKind: "root_turn",
2093 providerNodeStartedAt: input.startedAt,
2094 itemParentNodeId: input.turnInput.rootNodeId,
2095 rootNodeId: input.turnInput.rootNodeId,
2096 subagent: null,
2097 startedAt: input.startedAt,
2098 itemPositions: new Map(),
2099 };
2100 yield* Ref.update(limitedTurnItems, (current) => {
2101 const next = new Map(current);
2102 next.delete(context.providerThread.id);
2103 return next;
2104 });
2105 beginTurnTokenUsage(context);
2106 endSessionApprovalsOnModeChange(context);
2107 yield* Ref.update(activeTurns, (current) => {
2108 const updated = new Map(current);
2109 updated.set(input.nativeTurnId, context);
2110 return updated;
2111 });
2112 yield* emitProviderEvent({
2113 type: "provider_turn.updated",
2114 driver: CODEX_PROVIDER,
2115 threadId: input.turnInput.threadId,
2116 providerTurn: {
2117 id: providerTurnId,
2118 providerThreadId: input.turnInput.providerThread.id,
2119 nodeId: input.turnInput.rootNodeId,
2120 runAttemptId: input.turnInput.attemptId,
2121 nativeTurnRef: {
2122 driver: CODEX_PROVIDER,
2123 nativeId: input.nativeTurnId,
2124 strength: "strong",
2125 },
2126 ordinal: input.turnInput.providerTurnOrdinal,
2127 status: "running",
2128 startedAt: input.startedAt,
2129 completedAt: null,
2130 },
2131 });
2132 yield* rememberRootProviderThread(input.turnInput.providerThread);
2133 return context;
2134 });
2135
2136 const findActiveTurnByNativeThreadId = (nativeThreadId: string) =>
2137 Effect.gen(function* () {
2138 const turns = Array.from((yield* Ref.get(activeTurns)).values());
2139 return turns.find(
2140 (context) => context.providerThread.nativeThreadRef?.nativeId === nativeThreadId,
2141 );
2142 });
2143
2144 const awaitActiveTurn = (
2145 nativeTurnId: string,
2146 attemptsRemaining = 1_000,
2147 ): Effect.Effect<ActiveCodexTurnContext | undefined> =>
2148 Effect.gen(function* () {
2149 const context = (yield* Ref.get(activeTurns)).get(nativeTurnId);
2150 if (context !== undefined || attemptsRemaining <= 0) {
2151 return context;
2152 }
2153 yield* Effect.yieldNow;
2154 return yield* awaitActiveTurn(nativeTurnId, attemptsRemaining - 1);
2155 });
2156
2157 /**
2158 * Like awaitActiveTurn, but item lifecycle events for a turn that
2159 * already settled (background command completions) resolve the
2160 * retained settled context instead of dropping.
2161 */
2162 const resolveItemEventContext = (nativeTurnId: string) =>
2163 Effect.gen(function* () {
2164 const settled = (yield* Ref.get(settledTurns)).get(nativeTurnId);
2165 if (settled !== undefined) {
2166 return { context: settled, settled: true } as const;
2167 }
2168 const context = yield* awaitActiveTurn(nativeTurnId);
2169 return context === undefined ? undefined : ({ context, settled: false } as const);
2170 });
2171
2172 const trackRunningCommandItem = (nativeTurnId: string, item: TrackedRunningCommandItem) =>
2173 Ref.update(runningCommandItemsByTurn, (current) => {
2174 const updated = new Map(current);
2175 const items = new Map(updated.get(nativeTurnId) ?? []);
2176 items.set(item.id, item);
2177 updated.set(nativeTurnId, items);
2178 return updated;
2179 });
2180
2181 /** Returns true when the turn has no running command items left. */
2182 const clearRunningCommandItem = (nativeTurnId: string, nativeItemId: string) =>
2183 Ref.modify(runningCommandItemsByTurn, (current) => {
2184 const items = current.get(nativeTurnId);
2185 if (items === undefined || !items.has(nativeItemId)) {
2186 return [items === undefined || items.size === 0, current] as const;
2187 }
2188 const remaining = new Map(items);
2189 remaining.delete(nativeItemId);
2190 const updated = new Map(current);
2191 if (remaining.size === 0) {
2192 updated.delete(nativeTurnId);
2193 } else {
2194 updated.set(nativeTurnId, remaining);
2195 }
2196 return [remaining.size === 0, updated] as const;
2197 });
2198
2199 // MCP app captures still reading their resource, by native turn and
2200 // item. Each counts as retained background work, so the turn's context
2201 // and the run's ingestion stay open until its app lands on the item.
2202 // A turn that ends badly cancels its captures and settles their items
2203 // itself (terminalizeMcpAppCaptures).
2204 interface PendingMcpAppCapture {
2205 readonly item: Extract<CodexDynamicToolItem, { readonly type: "mcpToolCall" }>;
2206 readonly fiber: Fiber.Fiber<void>;
2207 }
2208 const pendingMcpAppCaptures = yield* Ref.make(
2209 new Map<string, ReadonlyMap<string, PendingMcpAppCapture>>(),
2210 );
2211 /** Removes a capture; true when this call removed it (each settles once). */
2212 const takePendingMcpAppCapture = (nativeTurnId: string, nativeItemId: string) =>
2213 Ref.modify(pendingMcpAppCaptures, (current) => {
2214 const captures = current.get(nativeTurnId);
2215 if (captures === undefined || !captures.has(nativeItemId)) {
2216 return [false, current] as const;
2217 }
2218 const remaining = new Map(captures);
2219 remaining.delete(nativeItemId);
2220 const updated = new Map(current);
2221 if (remaining.size === 0) updated.delete(nativeTurnId);
2222 else updated.set(nativeTurnId, remaining);
2223 return [true, updated] as const;
2224 });
2225
2226 const trackRunningDynamicTool = (nativeTurnId: string, item: CodexDynamicToolItem) =>
2227 Ref.update(runningDynamicToolsByTurn, (current) => {
2228 const updated = new Map(current);
2229 const items = new Map(updated.get(nativeTurnId) ?? []);
2230 items.set(item.id, item);
2231 updated.set(nativeTurnId, items);
2232 return updated;
2233 });
2234
2235 const clearRunningDynamicTool = (nativeTurnId: string, nativeItemId: string) =>
2236 Ref.update(runningDynamicToolsByTurn, (current) => {
2237 const items = current.get(nativeTurnId);
2238 if (items === undefined || !items.has(nativeItemId)) {
2239 return current;
2240 }
2241 const remaining = new Map(items);
2242 remaining.delete(nativeItemId);
2243 const updated = new Map(current);
2244 if (remaining.size === 0) {
2245 updated.delete(nativeTurnId);
2246 } else {
2247 updated.set(nativeTurnId, remaining);
2248 }
2249 return updated;
2250 });
2251
2252 const turnHasRetainedBackgroundWork = (nativeTurnId: string) =>
2253 Effect.gen(function* () {
2254 const commands = (yield* Ref.get(runningCommandItemsByTurn)).get(nativeTurnId);
2255 if (commands !== undefined && commands.size > 0) {
2256 return true;
2257 }
2258 if ((yield* Ref.get(pendingMcpAppCaptures)).has(nativeTurnId)) {
2259 return true;
2260 }
2261 const tools = (yield* Ref.get(runningDynamicToolsByTurn)).get(nativeTurnId);
2262 if (tools === undefined) {
2263 return false;
2264 }
2265 for (const item of tools.values()) {
2266 if (isPersistentCodexDynamicTool(item)) {
2267 return true;
2268 }
2269 }
2270 return false;
2271 });
2272
2273 const releaseSettledTurnIfIdle = (nativeTurnId: string) =>
2274 Effect.gen(function* () {
2275 if (yield* turnHasRetainedBackgroundWork(nativeTurnId)) {
2276 return;
2277 }
2278 yield* Ref.update(settledTurns, (current) => {
2279 if (!current.has(nativeTurnId)) {
2280 return current;
2281 }
2282 const updated = new Map(current);
2283 updated.delete(nativeTurnId);
2284 return updated;
2285 });
2286 yield* Ref.update(offeredContinuationItemsByTurn, (current) => {
2287 if (!current.has(nativeTurnId)) {
2288 return current;
2289 }
2290 const updated = new Map(current);
2291 updated.delete(nativeTurnId);
2292 return updated;
2293 });
2294 yield* Ref.update(completedFinalAnswerTextsByTurn, (current) => {
2295 if (!current.has(nativeTurnId)) {
2296 return current;
2297 }
2298 const updated = new Map(current);
2299 updated.delete(nativeTurnId);
2300 return updated;
2301 });
2302 yield* Ref.update(finalAnswerItemIdsByTurn, (current) => {
2303 if (!current.has(nativeTurnId)) {
2304 return current;
2305 }
2306 const updated = new Map(current);
2307 updated.delete(nativeTurnId);
2308 return updated;
2309 });
2310 });
2311
2312 /**
2313 * When a turn is interrupted or failed, Codex often leaves commandExecution
2314 * items mid-flight (no item/completed). Emit terminal turn items before
2315 * turn.terminal so the projection never keeps a forever-running command card.
2316 * Does not retain settled context: late completions must not wake the run.
2317 */
2318 const terminalizeRunningCommandItems = (
2319 context: ActiveCodexTurnContext,
2320 nativeTurnId: string,
2321 status: "interrupted" | "failed",
2322 completedAt: DateTime.Utc,
2323 ) =>
2324 Effect.gen(function* () {
2325 const items = (yield* Ref.get(runningCommandItemsByTurn)).get(nativeTurnId);
2326 if (items === undefined || items.size === 0) {
2327 return;
2328 }
2329 for (const tracked of items.values()) {
2330 const nodeId = idAllocator.derive.nodeFromProviderItem({
2331 driver: CODEX_PROVIDER,
2332 nativeItemId: tracked.id,
2333 });
2334 const turnItemId = idAllocator.derive.turnItemFromProviderItem({
2335 driver: CODEX_PROVIDER,
2336 nativeItemId: tracked.id,
2337 });
2338 const { ordinal, startedAt } = yield* resolveItemPosition(context, tracked.id);
2339 const node: OrchestrationV2ExecutionNode = {
2340 id: nodeId,
2341 threadId: context.projectionThreadId,
2342 runId: context.projectionRunId,
2343 parentNodeId: context.itemParentNodeId,
2344 rootNodeId: context.rootNodeId,
2345 kind: "tool_call",
2346 status,
2347 countsForRun: false,
2348 providerThreadId: context.providerThread.id,
2349 providerTurnId: context.providerTurnId,
2350 nativeItemRef: codexNativeItemRef(tracked.id),
2351 runtimeRequestId: null,
2352 checkpointScopeId: null,
2353 startedAt,
2354 completedAt,
2355 };
2356 const turnItem: OrchestrationV2TurnItem = {
2357 id: turnItemId,
2358 threadId: context.projectionThreadId,
2359 runId: context.projectionRunId,
2360 nodeId,
2361 providerThreadId: context.providerThread.id,
2362 providerTurnId: context.providerTurnId,
2363 nativeItemRef: codexNativeItemRef(tracked.id),
2364 parentItemId: null,
2365 ordinal,
2366 status,
2367 title: null,
2368 startedAt,
2369 completedAt,
2370 updatedAt: completedAt,
2371 type: "command_execution",
2372 input: tracked.command,
2373 ...(tracked.aggregatedOutput === undefined
2374 ? {}
2375 : { output: tracked.aggregatedOutput }),
2376 };
2377 yield* emitProviderEvent({
2378 type: "node.updated",
2379 driver: CODEX_PROVIDER,
2380 node,
2381 });
2382 yield* emitProviderEvent({
2383 type: "turn_item.updated",
2384 driver: CODEX_PROVIDER,
2385 turnItem,
2386 });
2387 }
2388 });
2389
2390 const terminateBackgroundTerminal = Effect.fn("CodexAdapterV2.terminateBackgroundTerminal")(
2391 function* (nativeThreadId: string, processId: string) {
2392 const response = yield* client.raw
2393 .request("thread/backgroundTerminals/terminate", {
2394 threadId: nativeThreadId,
2395 processId,
2396 })
2397 .pipe(
2398 // The app-server that ran the terminal is gone, and with it
2399 // the only handle to the terminal: nothing is left to stop.
2400 Effect.catchTags({
2401 CodexAppServerProcessExitedError: () => Effect.succeed({ terminated: true }),
2402 CodexAppServerInputStreamEndedError: () => Effect.succeed({ terminated: true }),
2403 // The thread is unloaded, as Codex does a minute after a
2404 // settle or archive unsubscribes it. Unloading kills the
2405 // thread's terminals, so nothing is left to stop.
2406 CodexAppServerRequestError: (error) =>
2407 error.code === -32600 && error.errorMessage.startsWith("thread not found:")
2408 ? Effect.succeed({ terminated: true })
2409 : Effect.fail(error),
2410 }),
2411 );
2412 const result = yield* decodeCodexBackgroundTerminalTerminateResponse(response);
2413 if (result.terminated) return;
2414 let cursor: string | null = null;
2415 while (true) {
2416 const response: unknown = yield* client.raw.request(
2417 "thread/backgroundTerminals/list",
2418 {
2419 threadId: nativeThreadId,
2420 ...(cursor === null ? {} : { cursor }),
2421 },
2422 );
2423 const page: CodexBackgroundTerminalsListPage =
2424 yield* decodeCodexBackgroundTerminalsListResponse(response);
2425 if (page.data.some((terminal) => terminal.processId === processId)) {
2426 return yield* toProtocolError(
2427 `Codex background terminal ${processId} remained active after termination.`,
2428 );
2429 }
2430 if (page.nextCursor === null) return;
2431 cursor = page.nextCursor;
2432 }
2433 },
2434 Effect.timeout("10 seconds"),
2435 );
2436
2437 /**
2438 * Yielded exec cells can start MCP / dynamic tools that never receive
2439 * item/completed when functions.wait terminates the cell. Terminalize
2440 * leftover nonpersistent tools before turn.terminal. Persistent tools
2441 * can outlive the root turn and must stay running.
2442 */
2443 const terminalizeRunningDynamicTools = (
2444 context: ActiveCodexTurnContext,
2445 nativeTurnId: string,
2446 status: "cancelled" | "interrupted" | "failed",
2447 completedAt: DateTime.Utc,
2448 includePersistent: boolean,
2449 ) =>
2450 Effect.gen(function* () {
2451 const items = (yield* Ref.get(runningDynamicToolsByTurn)).get(nativeTurnId);
2452 if (items === undefined || items.size === 0) {
2453 return;
2454 }
2455 for (const tracked of items.values()) {
2456 if (!includePersistent && isPersistentCodexDynamicTool(tracked)) {
2457 continue;
2458 }
2459 const artifacts = yield* buildDynamicToolArtifacts(context, tracked);
2460 yield* emitProviderEvent({
2461 type: "node.updated",
2462 driver: CODEX_PROVIDER,
2463 node: {
2464 ...artifacts.node,
2465 status,
2466 completedAt,
2467 },
2468 });
2469 yield* emitProviderEvent({
2470 type: "turn_item.updated",
2471 driver: CODEX_PROVIDER,
2472 turnItem: {
2473 ...artifacts.turnItem,
2474 status,
2475 completedAt,
2476 updatedAt: completedAt,
2477 },
2478 });
2479 }
2480 yield* Ref.update(runningDynamicToolsByTurn, (current) => {
2481 if (!current.has(nativeTurnId)) {
2482 return current;
2483 }
2484 const remaining = new Map(current.get(nativeTurnId) ?? []);
2485 for (const tracked of remaining.values()) {
2486 if (includePersistent || !isPersistentCodexDynamicTool(tracked)) {
2487 remaining.delete(tracked.id);
2488 }
2489 }
2490 const updated = new Map(current);
2491 if (remaining.size === 0) {
2492 updated.delete(nativeTurnId);
2493 } else {
2494 updated.set(nativeTurnId, remaining);
2495 }
2496 return updated;
2497 });
2498 });
2499
2500 /**
2501 * Allocates an item's ordinal and start time on first sight. `item/started` seeds
2502 * Codex's own start time; items that stream without it start when first seen.
2503 */
2504 const resolveItemPosition = (
2505 context: ActiveCodexTurnContext,
2506 nativeItemId: string,
2507 startedAt?: DateTime.Utc,
2508 ) =>
2509 Effect.gen(function* () {
2510 // Read the clock first so the lookup and the insert run without yielding.
2511 const now = startedAt ?? (yield* DateTime.now);
2512 const existing = context.itemPositions.get(nativeItemId);
2513 if (existing !== undefined) {
2514 return existing;
2515 }
2516 const position = {
2517 ordinal: context.providerTurnOrdinal * 100 + context.itemPositions.size + 1,
2518 startedAt: now,
2519 };
2520 context.itemPositions.set(nativeItemId, position);
2521 return position;
2522 });
2523
2524 const resolveItemOrdinal = (context: ActiveCodexTurnContext, nativeItemId: string) =>
2525 resolveItemPosition(context, nativeItemId).pipe(
2526 Effect.map((position) => position.ordinal),
2527 );
2528
2529 const nextProviderTurnOrdinal = (
2530 providerThreadId: OrchestrationV2ProviderThread["id"],
2531 minimum: number,
2532 ) =>
2533 Ref.modify(nextProviderTurnOrdinals, (current) => {
2534 const previous = current.get(String(providerThreadId));
2535 const next = previous === undefined ? minimum : Math.max(previous + 1, minimum);
2536 const updated = new Map(current);
2537 updated.set(String(providerThreadId), next);
2538 return [next, updated];
2539 });
2540
2541 const emitSubagentTaskUpdate = (input: {
2542 readonly subagent: CodexSubagentThreadContext;
2543 readonly status: OrchestrationV2Subagent["status"];
2544 readonly result?: string | null;
2545 readonly completedAt?: DateTime.Utc | null;
2546 readonly startedAt?: DateTime.Utc;
2547 readonly reopen?: boolean;
2548 }) =>
2549 Effect.gen(function* () {
2550 const now = yield* DateTime.now;
2551 const prior = input.subagent.task;
2552 const settled = !isOrchestrationV2WorkActive(prior.status);
2553 // Snapshots can arrive after completion. Only an actual new turn
2554 // may reopen the reusable child identity.
2555 if (settled && isOrchestrationV2WorkActive(input.status) && !input.reopen) return;
2556 const status = settled && !input.reopen ? prior.status : input.status;
2557 const completedAt = isOrchestrationV2WorkActive(status)
2558 ? null
2559 : (prior.completedAt ?? input.completedAt ?? now);
2560 const task = {
2561 ...prior,
2562 status,
2563 ...(input.reopen
2564 ? {
2565 runId: input.subagent.parentContext.projectionRunId,
2566 parentNodeId: input.subagent.parentContext.itemParentNodeId,
2567 }
2568 : {}),
2569 startedAt: input.startedAt ?? prior.startedAt,
2570 result: input.result === undefined ? input.subagent.task.result : input.result,
2571 completedAt,
2572 updatedAt: now,
2573 } satisfies OrchestrationV2Subagent;
2574 input.subagent.task = task;
2575 if (input.startedAt !== undefined) input.subagent.startedAt = input.startedAt;
2576
2577 yield* emitProviderEvent({
2578 type: "subagent.updated",
2579 driver: CODEX_PROVIDER,
2580 subagent: task,
2581 });
2582 yield* emitProviderEvent({
2583 type: "turn_item.updated",
2584 driver: CODEX_PROVIDER,
2585 turnItem: {
2586 id: input.subagent.turnItemId,
2587 threadId: task.threadId,
2588 runId: task.runId,
2589 nodeId: task.id,
2590 providerThreadId: task.providerThreadId,
2591 providerTurnId: input.subagent.parentContext.providerTurnId,
2592 nativeItemRef: task.nativeTaskRef,
2593 parentItemId: null,
2594 ordinal: input.subagent.turnItemOrdinal,
2595 status: task.status,
2596 title: task.title,
2597 startedAt: task.startedAt,
2598 completedAt: task.completedAt,
2599 updatedAt: task.updatedAt,
2600 type: "subagent",
2601 subagentId: task.id,
2602 origin: task.origin,
2603 driver: task.driver,
2604 providerInstanceId: task.providerInstanceId,
2605 childThreadId: task.childThreadId,
2606 prompt: task.prompt,
2607 result: task.result,
2608 },
2609 });
2610 });
2611
2612 const emitSubagentProviderTurnStarted = (
2613 subagent: CodexSubagentThreadContext,
2614 turn: PendingCodexSubagentTurnStarted,
2615 ) =>
2616 Effect.gen(function* () {
2617 if (subagent.nativeTurnIds.has(turn.nativeTurnId)) return;
2618 subagent.nativeTurnIds.add(turn.nativeTurnId);
2619 const parentNativeThreadId = yield* getNativeThreadId(
2620 subagent.parentContext.providerThread,
2621 );
2622 const currentParent = yield* findActiveTurnByNativeThreadId(parentNativeThreadId);
2623 if (currentParent !== undefined) subagent.parentContext = currentParent;
2624 const terminalizedNativeTurns = yield* Ref.get(terminalizedNonCompletedNativeTurns);
2625 let ancestor: ActiveCodexTurnContext | undefined = subagent.parentContext;
2626 while (ancestor !== undefined) {
2627 if (terminalizedNativeTurns.has(ancestor.nativeTurnId)) {
2628 const nativeThreadId = yield* getNativeThreadId(subagent.providerThread);
2629 const interrupted = yield* client
2630 .request("turn/interrupt", {
2631 threadId: nativeThreadId,
2632 turnId: turn.nativeTurnId,
2633 })
2634 .pipe(
2635 Effect.as(true),
2636 Effect.catch((cause) =>
2637 Effect.logWarning("orchestration-v2.codex-late-subagent-interrupt-failed", {
2638 nativeThreadId,
2639 nativeTurnId: turn.nativeTurnId,
2640 cause,
2641 }).pipe(Effect.as(false)),
2642 ),
2643 Effect.timeoutOption("10 seconds"),
2644 );
2645 if (Option.isNone(interrupted)) {
2646 yield* Effect.logWarning(
2647 "orchestration-v2.codex-late-subagent-interrupt-timeout",
2648 {
2649 nativeThreadId,
2650 nativeTurnId: turn.nativeTurnId,
2651 },
2652 );
2653 }
2654 return;
2655 }
2656 ancestor = ancestor.subagent?.parentContext;
2657 }
2658 const providerTurnId = idAllocator.derive.providerTurn({
2659 driver: CODEX_PROVIDER,
2660 nativeTurnId: turn.nativeTurnId,
2661 });
2662 const providerTurnOrdinal = yield* nextProviderTurnOrdinal(
2663 subagent.providerThread.id,
2664 1,
2665 );
2666 const providerNodeId =
2667 providerTurnOrdinal === 1
2668 ? subagent.childRootNodeId
2669 : idAllocator.derive.nodeFromProviderItem({
2670 driver: CODEX_PROVIDER,
2671 nativeItemId: `${turn.nativeTurnId}:thread-root`,
2672 });
2673 const activeContext: ActiveCodexTurnContext = {
2674 input: subagent.parentContext.input,
2675 projectionAppThread: subagent.childThread,
2676 projectionThreadId: subagent.childThreadId,
2677 projectionRunId: null,
2678 nativeTurnId: turn.nativeTurnId,
2679 providerThread: subagent.providerThread,
2680 providerTurnId,
2681 providerTurnOrdinal,
2682 providerNodeId,
2683 providerNodeKind: "root_turn",
2684 providerNodeStartedAt: turn.startedAt,
2685 itemParentNodeId: providerNodeId,
2686 rootNodeId: providerNodeId,
2687 subagent,
2688 startedAt: turn.startedAt,
2689 itemPositions: new Map(),
2690 };
2691 beginTurnTokenUsage(activeContext);
2692 endSessionApprovalsOnModeChange(activeContext);
2693 yield* Ref.update(activeTurns, (current) => {
2694 const updated = new Map(current);
2695 updated.set(turn.nativeTurnId, activeContext);
2696 return updated;
2697 });
2698 yield* emitSubagentTaskUpdate({
2699 subagent,
2700 status: "running",
2701 startedAt: turn.startedAt,
2702 ...(providerTurnOrdinal > 1 ? { reopen: true, result: null } : {}),
2703 });
2704 const now = yield* DateTime.now;
2705 yield* emitProviderEvent({
2706 type: "provider_thread.updated",
2707 driver: CODEX_PROVIDER,
2708 providerThread: {
2709 ...subagent.providerThread,
2710 status: "active",
2711 updatedAt: now,
2712 },
2713 });
2714 yield* emitProviderEvent({
2715 type: "provider_turn.updated",
2716 driver: CODEX_PROVIDER,
2717 threadId: subagent.childThreadId,
2718 providerTurn: {
2719 id: providerTurnId,
2720 providerThreadId: subagent.providerThread.id,
2721 nodeId: providerNodeId,
2722 runAttemptId: null,
2723 nativeTurnRef: {
2724 driver: CODEX_PROVIDER,
2725 nativeId: turn.nativeTurnId,
2726 strength: "strong",
2727 },
2728 ordinal: activeContext.providerTurnOrdinal,
2729 status: "running",
2730 startedAt: turn.startedAt,
2731 completedAt: null,
2732 },
2733 });
2734 yield* emitProviderEvent({
2735 type: "node.updated",
2736 driver: CODEX_PROVIDER,
2737 node: {
2738 id: providerNodeId,
2739 threadId: subagent.childThreadId,
2740 runId: null,
2741 parentNodeId: null,
2742 rootNodeId: providerNodeId,
2743 kind: "root_turn",
2744 status: "running",
2745 countsForRun: false,
2746 providerThreadId: subagent.providerThread.id,
2747 providerTurnId,
2748 nativeItemRef: subagent.task.nativeTaskRef,
2749 runtimeRequestId: null,
2750 checkpointScopeId: null,
2751 startedAt: turn.startedAt,
2752 completedAt: null,
2753 },
2754 });
2755 });
2756
2757 const rememberSubagentTurnStarted = (input: {
2758 readonly nativeThreadId: string;
2759 readonly nativeTurnId: string;
2760 readonly startedAt: DateTime.Utc;
2761 }) =>
2762 Effect.gen(function* () {
2763 const subagent = (yield* Ref.get(subagentThreads)).get(input.nativeThreadId);
2764 if (subagent !== undefined) {
2765 yield* emitSubagentProviderTurnStarted(subagent, input);
2766 return;
2767 }
2768 yield* Ref.update(pendingSubagentTurns, (current) => {
2769 const updated = new Map(current);
2770 updated.set(input.nativeThreadId, [
2771 ...(updated.get(input.nativeThreadId) ?? []),
2772 { nativeTurnId: input.nativeTurnId, startedAt: input.startedAt },
2773 ]);
2774 return updated;
2775 });
2776 });
2777
2778 const updateSubagentSelection = Effect.fnUntraced(function* (
2779 nativeThreadId: string,
2780 value: string | null,
2781 effort?: string | null,
2782 tier?: string | null,
2783 ) {
2784 const model = value?.trim();
2785 if (!model) return;
2786 const previous = subagentSelections.get(nativeThreadId);
2787 const previousOptions = previous?.options;
2788 const options =
2789 effort === undefined && tier === undefined
2790 ? previousOptions
2791 : [
2792 ...(previousOptions ?? []).filter(({ id }) =>
2793 id === "reasoningEffort"
2794 ? effort === undefined
2795 : id === "serviceTier" && tier === undefined,
2796 ),
2797 ...[
2798 { id: "reasoningEffort", value: effort },
2799 { id: "serviceTier", value: tier },
2800 ].flatMap(({ id, value }) =>
2801 value?.trim() ? [{ id, value: value.trim() }] : [],
2802 ),
2803 ];
2804 const selection = {
2805 model,
2806 ...(options === undefined ? {} : { options }),
2807 };
2808 subagentSelections.set(nativeThreadId, selection);
2809 const subagent = (yield* Ref.get(subagentThreads)).get(nativeThreadId);
2810 if (subagent === undefined) return;
2811 const modelSelection = { instanceId: subagent.task.providerInstanceId, ...selection };
2812 if (
2813 subagent.task.modelSelection &&
2814 modelSelectionsEqual(subagent.task.modelSelection, modelSelection)
2815 )
2816 return;
2817 subagent.task = {
2818 ...subagent.task,
2819 model,
2820 modelSelection,
2821 updatedAt: yield* DateTime.now,
2822 };
2823 yield* emitProviderEvent({
2824 type: "subagent.updated",
2825 driver: CODEX_PROVIDER,
2826 subagent: subagent.task,
2827 });
2828 });
2829
2830 const registerSubagentThread = (input: {
2831 readonly context: ActiveCodexTurnContext;
2832 readonly nativeThreadId: string;
2833 readonly nativeItemId: string;
2834 readonly nativeToolCallId: string;
2835 readonly prompt: string;
2836 readonly title: string | null;
2837 readonly model: string | null;
2838 readonly reasoningEffort?: string | null | undefined;
2839 readonly ordinal: number;
2840 readonly emitInitialPrompt: boolean;
2841 }) =>
2842 Effect.gen(function* () {
2843 const registeredSubagents = yield* Ref.get(subagentThreads);
2844 if (registeredSubagents.has(input.nativeThreadId)) {
2845 return;
2846 }
2847 if (input.model && !subagentSelections.has(input.nativeThreadId)) {
2848 yield* updateSubagentSelection(
2849 input.nativeThreadId,
2850 input.model,
2851 input.reasoningEffort,
2852 );
2853 }
2854
2855 const now = yield* DateTime.now;
2856 const subagentNodeId = idAllocator.derive.nodeFromProviderItem({
2857 driver: CODEX_PROVIDER,
2858 nativeItemId: input.nativeItemId,
2859 });
2860 const childRootNodeId = idAllocator.derive.nodeFromProviderItem({
2861 driver: CODEX_PROVIDER,
2862 nativeItemId: `${input.nativeItemId}:thread-root`,
2863 });
2864 const childThreadId = idAllocator.derive.threadFromProviderThread({
2865 driver: CODEX_PROVIDER,
2866 nativeThreadId: input.nativeThreadId,
2867 });
2868 const turnItemOrdinal = yield* resolveItemOrdinal(input.context, input.nativeItemId);
2869 const providerThread = {
2870 id: idAllocator.derive.providerThread({
2871 driver: CODEX_PROVIDER,
2872 nativeThreadId: input.nativeThreadId,
2873 }),
2874 driver: CODEX_PROVIDER,
2875 providerInstanceId: input.context.input.modelSelection.instanceId,
2876 providerSessionId: input.context.providerThread.providerSessionId,
2877 appThreadId: childThreadId,
2878 ownerNodeId: null,
2879 nativeThreadRef: {
2880 driver: CODEX_PROVIDER,
2881 nativeId: input.nativeThreadId,
2882 strength: "strong" as const,
2883 },
2884 nativeConversationHeadRef: null,
2885 status: "idle" as const,
2886 firstRunOrdinal: null,
2887 lastRunOrdinal: null,
2888 handoffIds: [],
2889 forkedFrom: {
2890 providerThreadId: input.context.providerThread.id,
2891 providerTurnId: input.context.providerTurnId,
2892 },
2893 createdAt: now,
2894 updatedAt: now,
2895 } satisfies OrchestrationV2ProviderThread;
2896 const reportedSelection = subagentSelections.get(input.nativeThreadId);
2897 const task = {
2898 id: subagentNodeId,
2899 threadId: input.context.projectionThreadId,
2900 runId: input.context.projectionRunId,
2901 parentNodeId: input.context.itemParentNodeId,
2902 origin: "provider_native",
2903 createdBy: "agent",
2904 driver: CODEX_PROVIDER,
2905 providerInstanceId: input.context.input.modelSelection.instanceId,
2906 providerThreadId: providerThread.id,
2907 childThreadId,
2908 nativeTaskRef: codexNativeItemRef(input.nativeItemId),
2909 prompt: input.prompt,
2910 title: input.title,
2911 model: reportedSelection?.model ?? input.model,
2912 modelSelection: reportedSelection
2913 ? {
2914 instanceId: input.context.input.modelSelection.instanceId,
2915 ...reportedSelection,
2916 }
2917 : undefined,
2918 status: "running",
2919 result: null,
2920 startedAt: now,
2921 completedAt: null,
2922 updatedAt: now,
2923 } satisfies OrchestrationV2Subagent;
2924 const childThread = makeSubagentChildThread({
2925 parentThread: input.context.projectionAppThread,
2926 childThreadId,
2927 parentNodeId: subagentNodeId,
2928 activeProviderThreadId: providerThread.id,
2929 providerInstanceId: input.context.input.modelSelection.instanceId,
2930 modelSelection: {
2931 ...input.context.input.modelSelection,
2932 model: task.model ?? input.context.input.modelSelection.model,
2933 },
2934 title: subagentThreadTitle({
2935 parentTitle: input.context.projectionAppThread.title,
2936 prompt: task.prompt,
2937 title: task.title,
2938 ordinal: input.ordinal,
2939 }),
2940 now,
2941 createdBy: "agent",
2942 creationSource: "provider",
2943 });
2944 const subagent = {
2945 parentContext: input.context,
2946 providerThread,
2947 childThread,
2948 subagentNodeId,
2949 childRootNodeId,
2950 childThreadId,
2951 nativeToolCallId: input.nativeToolCallId,
2952 ordinal: input.ordinal,
2953 nativeTurnIds: new Set<string>(),
2954 startedAt: now,
2955 turnItemId: idAllocator.derive.turnItemFromProviderItem({
2956 driver: CODEX_PROVIDER,
2957 nativeItemId: input.nativeItemId,
2958 }),
2959 turnItemOrdinal,
2960 task,
2961 } satisfies CodexSubagentThreadContext;
2962
2963 yield* Ref.update(subagentThreads, (current) => {
2964 const updated = new Map(current);
2965 updated.set(input.nativeThreadId, subagent);
2966 return updated;
2967 });
2968 yield* emitProviderEvent({
2969 type: "app_thread.created",
2970 driver: CODEX_PROVIDER,
2971 appThread: childThread,
2972 });
2973 yield* emitProviderEvent({
2974 type: "node.updated",
2975 driver: CODEX_PROVIDER,
2976 node: {
2977 id: subagentNodeId,
2978 threadId: input.context.projectionThreadId,
2979 runId: input.context.projectionRunId,
2980 parentNodeId: input.context.itemParentNodeId,
2981 rootNodeId: input.context.rootNodeId,
2982 kind: "subagent",
2983 status: "running",
2984 countsForRun: false,
2985 providerThreadId: providerThread.id,
2986 providerTurnId: input.context.providerTurnId,
2987 nativeItemRef: codexNativeItemRef(input.nativeToolCallId),
2988 runtimeRequestId: null,
2989 checkpointScopeId: null,
2990 startedAt: now,
2991 completedAt: null,
2992 },
2993 });
2994 yield* emitProviderEvent({
2995 type: "node.updated",
2996 driver: CODEX_PROVIDER,
2997 node: {
2998 id: childRootNodeId,
2999 threadId: childThreadId,
3000 runId: null,
3001 parentNodeId: null,
3002 rootNodeId: childRootNodeId,
3003 kind: "root_turn",
3004 status: "running",
3005 countsForRun: false,
3006 providerThreadId: providerThread.id,
3007 providerTurnId: null,
3008 nativeItemRef: codexNativeItemRef(input.nativeItemId),
3009 runtimeRequestId: null,
3010 checkpointScopeId: null,
3011 startedAt: now,
3012 completedAt: null,
3013 },
3014 });
3015 yield* emitProviderEvent({
3016 type: "provider_thread.updated",
3017 driver: CODEX_PROVIDER,
3018 providerThread,
3019 });
3020 if (input.emitInitialPrompt && input.prompt.length > 0) {
3021 const promptNativeItemId = `${input.nativeItemId}:prompt`;
3022 const promptArtifacts = makeSubagentConversationArtifacts({
3023 senderThreadId: input.context.projectionThreadId,
3024 messageId: idAllocator.derive.messageFromProviderItem({
3025 driver: CODEX_PROVIDER,
3026 nativeItemId: promptNativeItemId,
3027 }),
3028 turnItemId: idAllocator.derive.turnItemFromProviderItem({
3029 driver: CODEX_PROVIDER,
3030 nativeItemId: promptNativeItemId,
3031 }),
3032 threadId: childThreadId,
3033 rootNodeId: childRootNodeId,
3034 providerThreadId: providerThread.id,
3035 providerTurnId: null,
3036 nativeItemRef: codexNativeItemRef(promptNativeItemId),
3037 role: "user",
3038 text: input.prompt,
3039 ordinal: 100,
3040 now,
3041 });
3042 yield* emitProviderEvent({
3043 type: "message.updated",
3044 driver: CODEX_PROVIDER,
3045 message: promptArtifacts.message,
3046 });
3047 yield* emitProviderEvent({
3048 type: "turn_item.updated",
3049 driver: CODEX_PROVIDER,
3050 turnItem: promptArtifacts.turnItem,
3051 });
3052 }
3053 yield* emitSubagentTaskUpdate({
3054 subagent,
3055 status: "running",
3056 });
3057
3058 const pendingTurns = yield* Ref.modify(pendingSubagentTurns, (current) => {
3059 const pending = current.get(input.nativeThreadId) ?? [];
3060 const updated = new Map(current);
3061 updated.delete(input.nativeThreadId);
3062 return [pending, updated];
3063 });
3064 for (const pendingTurn of pendingTurns) {
3065 yield* emitSubagentProviderTurnStarted(subagent, pendingTurn);
3066 }
3067 if (task.model === null) {
3068 yield* client.raw
3069 .request("thread/read", { threadId: input.nativeThreadId, includeTurns: false })
3070 .pipe(
3071 Effect.flatMap(decodeCodexChildThread),
3072 Effect.map((response): typeof CodexChildSelection.Type | null =>
3073 response.thread.id === input.nativeThreadId && response.thread.model?.trim()
3074 ? {
3075 thread: response.thread,
3076 model: response.thread.model,
3077 reasoningEffort: response.thread.reasoningEffort,
3078 }
3079 : null,
3080 ),
3081 Effect.catch(() => Effect.succeed(null)),
3082 Effect.flatMap((response) =>
3083 response === null
3084 ? client.raw
3085 .request("thread/resume", {
3086 threadId: input.nativeThreadId,
3087 excludeTurns: true,
3088 })
3089 .pipe(Effect.flatMap(decodeCodexChildModel))
3090 : Effect.succeed(response),
3091 ),
3092 Effect.timeout("5 seconds"),
3093 Effect.flatMap((response) =>
3094 response.thread.id === input.nativeThreadId &&
3095 !subagentSelections.has(input.nativeThreadId)
3096 ? updateSubagentSelection(
3097 input.nativeThreadId,
3098 response.model,
3099 response.reasoningEffort,
3100 response.serviceTier,
3101 )
3102 : Effect.void,
3103 ),
3104 Effect.catch(() => Effect.void),
3105 Effect.forkIn(scope),
3106 );
3107 }
3108 });
3109
3110 const registerSubagentThreads = (input: {
3111 readonly context: ActiveCodexTurnContext;
3112 readonly item: CodexCollabAgentToolCallItem;
3113 }) =>
3114 Effect.gen(function* () {
3115 if (input.item.tool !== "spawnAgent" || input.item.receiverThreadIds.length === 0) {
3116 return;
3117 }
3118
3119 for (const [index, nativeThreadId] of input.item.receiverThreadIds.entries()) {
3120 const model =
3121 typeof input.item.model === "string" && input.item.model.length > 0
3122 ? input.item.model
3123 : null;
3124 yield* registerSubagentThread({
3125 context: input.context,
3126 nativeThreadId,
3127 nativeItemId: `${input.item.id}:${nativeThreadId}`,
3128 nativeToolCallId: input.item.id,
3129 prompt: input.item.prompt ?? "",
3130 title: null,
3131 model,
3132 reasoningEffort: input.item.reasoningEffort,
3133 ordinal: index + 1,
3134 emitInitialPrompt: true,
3135 });
3136 }
3137 });
3138
3139 const registerSubagentActivity = (input: {
3140 readonly context?: ActiveCodexTurnContext;
3141 readonly item: CodexSubAgentActivityItem;
3142 }) =>
3143 Effect.gen(function* () {
3144 if (input.item.kind === "started") {
3145 const context = input.context;
3146 if (context === undefined) return;
3147 const registeredSubagents = yield* Ref.get(subagentThreads);
3148 const ordinal =
3149 Array.from(registeredSubagents.values()).filter(
3150 (subagent) => subagent.parentContext.rootNodeId === context.rootNodeId,
3151 ).length + 1;
3152 yield* registerSubagentThread({
3153 context,
3154 nativeThreadId: input.item.agentThreadId,
3155 nativeItemId: `${input.item.id}:${input.item.agentThreadId}`,
3156 nativeToolCallId: input.item.id,
3157 prompt: "",
3158 title: input.item.agentPath,
3159 model: null,
3160 ordinal,
3161 emitInitialPrompt: false,
3162 });
3163 return;
3164 }
3165
3166 const subagent = (yield* Ref.get(subagentThreads)).get(input.item.agentThreadId);
3167 if (subagent === undefined) {
3168 return;
3169 }
3170
3171 if (input.item.kind === "interrupted" || input.item.kind === "completed") {
3172 yield* emitSubagentTaskUpdate({
3173 subagent,
3174 status: input.item.kind,
3175 });
3176 }
3177 });
3178
3179 const updateSubagentStates = (input: { readonly item: CodexCollabAgentToolCallItem }) =>
3180 Effect.gen(function* () {
3181 const subagents = yield* Ref.get(subagentThreads);
3182 for (const [nativeThreadId, state] of Object.entries(input.item.agentsStates)) {
3183 const subagent = subagents.get(nativeThreadId);
3184 if (subagent === undefined) {
3185 continue;
3186 }
3187 const statuses = {
3188 pendingInit: "pending",
3189 running: "running",
3190 interrupted: "interrupted",
3191 completed: "completed",
3192 errored: "failed",
3193 shutdown: "cancelled",
3194 notFound: "failed",
3195 } as const satisfies Record<
3196 CodexSchema.ServerNotification__CollabAgentStatus,
3197 OrchestrationV2Subagent["status"]
3198 >;
3199 const status = statuses[state.status];
3200 yield* emitSubagentTaskUpdate({
3201 subagent,
3202 status,
3203 ...(state.message === null ? {} : { result: state.message }),
3204 });
3205 }
3206 });
3207
3208 const resolvePlanId = (context: ActiveCodexTurnContext, planKey: string) =>
3209 Effect.gen(function* () {
3210 const existing = (yield* Ref.get(planIds)).get(planKey);
3211 if (existing !== undefined) {
3212 return existing;
3213 }
3214 const planId = yield* idAllocator.allocate.plan({
3215 threadId: context.projectionThreadId,
3216 ...(context.projectionRunId === null ? {} : { runId: context.projectionRunId }),
3217 driver: CODEX_PROVIDER,
3218 });
3219 yield* Ref.update(planIds, (current) => {
3220 const updated = new Map(current);
3221 updated.set(planKey, planId);
3222 return updated;
3223 });
3224 return planId;
3225 });
3226
3227 const resolveCodexAttachment = (attachment: ChatAttachment) =>
3228 Effect.gen(function* () {
3229 const attachmentPath = resolveAttachmentPath({
3230 attachmentsDir: serverConfig.attachmentsDir,
3231 attachment,
3232 });
3233 if (attachmentPath === null) {
3234 return yield* toProtocolError(`Invalid attachment id '${attachment.id}'`);
3235 }
3236 const bytes = yield* fileSystem
3237 .readFile(attachmentPath)
3238 .pipe(
3239 Effect.mapError((cause) =>
3240 toProtocolError(`Failed to read attachment '${attachment.id}'.`, cause),
3241 ),
3242 );
3243 return {
3244 type: "image" as const,
3245 url: `data:${attachment.mimeType};base64,${Buffer.from(bytes).toString("base64")}`,
3246 } satisfies CodexSchema.V2TurnStartParams__UserInput;
3247 });
3248
3249 const toCodexInput = (
3250 turnInput: Pick<
3251 | ProviderAdapter.ProviderAdapterV2TurnInput
3252 | ProviderAdapter.ProviderAdapterV2SteerInput,
3253 "message"
3254 >,
3255 ) =>
3256 Effect.gen(function* () {
3257 const inputItems: Array<CodexSchema.V2TurnStartParams__UserInput> = [];
3258 const text = providerMessageTextWithAttachmentPaths({
3259 text: codexSkillMentionText(turnInput.message.text),
3260 attachments: turnInput.message.attachments,
3261 resolveAttachmentPath: (attachment) =>
3262 resolveAttachmentPath({ attachmentsDir: serverConfig.attachmentsDir, attachment }),
3263 });
3264 if (text.length > 0) {
3265 inputItems.push({
3266 type: "text",
3267 text,
3268 });
3269 }
3270 const attachmentItems = yield* Effect.forEach(
3271 turnInput.message.attachments.filter(isProviderNativeImageAttachment),
3272 resolveCodexAttachment,
3273 { concurrency: 1 },
3274 );
3275 inputItems.push(...attachmentItems);
3276 if (inputItems.length === 0) {
3277 return yield* toProtocolError("Turn requires non-empty text or attachments.");
3278 }
3279 return inputItems;
3280 });
3281
3282 const buildAgentMessageArtifacts = (
3283 context: ActiveCodexTurnContext,
3284 item: { readonly id: string; readonly text: string },
3285 completed: boolean,
3286 ) =>
3287 Effect.gen(function* () {
3288 const updatedAt = yield* DateTime.now;
3289 const completedAt = completed ? updatedAt : null;
3290 const nodeId = idAllocator.derive.nodeFromProviderItem({
3291 driver: CODEX_PROVIDER,
3292 nativeItemId: item.id,
3293 });
3294 const { ordinal, startedAt } = yield* resolveItemPosition(context, item.id);
3295 const messageId = idAllocator.derive.messageFromProviderItem({
3296 driver: CODEX_PROVIDER,
3297 nativeItemId: item.id,
3298 });
3299 const turnItemId = idAllocator.derive.turnItemFromProviderItem({
3300 driver: CODEX_PROVIDER,
3301 nativeItemId: item.id,
3302 });
3303 const node: OrchestrationV2ExecutionNode = {
3304 id: nodeId,
3305 threadId: context.projectionThreadId,
3306 runId: context.projectionRunId,
3307 parentNodeId: context.itemParentNodeId,
3308 rootNodeId: context.rootNodeId,
3309 kind: "assistant_message",
3310 status: completed ? "completed" : "running",
3311 countsForRun: false,
3312 providerThreadId: context.providerThread.id,
3313 providerTurnId: context.providerTurnId,
3314 nativeItemRef: codexNativeItemRef(item.id),
3315 runtimeRequestId: null,
3316 checkpointScopeId: null,
3317 startedAt,
3318 completedAt,
3319 };
3320 const message: OrchestrationV2ConversationMessage = {
3321 createdBy: "agent",
3322 creationSource: "provider",
3323 id: messageId,
3324 threadId: context.projectionThreadId,
3325 runId: context.projectionRunId,
3326 nodeId,
3327 role: "assistant",
3328 text: item.text,
3329 attachments: [],
3330 streaming: !completed,
3331 createdAt: startedAt,
3332 updatedAt,
3333 };
3334 const turnItem: OrchestrationV2TurnItem = {
3335 id: turnItemId,
3336 threadId: context.projectionThreadId,
3337 runId: context.projectionRunId,
3338 nodeId,
3339 providerThreadId: context.providerThread.id,
3340 providerTurnId: context.providerTurnId,
3341 nativeItemRef: codexNativeItemRef(item.id),
3342 parentItemId: null,
3343 ordinal,
3344 status: completed ? "completed" : "running",
3345 title: null,
3346 startedAt,
3347 completedAt,
3348 updatedAt,
3349 type: "assistant_message",
3350 messageId,
3351 text: item.text,
3352 streaming: !completed,
3353 };
3354 return { node, message, turnItem };
3355 });
3356
3357 // Summary and raw reasoning are separate streams, with independently indexed parts.
3358 // Reuse the text coalescer so token bursts do not create one database write per token.
3359 const reasoningParts = new Map<
3360 string,
3361 { turnId: string; nativeItemId: string; stream: "summary" | "content"; index: number }
3362 >();
3363 const reasoningDeltas = yield* makeProviderTextDeltaCoalescer({
3364 flushIntervalMs: CODEX_ASSISTANT_DELTA_FLUSH_INTERVAL_MS,
3365 emit: (update) =>
3366 Effect.gen(function* () {
3367 const part = reasoningParts.get(update.itemId);
3368 if (part === undefined) return;
3369 const context = yield* awaitActiveTurn(update.turnId);
3370 if (context === undefined) return;
3371 const artifacts = yield* buildAgentMessageArtifacts(
3372 context,
3373 { id: update.itemId, text: update.text },
3374 update.completed,
3375 );
3376 const interrupted = (yield* Ref.get(terminalizedNonCompletedNativeTurns)).has(
3377 update.turnId,
3378 );
3379 const { messageId: _messageId, ...item } = artifacts.turnItem;
3380 const nativeItemRef = codexNativeItemRef(part.nativeItemId);
3381 yield* emitProviderEvent({
3382 type: "node.updated",
3383 driver: CODEX_PROVIDER,
3384 node: {
3385 ...artifacts.node,
3386 kind: "reasoning",
3387 nativeItemRef,
3388 ...(interrupted ? { status: "interrupted" as const } : {}),
3389 },
3390 });
3391 yield* emitProviderEvent({
3392 type: "turn_item.updated",
3393 driver: CODEX_PROVIDER,
3394 turnItem: {
3395 ...item,
3396 type: "reasoning",
3397 nativeItemRef,
3398 ...(interrupted ? { status: "interrupted" as const } : {}),
3399 },
3400 });
3401 }),
3402 });
3403 const reasoningPartKey = (
3404 turnId: string,
3405 nativeItemId: string,
3406 stream: "summary" | "content",
3407 index: number,
3408 ) => {
3409 const key = JSON.stringify([turnId, nativeItemId, stream, index]);
3410 reasoningParts.set(key, { turnId, nativeItemId, stream, index });
3411 return key;
3412 };
3413 const appendReasoning = Effect.fn("CodexAdapterV2.appendReasoning")(function* (
3414 payload: { turnId: string; itemId: string; delta: string },
3415 stream: "summary" | "content",
3416 index: number,
3417 ) {
3418 const context = yield* awaitActiveTurn(payload.turnId);
3419 if (context === undefined || payload.delta.length === 0) return;
3420 yield* completeProviderRetry(context, yield* DateTime.now);
3421 const itemId = reasoningPartKey(payload.turnId, payload.itemId, stream, index);
3422 // Reserve the position before the delayed flush, ahead of later tool items.
3423 yield* resolveItemOrdinal(context, itemId);
3424 yield* reasoningDeltas.append({ turnId: payload.turnId, itemId, delta: payload.delta });
3425 });
3426 const completeReasoning = Effect.fn("CodexAdapterV2.completeReasoning")(function* (
3427 turnId: string,
3428 item: { id: string; summary?: ReadonlyArray<string>; content?: ReadonlyArray<string> },
3429 ) {
3430 for (const stream of ["summary", "content"] as const) {
3431 for (const [index] of (item[stream] ?? []).entries()) {
3432 reasoningPartKey(turnId, item.id, stream, index);
3433 }
3434 }
3435 for (const [key, part] of reasoningParts) {
3436 if (part.turnId !== turnId || part.nativeItemId !== item.id) continue;
3437 const finalText = item[part.stream]?.[part.index];
3438 yield* reasoningDeltas.complete({
3439 turnId,
3440 itemId: key,
3441 ...(finalText ? { finalText } : {}),
3442 emitEmpty: false,
3443 });
3444 reasoningParts.delete(key);
3445 }
3446 });
3447
3448 const agentMessageDeltas = yield* makeProviderTextDeltaCoalescer({
3449 flushIntervalMs: CODEX_ASSISTANT_DELTA_FLUSH_INTERVAL_MS,
3450 emit: (update) =>
3451 Effect.gen(function* () {
3452 const context = yield* awaitActiveTurn(update.turnId);
3453 if (context === undefined) {
3454 return;
3455 }
3456 const finalAnswerItem = (yield* Ref.get(finalAnswerItemIdsByTurn))
3457 .get(update.turnId)
3458 ?.has(update.itemId);
3459 if (finalAnswerItem) {
3460 const finalAnswerItemIds = (yield* Ref.get(finalAnswerItemIdsByTurn)).get(
3461 update.turnId,
3462 );
3463 const completedTexts =
3464 (yield* Ref.get(completedFinalAnswerTextsByTurn)).get(update.turnId) ??
3465 new Set<string>();
3466 const firstFinalAnswerItemId = finalAnswerItemIds?.values().next().value;
3467 const duplicateCompletion =
3468 update.completed &&
3469 completedTexts.size > 0 &&
3470 (update.text.length === 0 || completedTexts.has(update.text));
3471 const deferredStreamingUpdate =
3472 !update.completed &&
3473 (completedTexts.size > 0 || firstFinalAnswerItemId !== update.itemId);
3474 if (deferredStreamingUpdate || duplicateCompletion) {
3475 return;
3476 }
3477 }
3478 const artifacts = yield* buildAgentMessageArtifacts(
3479 context,
3480 { id: update.itemId, text: update.text },
3481 update.completed,
3482 );
3483 yield* emitProviderEvent({
3484 type: "node.updated",
3485 driver: CODEX_PROVIDER,
3486 node: artifacts.node,
3487 });
3488 // Clients render streaming text from the turn item, so the
3489 // message only carries the final text. flushTurn completes every
3490 // buffered item when a turn ends, interrupted or not.
3491 if (update.completed) {
3492 yield* emitProviderEvent({
3493 type: "message.updated",
3494 driver: CODEX_PROVIDER,
3495 message: artifacts.message,
3496 });
3497 }
3498 yield* emitProviderEvent({
3499 type: "turn_item.updated",
3500 driver: CODEX_PROVIDER,
3501 turnItem: artifacts.turnItem,
3502 });
3503 if (finalAnswerItem && update.completed) {
3504 yield* Ref.update(completedFinalAnswerTextsByTurn, (current) => {
3505 const updated = new Map(current);
3506 const texts = new Set(updated.get(update.turnId) ?? []);
3507 texts.add(update.text);
3508 updated.set(update.turnId, texts);
3509 return updated;
3510 });
3511 }
3512 }),
3513 });
3514
3515 const emitSubagentUserMessage = (
3516 context: ActiveCodexTurnContext,
3517 item: Extract<
3518 | CodexSchema.V2ItemStartedNotification__ThreadItem
3519 | CodexSchema.V2ItemCompletedNotification__ThreadItem,
3520 { type: "userMessage" }
3521 >,
3522 nativeStartedAt?: DateTime.Utc,
3523 ) =>
3524 Effect.gen(function* () {
3525 if (context.subagent === null || context.providerTurnOrdinal === 1) {
3526 return false;
3527 }
3528 const text = codexUserMessageText(item.content);
3529 if (text.length === 0) {
3530 return false;
3531 }
3532 const { ordinal, startedAt } = yield* resolveItemPosition(
3533 context,
3534 item.id,
3535 nativeStartedAt,
3536 );
3537 const artifacts = makeSubagentConversationArtifacts({
3538 senderThreadId: context.subagent.parentContext.projectionThreadId,
3539 messageId: idAllocator.derive.messageFromProviderItem({
3540 driver: CODEX_PROVIDER,
3541 nativeItemId: item.id,
3542 }),
3543 turnItemId: idAllocator.derive.turnItemFromProviderItem({
3544 driver: CODEX_PROVIDER,
3545 nativeItemId: item.id,
3546 }),
3547 threadId: context.projectionThreadId,
3548 rootNodeId: context.rootNodeId,
3549 providerThreadId: context.providerThread.id,
3550 providerTurnId: context.providerTurnId,
3551 nativeItemRef: codexNativeItemRef(item.id),
3552 role: "user",
3553 text,
3554 ordinal,
3555 now: startedAt,
3556 });
3557 yield* emitProviderEvent({
3558 type: "message.updated",
3559 driver: CODEX_PROVIDER,
3560 message: artifacts.message,
3561 });
3562 yield* emitProviderEvent({
3563 type: "turn_item.updated",
3564 driver: CODEX_PROVIDER,
3565 turnItem: artifacts.turnItem,
3566 });
3567 return true;
3568 });
3569
3570 const buildCommandExecutionArtifacts = (
3571 context: ActiveCodexTurnContext,
3572 item: Extract<
3573 | CodexSchema.V2ItemStartedNotification__ThreadItem
3574 | CodexSchema.V2ItemCompletedNotification__ThreadItem,
3575 { type: "commandExecution" }
3576 >,
3577 ) =>
3578 Effect.gen(function* () {
3579 const updatedAt = yield* DateTime.now;
3580 const status = codexItemStatus(item.status);
3581 const completedAt = status.completed ? updatedAt : null;
3582 const nodeId = idAllocator.derive.nodeFromProviderItem({
3583 driver: CODEX_PROVIDER,
3584 nativeItemId: item.id,
3585 });
3586 const turnItemId = idAllocator.derive.turnItemFromProviderItem({
3587 driver: CODEX_PROVIDER,
3588 nativeItemId: item.id,
3589 });
3590 const { ordinal, startedAt } = yield* resolveItemPosition(context, item.id);
3591 const node: OrchestrationV2ExecutionNode = {
3592 id: nodeId,
3593 threadId: context.projectionThreadId,
3594 runId: context.projectionRunId,
3595 parentNodeId: context.itemParentNodeId,
3596 rootNodeId: context.rootNodeId,
3597 kind: "tool_call",
3598 status: status.node,
3599 countsForRun: false,
3600 providerThreadId: context.providerThread.id,
3601 providerTurnId: context.providerTurnId,
3602 nativeItemRef: codexNativeItemRef(item.id),
3603 runtimeRequestId: null,
3604 checkpointScopeId: null,
3605 startedAt,
3606 completedAt,
3607 };
3608 const turnItem: OrchestrationV2TurnItem = {
3609 id: turnItemId,
3610 threadId: context.projectionThreadId,
3611 runId: context.projectionRunId,
3612 nodeId,
3613 providerThreadId: context.providerThread.id,
3614 providerTurnId: context.providerTurnId,
3615 nativeItemRef: codexNativeItemRef(item.id),
3616 parentItemId: null,
3617 ordinal,
3618 status: status.turnItem,
3619 title: null,
3620 startedAt,
3621 completedAt,
3622 updatedAt,
3623 type: "command_execution",
3624 input: item.command,
3625 ...(item.aggregatedOutput === null || item.aggregatedOutput === undefined
3626 ? {}
3627 : { output: item.aggregatedOutput }),
3628 ...(item.exitCode === null || item.exitCode === undefined
3629 ? {}
3630 : { exitCode: item.exitCode }),
3631 };
3632 return { node, turnItem };
3633 });
3634
3635 const buildFileChangeArtifacts = (
3636 context: ActiveCodexTurnContext,
3637 item: Extract<
3638 CodexSchema.V2ItemCompletedNotification__ThreadItem,
3639 { type: "fileChange" }
3640 >,
3641 ) =>
3642 Effect.gen(function* () {
3643 const firstChange = item.changes[0];
3644 if (firstChange === undefined) {
3645 return null;
3646 }
3647
3648 const updatedAt = yield* DateTime.now;
3649 const status = codexItemStatus(item.status);
3650 const completedAt = status.completed ? updatedAt : null;
3651 const nodeId = idAllocator.derive.nodeFromProviderItem({
3652 driver: CODEX_PROVIDER,
3653 nativeItemId: item.id,
3654 });
3655 const turnItemId = idAllocator.derive.turnItemFromProviderItem({
3656 driver: CODEX_PROVIDER,
3657 nativeItemId: item.id,
3658 });
3659 const { ordinal, startedAt } = yield* resolveItemPosition(context, item.id);
3660 const node: OrchestrationV2ExecutionNode = {
3661 id: nodeId,
3662 threadId: context.projectionThreadId,
3663 runId: context.projectionRunId,
3664 parentNodeId: context.itemParentNodeId,
3665 rootNodeId: context.rootNodeId,
3666 kind: "tool_call",
3667 status: status.node,
3668 countsForRun: false,
3669 providerThreadId: context.providerThread.id,
3670 providerTurnId: context.providerTurnId,
3671 nativeItemRef: codexNativeItemRef(item.id),
3672 runtimeRequestId: null,
3673 checkpointScopeId: null,
3674 startedAt,
3675 completedAt,
3676 };
3677 const turnItem: OrchestrationV2TurnItem = {
3678 id: turnItemId,
3679 threadId: context.projectionThreadId,
3680 runId: context.projectionRunId,
3681 nodeId,
3682 providerThreadId: context.providerThread.id,
3683 providerTurnId: context.providerTurnId,
3684 nativeItemRef: codexNativeItemRef(item.id),
3685 parentItemId: null,
3686 ordinal,
3687 status: status.turnItem,
3688 title: null,
3689 startedAt,
3690 completedAt,
3691 updatedAt,
3692 type: "file_change",
3693 fileName: firstChange.path,
3694 diffStr: firstChange.diff,
3695 };
3696 return { node, turnItem };
3697 });
3698
3699 const buildWebSearchArtifacts = (input: {
3700 readonly context: ActiveCodexTurnContext;
3701 readonly item: CodexWebSearchItem;
3702 readonly completed: boolean;
3703 }) =>
3704 Effect.gen(function* () {
3705 const updatedAt = yield* DateTime.now;
3706 const completedAt = input.completed ? updatedAt : null;
3707 const nodeId = idAllocator.derive.nodeFromProviderItem({
3708 driver: CODEX_PROVIDER,
3709 nativeItemId: input.item.id,
3710 });
3711 const turnItemId = idAllocator.derive.turnItemFromProviderItem({
3712 driver: CODEX_PROVIDER,
3713 nativeItemId: input.item.id,
3714 });
3715 const { ordinal, startedAt } = yield* resolveItemPosition(input.context, input.item.id);
3716 const patterns = webSearchPatterns(input.item);
3717 const status = input.completed ? "completed" : "running";
3718 const node: OrchestrationV2ExecutionNode = {
3719 id: nodeId,
3720 threadId: input.context.projectionThreadId,
3721 runId: input.context.projectionRunId,
3722 parentNodeId: input.context.itemParentNodeId,
3723 rootNodeId: input.context.rootNodeId,
3724 kind: "tool_call",
3725 status,
3726 countsForRun: false,
3727 providerThreadId: input.context.providerThread.id,
3728 providerTurnId: input.context.providerTurnId,
3729 nativeItemRef: codexNativeItemRef(input.item.id),
3730 runtimeRequestId: null,
3731 checkpointScopeId: null,
3732 startedAt,
3733 completedAt,
3734 };
3735 const turnItem: OrchestrationV2TurnItem = {
3736 id: turnItemId,
3737 threadId: input.context.projectionThreadId,
3738 runId: input.context.projectionRunId,
3739 nodeId,
3740 providerThreadId: input.context.providerThread.id,
3741 providerTurnId: input.context.providerTurnId,
3742 nativeItemRef: codexNativeItemRef(input.item.id),
3743 parentItemId: null,
3744 ordinal,
3745 status,
3746 title: null,
3747 startedAt,
3748 completedAt,
3749 updatedAt,
3750 type: "web_search",
3751 ...(patterns.length === 0 ? {} : { patterns: [...patterns] }),
3752 };
3753 return { node, turnItem };
3754 });
3755
3756 const buildDynamicToolArtifacts = (
3757 context: ActiveCodexTurnContext,
3758 item: CodexDynamicToolItem,
3759 ) =>
3760 Effect.gen(function* () {
3761 const updatedAt = yield* DateTime.now;
3762 const status = codexItemStatus(item.status);
3763 const completedAt = status.completed ? updatedAt : null;
3764 const nodeId = idAllocator.derive.nodeFromProviderItem({
3765 driver: CODEX_PROVIDER,
3766 nativeItemId: item.id,
3767 });
3768 const turnItemId = idAllocator.derive.turnItemFromProviderItem({
3769 driver: CODEX_PROVIDER,
3770 nativeItemId: item.id,
3771 });
3772 const { ordinal, startedAt } = yield* resolveItemPosition(context, item.id);
3773 const projection = yield* projectCodexDynamicToolItem(item).pipe(
3774 Effect.provideService(Crypto.Crypto, crypto),
3775 );
3776 const node: OrchestrationV2ExecutionNode = {
3777 id: nodeId,
3778 threadId: context.projectionThreadId,
3779 runId: context.projectionRunId,
3780 parentNodeId: context.itemParentNodeId,
3781 rootNodeId: context.rootNodeId,
3782 kind: "tool_call",
3783 status: status.node,
3784 countsForRun: false,
3785 providerThreadId: context.providerThread.id,
3786 providerTurnId: context.providerTurnId,
3787 nativeItemRef: codexNativeItemRef(item.id),
3788 runtimeRequestId: null,
3789 checkpointScopeId: null,
3790 startedAt,
3791 completedAt,
3792 };
3793 const turnItem: OrchestrationV2TurnItem = {
3794 id: turnItemId,
3795 threadId: context.projectionThreadId,
3796 runId: context.projectionRunId,
3797 nodeId,
3798 providerThreadId: context.providerThread.id,
3799 providerTurnId: context.providerTurnId,
3800 nativeItemRef: codexNativeItemRef(item.id),
3801 parentItemId: null,
3802 ordinal,
3803 title: projection.title ?? null,
3804 startedAt,
3805 completedAt,
3806 updatedAt,
3807 type: "dynamic_tool",
3808 ...projection,
3809 };
3810 return { node, turnItem };
3811 });
3812
3813 /**
3814 * Completes an MCP tool call whose tool names a UI resource: reads the
3815 * resource through Codex's MCP client, stores it, and records the app
3816 * in the item's output. The item stays running until then, which keeps
3817 * the run's event ingestion open past turn/completed; it runs off the
3818 * notification reader so a slow server cannot stall other events.
3819 */
3820 const captureMcpApp = (
3821 context: ActiveCodexTurnContext,
3822 nativeTurnId: string,
3823 item: Extract<CodexDynamicToolItem, { readonly type: "mcpToolCall" }>,
3824 resourceUri: string,
3825 ) =>
3826 Effect.gen(function* () {
3827 // Registered before it can run, so a turn ending right away sees it.
3828 const started = yield* Deferred.make<void>();
3829 const fiber = yield* Deferred.await(started).pipe(
3830 Effect.andThen(captureMcpAppItem(context, nativeTurnId, item, resourceUri)),
3831 Effect.forkIn(scope, { startImmediately: true }),
3832 );
3833 yield* Ref.update(pendingMcpAppCaptures, (current) => {
3834 const updated = new Map(current);
3835 updated.set(
3836 nativeTurnId,
3837 new Map(current.get(nativeTurnId) ?? []).set(item.id, { item, fiber }),
3838 );
3839 return updated;
3840 });
3841 yield* Deferred.succeed(started, undefined);
3842 });
3843
3844 const captureMcpAppItem = (
3845 context: ActiveCodexTurnContext,
3846 nativeTurnId: string,
3847 item: Extract<CodexDynamicToolItem, { readonly type: "mcpToolCall" }>,
3848 resourceUri: string,
3849 ) =>
3850 // Only reading and storing the document can be cancelled; a write
3851 // cut short removes its file. Once a document is stored, the capture
3852 // either lands it on the item or, when its turn already settled the
3853 // item, removes it, so no stored app goes unreferenced.
3854 Effect.uninterruptibleMask((restore) =>
3855 Effect.gen(function* () {
3856 const reference =
3857 item.status === "completed" && item.result != null && item.error == null
3858 ? yield* restore(
3859 getNativeThreadId(context.providerThread).pipe(
3860 Effect.flatMap((threadId) =>
3861 client.request("mcpServer/resource/read", {
3862 threadId,
3863 server: item.server,
3864 uri: resourceUri,
3865 }),
3866 ),
3867 Effect.timeout(MCP_APP_CAPTURE_TIMEOUT),
3868 Effect.flatMap((response) =>
3869 snapshotMcpApp({
3870 attachmentsDir: serverConfig.attachmentsDir,
3871 threadId: context.projectionThreadId,
3872 server: item.server,
3873 tool: item.tool,
3874 resourceUri,
3875 contents: response.contents,
3876 }),
3877 ),
3878 Effect.provideService(FileSystem.FileSystem, fileSystem),
3879 // Every failure and defect becomes a plain tool row,
3880 // so a crashed read never holds the turn open; only
3881 // interruption, the turn cancelling the capture, ends
3882 // the fiber here. Nothing typed is left for orDie.
3883 Effect.catchCauseIf(
3884 (cause) => !Cause.hasInterruptsOnly(cause),
3885 (cause) =>
3886 Effect.logWarning(
3887 "Failed to capture an MCP app; showing a plain tool row.",
3888 { server: item.server, tool: item.tool, resourceUri, cause },
3889 ).pipe(Effect.as(undefined)),
3890 ),
3891 Effect.orDie,
3892 ),
3893 )
3894 : undefined;
3895 const artifacts = yield* buildDynamicToolArtifacts(context, item);
3896 // Cleared before the final emit: ingestion re-checks pending work
3897 // after each event, and must see this capture done by then. A turn
3898 // that already ended badly took the capture and settled its item.
3899 if (!(yield* takePendingMcpAppCapture(nativeTurnId, item.id))) {
3900 if (reference !== undefined) {
3901 const filePath = resolveAttachmentPathById({
3902 attachmentsDir: serverConfig.attachmentsDir,
3903 attachmentId: reference.attachmentId,
3904 });
3905 if (filePath !== null) {
3906 yield* fileSystem.remove(filePath, { force: true }).pipe(Effect.ignore);
3907 }
3908 }
3909 return;
3910 }
3911 const turnItem =
3912 reference === undefined || artifacts.turnItem.type !== "dynamic_tool"
3913 ? artifacts.turnItem
3914 : {
3915 ...artifacts.turnItem,
3916 // The full CallToolResult the app replays, beside the app.
3917 output: { [MCP_APP_OUTPUT_KEY]: reference, result: item.result },
3918 };
3919 yield* emitProviderEvent({
3920 type: "node.updated",
3921 driver: CODEX_PROVIDER,
3922 node: artifacts.node,
3923 });
3924 yield* emitProviderEvent({
3925 type: "turn_item.updated",
3926 driver: CODEX_PROVIDER,
3927 turnItem,
3928 });
3929 // Only a turn that already ended is released; an active one
3930 // still needs its final-answer bookkeeping.
3931 if ((yield* Ref.get(settledTurns)).has(nativeTurnId)) {
3932 yield* releaseSettledTurnIfIdle(nativeTurnId);
3933 }
3934 }),
3935 );
3936
3937 /**
3938 * Settles the items of app captures still running when their turn is
3939 * interrupted or fails: the capture is cancelled and its tool call is
3940 * reported as it completed, without the app, before the turn's
3941 * terminal event closes ingestion.
3942 */
3943 const terminalizeMcpAppCaptures = (context: ActiveCodexTurnContext, nativeTurnId: string) =>
3944 Effect.gen(function* () {
3945 const captures = (yield* Ref.get(pendingMcpAppCaptures)).get(nativeTurnId);
3946 if (captures === undefined) return;
3947 for (const { item, fiber } of captures.values()) {
3948 if (!(yield* takePendingMcpAppCapture(nativeTurnId, item.id))) continue;
3949 yield* Fiber.interrupt(fiber);
3950 const artifacts = yield* buildDynamicToolArtifacts(context, item);
3951 yield* emitProviderEvent({
3952 type: "node.updated",
3953 driver: CODEX_PROVIDER,
3954 node: artifacts.node,
3955 });
3956 yield* emitProviderEvent({
3957 type: "turn_item.updated",
3958 driver: CODEX_PROVIDER,
3959 turnItem: artifacts.turnItem,
3960 });
3961 }
3962 });
3963
3964 const buildProposedPlanArtifacts = (input: {
3965 readonly context: ActiveCodexTurnContext;
3966 readonly nativeItemId: string;
3967 readonly status: OrchestrationV2PlanArtifact["status"];
3968 readonly markdown: string;
3969 readonly completed?: boolean;
3970 }) =>
3971 Effect.gen(function* () {
3972 const updatedAt = yield* DateTime.now;
3973 const completedAt = input.completed === true ? updatedAt : null;
3974 const planId = yield* resolvePlanId(input.context, input.nativeItemId);
3975 const nodeId = idAllocator.derive.nodeFromProviderItem({
3976 driver: CODEX_PROVIDER,
3977 nativeItemId: input.nativeItemId,
3978 });
3979 const turnItemId = idAllocator.derive.turnItemFromProviderItem({
3980 driver: CODEX_PROVIDER,
3981 nativeItemId: input.nativeItemId,
3982 });
3983 const { ordinal, startedAt } = yield* resolveItemPosition(
3984 input.context,
3985 input.nativeItemId,
3986 );
3987 const plan: OrchestrationV2PlanArtifact = {
3988 id: planId,
3989 threadId: input.context.projectionThreadId,
3990 runId: input.context.projectionRunId,
3991 nodeId,
3992 kind: "proposed_plan",
3993 status: input.status,
3994 markdown: input.markdown,
3995 };
3996 const node: OrchestrationV2ExecutionNode = {
3997 id: nodeId,
3998 threadId: input.context.projectionThreadId,
3999 runId: input.context.projectionRunId,
4000 parentNodeId: input.context.itemParentNodeId,
4001 rootNodeId: input.context.rootNodeId,
4002 kind: "plan",
4003 status: input.completed === true ? "completed" : "running",
4004 countsForRun: false,
4005 providerThreadId: input.context.providerThread.id,
4006 providerTurnId: input.context.providerTurnId,
4007 nativeItemRef: codexNativeItemRef(input.nativeItemId),
4008 runtimeRequestId: null,
4009 checkpointScopeId: null,
4010 startedAt,
4011 completedAt,
4012 };
4013 const turnItem: OrchestrationV2TurnItem = {
4014 id: turnItemId,
4015 threadId: input.context.projectionThreadId,
4016 runId: input.context.projectionRunId,
4017 nodeId,
4018 providerThreadId: input.context.providerThread.id,
4019 providerTurnId: input.context.providerTurnId,
4020 nativeItemRef: codexNativeItemRef(input.nativeItemId),
4021 parentItemId: null,
4022 ordinal,
4023 status: input.completed === true ? "completed" : "running",
4024 title: null,
4025 startedAt,
4026 completedAt,
4027 updatedAt,
4028 type: "proposed_plan",
4029 planId,
4030 markdown: input.markdown,
4031 streaming: input.completed !== true,
4032 };
4033 return { node, plan, turnItem };
4034 });
4035
4036 // Streamed plan text, flushed at most once per interval. A turn that ends
4037 // early leaves its partial plan streaming, as an unfinished plan item.
4038 const planDeltas = yield* makeProviderTextDeltaCoalescer({
4039 flushIntervalMs: CODEX_PLAN_DELTA_FLUSH_INTERVAL_MS,
4040 emit: (update) =>
4041 Effect.gen(function* () {
4042 const resolved = yield* resolveItemEventContext(update.turnId);
4043 if (resolved === undefined) return;
4044 const interrupted = (yield* Ref.get(terminalizedNonCompletedNativeTurns)).has(
4045 update.turnId,
4046 );
4047 const artifacts = yield* buildProposedPlanArtifacts({
4048 context: resolved.context,
4049 nativeItemId: update.itemId,
4050 // A finished proposal stays active until Implement consumes it.
4051 status: "active",
4052 markdown: update.text,
4053 completed: update.completed && !interrupted,
4054 });
4055 yield* emitProviderEvent({
4056 type: "node.updated",
4057 driver: CODEX_PROVIDER,
4058 node: artifacts.node,
4059 });
4060 yield* emitProviderEvent({
4061 type: "plan.updated",
4062 driver: CODEX_PROVIDER,
4063 plan: artifacts.plan,
4064 });
4065 yield* emitProviderEvent({
4066 type: "turn_item.updated",
4067 driver: CODEX_PROVIDER,
4068 turnItem: artifacts.turnItem,
4069 });
4070 }).pipe(Effect.orDie),
4071 });
4072
4073 const buildTodoListArtifacts = (input: {
4074 readonly context: ActiveCodexTurnContext;
4075 readonly nativeItemId: string;
4076 readonly status: OrchestrationV2PlanArtifact["status"];
4077 readonly steps: ReadonlyArray<OrchestrationV2PlanStep>;
4078 readonly explanation?: string;
4079 readonly completed?: boolean;
4080 }) =>
4081 Effect.gen(function* () {
4082 const updatedAt = yield* DateTime.now;
4083 const completedAt = input.completed === true ? updatedAt : null;
4084 const planId = yield* resolvePlanId(input.context, input.nativeItemId);
4085 const nodeId = idAllocator.derive.nodeFromProviderItem({
4086 driver: CODEX_PROVIDER,
4087 nativeItemId: input.nativeItemId,
4088 });
4089 const turnItemId = idAllocator.derive.turnItemFromProviderItem({
4090 driver: CODEX_PROVIDER,
4091 nativeItemId: input.nativeItemId,
4092 });
4093 const { ordinal, startedAt } = yield* resolveItemPosition(
4094 input.context,
4095 input.nativeItemId,
4096 );
4097 const plan: OrchestrationV2PlanArtifact = {
4098 id: planId,
4099 threadId: input.context.projectionThreadId,
4100 runId: input.context.projectionRunId,
4101 nodeId,
4102 kind: "todo_list",
4103 status: input.status,
4104 steps: [...input.steps],
4105 ...(input.explanation === undefined ? {} : { explanation: input.explanation }),
4106 };
4107 const node: OrchestrationV2ExecutionNode = {
4108 id: nodeId,
4109 threadId: input.context.projectionThreadId,
4110 runId: input.context.projectionRunId,
4111 parentNodeId: input.context.itemParentNodeId,
4112 rootNodeId: input.context.rootNodeId,
4113 kind: "todo_list",
4114 status: input.completed === true ? "completed" : "running",
4115 countsForRun: false,
4116 providerThreadId: input.context.providerThread.id,
4117 providerTurnId: input.context.providerTurnId,
4118 nativeItemRef: codexNativeItemRef(input.nativeItemId),
4119 runtimeRequestId: null,
4120 checkpointScopeId: null,
4121 startedAt,
4122 completedAt,
4123 };
4124 const turnItem: OrchestrationV2TurnItem = {
4125 id: turnItemId,
4126 threadId: input.context.projectionThreadId,
4127 runId: input.context.projectionRunId,
4128 nodeId,
4129 providerThreadId: input.context.providerThread.id,
4130 providerTurnId: input.context.providerTurnId,
4131 nativeItemRef: codexNativeItemRef(input.nativeItemId),
4132 parentItemId: null,
4133 ordinal,
4134 status: input.completed === true ? "completed" : "running",
4135 title: null,
4136 startedAt,
4137 completedAt,
4138 updatedAt,
4139 type: "todo_list",
4140 planId,
4141 steps: [...input.steps],
4142 ...(input.explanation === undefined ? {} : { explanation: input.explanation }),
4143 };
4144 return { node, plan, turnItem };
4145 });
4146
4147 const buildApprovalRequestArtifacts = (input: {
4148 readonly context: ActiveCodexTurnContext;
4149 readonly nativeItemId: string;
4150 readonly nativeRequestId: string;
4151 readonly requestKind: ProviderRequestKind;
4152 readonly prompt?: string | null;
4153 readonly appName?: string;
4154 readonly options?: ReadonlyArray<ProviderApprovalOption>;
4155 }) =>
4156 Effect.gen(function* () {
4157 const createdAt = yield* DateTime.now;
4158 // A subagent's approval is asked on the top-level thread and run,
4159 // under the subagent that asked, where the user can see and answer it.
4160 const { owner, subagent } = approvalOwnerCodexTurn(input.context);
4161 const parentNodeId =
4162 subagent?.subagentNodeId ??
4163 idAllocator.derive.nodeFromProviderItem({
4164 driver: CODEX_PROVIDER,
4165 nativeItemId: input.nativeItemId,
4166 });
4167 const ordinal = yield* resolveItemOrdinal(
4168 owner,
4169 `${input.nativeItemId}:approval:${input.nativeRequestId}`,
4170 );
4171 const requestId = yield* idAllocator.allocate.runtimeRequest({
4172 driver: CODEX_PROVIDER,
4173 providerTurnId: owner.providerTurnId,
4174 nativeRequestId: input.nativeRequestId,
4175 });
4176 const nodeId = idAllocator.derive.approvalNode({ requestId });
4177 const providerSessionId = owner.input.providerThread.providerSessionId;
4178 if (providerSessionId === null) {
4179 return yield* toProtocolError(
4180 `Provider thread ${owner.providerThread.id} is missing a provider session id.`,
4181 );
4182 }
4183 const node: OrchestrationV2ExecutionNode = {
4184 id: nodeId,
4185 threadId: owner.projectionThreadId,
4186 runId: owner.projectionRunId,
4187 parentNodeId,
4188 rootNodeId: owner.rootNodeId,
4189 kind: "approval_request",
4190 status: "waiting",
4191 countsForRun: false,
4192 providerThreadId: owner.providerThread.id,
4193 providerTurnId: owner.providerTurnId,
4194 nativeItemRef: codexNativeItemRef(input.nativeItemId),
4195 runtimeRequestId: requestId,
4196 checkpointScopeId: null,
4197 startedAt: createdAt,
4198 completedAt: null,
4199 };
4200 const request: OrchestrationV2RuntimeRequest = {
4201 id: requestId,
4202 nodeId,
4203 providerTurnId: owner.providerTurnId,
4204 nativeRequestRef: {
4205 driver: CODEX_PROVIDER,
4206 nativeId: input.nativeRequestId,
4207 strength: "strong",
4208 },
4209 kind: input.requestKind,
4210 status: "pending",
4211 responseCapability: {
4212 type: "live",
4213 providerSessionId,
4214 },
4215 createdAt,
4216 resolvedAt: null,
4217 };
4218 const turnItem: OrchestrationV2TurnItem = {
4219 id: idAllocator.derive.approvalTurnItem({ requestId }),
4220 threadId: owner.projectionThreadId,
4221 runId: owner.projectionRunId,
4222 nodeId,
4223 providerThreadId: owner.providerThread.id,
4224 providerTurnId: owner.providerTurnId,
4225 nativeItemRef: codexNativeItemRef(input.nativeItemId),
4226 parentItemId: null,
4227 ordinal,
4228 status: "waiting",
4229 title: null,
4230 startedAt: createdAt,
4231 completedAt: null,
4232 updatedAt: createdAt,
4233 type: "approval_request",
4234 requestId,
4235 requestKind: input.requestKind,
4236 ...(input.prompt === null || input.prompt === undefined
4237 ? {}
4238 : { prompt: input.prompt }),
4239 ...(input.appName === undefined ? {} : { appName: input.appName }),
4240 ...(input.options === undefined ? {} : { options: input.options }),
4241 };
4242 return { node, request, turnItem };
4243 });
4244
4245 const buildUserInputRequestArtifacts = (input: {
4246 readonly context: ActiveCodexTurnContext;
4247 readonly nativeItemId: string;
4248 readonly nativeRequestId: string;
4249 readonly questions: ReadonlyArray<CodexSchema.ToolRequestUserInputParams__ToolRequestUserInputQuestion>;
4250 readonly responseMode?: "message";
4251 }) =>
4252 Effect.gen(function* () {
4253 const createdAt = yield* DateTime.now;
4254 const requestId = yield* idAllocator.allocate.runtimeRequest({
4255 driver: CODEX_PROVIDER,
4256 providerTurnId: input.context.providerTurnId,
4257 nativeRequestId: input.nativeRequestId,
4258 });
4259 const providerSessionId = input.context.input.providerThread.providerSessionId;
4260 if (providerSessionId === null) {
4261 return yield* toProtocolError(
4262 `Provider thread ${input.context.providerThread.id} is missing a provider session id.`,
4263 );
4264 }
4265 const questions = input.questions.map((question, index) => ({
4266 id: nonEmptyText(question.id, `question-${index + 1}`),
4267 header: nonEmptyText(question.header, "Question"),
4268 question: nonEmptyText(question.question, "Choose an answer."),
4269 options:
4270 question.options?.map((option, optionIndex) => ({
4271 label: nonEmptyText(option.label, `Option ${optionIndex + 1}`),
4272 description: nonEmptyText(option.description, option.label),
4273 })) ?? [],
4274 }));
4275 const nodeId = idAllocator.derive.nodeFromProviderItem({
4276 driver: CODEX_PROVIDER,
4277 nativeItemId: input.nativeItemId,
4278 });
4279 const turnItemId = idAllocator.derive.turnItemFromProviderItem({
4280 driver: CODEX_PROVIDER,
4281 nativeItemId: input.nativeItemId,
4282 });
4283 const ordinal = yield* resolveItemOrdinal(input.context, input.nativeItemId);
4284 const node: OrchestrationV2ExecutionNode = {
4285 id: nodeId,
4286 threadId: input.context.projectionThreadId,
4287 runId: input.context.projectionRunId,
4288 parentNodeId: input.context.itemParentNodeId,
4289 rootNodeId: input.context.rootNodeId,
4290 kind: "user_input_request",
4291 status: "waiting",
4292 countsForRun: false,
4293 providerThreadId: input.context.providerThread.id,
4294 providerTurnId: input.context.providerTurnId,
4295 nativeItemRef: codexNativeItemRef(input.nativeItemId),
4296 runtimeRequestId: requestId,
4297 checkpointScopeId: null,
4298 startedAt: createdAt,
4299 completedAt: null,
4300 };
4301 const request: OrchestrationV2RuntimeRequest = {
4302 id: requestId,
4303 nodeId,
4304 providerTurnId: input.context.providerTurnId,
4305 nativeRequestRef: {
4306 driver: CODEX_PROVIDER,
4307 nativeId: input.nativeRequestId,
4308 strength: "strong",
4309 },
4310 kind: "user_input",
4311 status: "pending",
4312 responseCapability:
4313 input.responseMode === "message"
4314 ? { type: "message" }
4315 : { type: "live", providerSessionId },
4316 createdAt,
4317 resolvedAt: null,
4318 };
4319 const turnItem: OrchestrationV2TurnItem = {
4320 id: turnItemId,
4321 threadId: input.context.projectionThreadId,
4322 runId: input.context.projectionRunId,
4323 nodeId,
4324 providerThreadId: input.context.providerThread.id,
4325 providerTurnId: input.context.providerTurnId,
4326 nativeItemRef: codexNativeItemRef(input.nativeItemId),
4327 parentItemId: null,
4328 ordinal,
4329 status: "waiting",
4330 title: null,
4331 startedAt: createdAt,
4332 completedAt: null,
4333 updatedAt: createdAt,
4334 type: "user_input_request",
4335 requestId,
4336 questions,
4337 ...(input.responseMode === undefined ? {} : { responseMode: input.responseMode }),
4338 };
4339 return { node, request, turnItem };
4340 });
4341
4342 yield* client.handleServerNotification("item/agentMessage/delta", (payload) =>
4343 Effect.gen(function* () {
4344 const context = (yield* Ref.get(activeTurns)).get(payload.turnId);
4345 if (context !== undefined) {
4346 yield* completeProviderRetry(context, yield* DateTime.now);
4347 }
4348 yield* agentMessageDeltas.append({
4349 turnId: payload.turnId,
4350 itemId: payload.itemId,
4351 delta: payload.delta,
4352 });
4353 }),
4354 );
4355
4356 yield* client.handleServerNotification("item/reasoning/summaryTextDelta", (payload) =>
4357 appendReasoning(payload, "summary", payload.summaryIndex),
4358 );
4359 yield* client.handleServerNotification("item/reasoning/textDelta", (payload) =>
4360 appendReasoning(payload, "content", payload.contentIndex),
4361 );
4362
4363 yield* client.handleServerNotification("item/plan/delta", (payload) =>
4364 Effect.gen(function* () {
4365 const context = yield* awaitActiveTurn(payload.turnId);
4366 if (context === undefined) {
4367 return;
4368 }
4369 yield* completeProviderRetry(context, yield* DateTime.now);
4370 yield* planDeltas.append({
4371 turnId: payload.turnId,
4372 itemId: payload.itemId,
4373 delta: payload.delta,
4374 });
4375 }).pipe(Effect.orDie),
4376 );
4377
4378 yield* client.handleServerNotification("turn/plan/updated", (payload) =>
4379 Effect.gen(function* () {
4380 const context = yield* awaitActiveTurn(payload.turnId);
4381 if (context === undefined) {
4382 return;
4383 }
4384 yield* completeProviderRetry(context, yield* DateTime.now);
4385 const steps = payload.plan.map((step, index) => ({
4386 id: `step-${index + 1}`,
4387 text: nonEmptyText(step.step, `Step ${index + 1}`),
4388 status: codexPlanStepStatus(step.status),
4389 }));
4390 const explanation = trimText(payload.explanation);
4391 const artifacts = yield* buildTodoListArtifacts({
4392 context,
4393 nativeItemId: `turn-plan:${payload.turnId}`,
4394 status: "active",
4395 ...(explanation === undefined ? {} : { explanation }),
4396 steps,
4397 });
4398 yield* emitProviderEvent({
4399 type: "node.updated",
4400 driver: CODEX_PROVIDER,
4401 node: artifacts.node,
4402 });
4403 yield* emitProviderEvent({
4404 type: "plan.updated",
4405 driver: CODEX_PROVIDER,
4406 plan: artifacts.plan,
4407 });
4408 yield* emitProviderEvent({
4409 type: "turn_item.updated",
4410 driver: CODEX_PROVIDER,
4411 turnItem: artifacts.turnItem,
4412 });
4413 }).pipe(Effect.orDie),
4414 );
4415
4416 yield* client.handleServerNotification("account/rateLimits/updated", (payload) =>
4417 Effect.gen(function* () {
4418 yield* Ref.update(rateLimitSnapshot, (previous) =>
4419 mergeCodexRateLimits(previous, payload.rateLimits),
4420 );
4421 const resetAt = codexUsageLimitResetAt(yield* Ref.get(rateLimitSnapshot));
4422 for (const item of (yield* Ref.get(limitedTurnItems)).values()) {
4423 // Fill late reset data once; later account windows do not change this stopped turn.
4424 if (resetAt === null || item.failure.resetAt != null) continue;
4425 const updated = {
4426 ...item,
4427 updatedAt: yield* DateTime.now,
4428 failure: { ...item.failure, resetAt },
4429 };
4430 yield* Ref.update(limitedTurnItems, (current) =>
4431 new Map(current).set(item.providerThreadId!, updated),
4432 );
4433 yield* emitProviderEvent({
4434 type: "turn_item.updated",
4435 driver: CODEX_PROVIDER,
4436 turnItem: updated,
4437 });
4438 }
4439 const update = codexRateLimitsToUpdate(payload.rateLimits);
4440 if (update && adapterOptions.onUsageLimits) {
4441 const checkedAt = DateTime.formatIso(yield* DateTime.now);
4442 yield* adapterOptions.onUsageLimits({ ...update, checkedAt });
4443 }
4444 }).pipe(Effect.orDie),
4445 );
4446
4447 yield* client.handleServerNotification("thread/tokenUsage/updated", (payload) =>
4448 Effect.gen(function* () {
4449 accumulateCodexTurnTokenUsage(
4450 usageStateForThread(payload.threadId),
4451 payload.turnId,
4452 payload.tokenUsage,
4453 );
4454 const context = yield* awaitActiveTurn(payload.turnId);
4455 if (context === undefined) {
4456 return;
4457 }
4458 const now = yield* DateTime.now;
4459 // Live context usage rides on the provider turn (#8144): the turn
4460 // is the natural owner and re-emitting it never disturbs items.
4461 yield* emitProviderEvent({
4462 type: "provider_turn.updated",
4463 driver: CODEX_PROVIDER,
4464 threadId: context.projectionThreadId,
4465 providerTurn: {
4466 id: context.providerTurnId,
4467 providerThreadId: context.providerThread.id,
4468 nodeId: context.providerNodeId,
4469 runAttemptId: context.subagent === null ? context.input.attemptId : null,
4470 nativeTurnRef: {
4471 driver: CODEX_PROVIDER,
4472 nativeId: payload.turnId,
4473 strength: "strong",
4474 },
4475 ordinal: context.providerTurnOrdinal,
4476 status: "running",
4477 startedAt: context.startedAt,
4478 completedAt: null,
4479 tokenUsage: codexProviderTurnTokenUsage(
4480 payload.tokenUsage,
4481 DateTime.formatIso(now),
4482 ),
4483 },
4484 });
4485 }).pipe(Effect.orDie),
4486 );
4487
4488 yield* client.handleServerNotification("thread/settings/updated", (payload) =>
4489 updateSubagentSelection(
4490 payload.threadId,
4491 payload.threadSettings.model,
4492 payload.threadSettings.effort,
4493 payload.threadSettings.serviceTier,
4494 ),
4495 );
4496 yield* client.handleServerNotification("model/rerouted", (payload) =>
4497 updateSubagentSelection(payload.threadId, payload.toModel),
4498 );
4499
4500 yield* client.handleServerNotification("turn/started", (payload) =>
4501 Effect.gen(function* () {
4502 const context = (yield* Ref.get(activeTurns)).get(payload.turn.id);
4503 if (context !== undefined) {
4504 if (context.nativeStartReady !== undefined) {
4505 yield* Deferred.succeed(context.nativeStartReady, undefined);
4506 }
4507 return;
4508 }
4509 const pendingRootTurn = (yield* Ref.get(pendingRootTurns)).get(payload.threadId);
4510 if (pendingRootTurn !== undefined) {
4511 yield* registerRootTurn({
4512 turnInput: pendingRootTurn,
4513 nativeTurnId: payload.turn.id,
4514 startedAt: codexTimestamp(payload.turn.startedAt),
4515 });
4516 yield* Ref.update(pendingRootTurns, (current) => {
4517 const updated = new Map(current);
4518 updated.delete(payload.threadId);
4519 return updated;
4520 });
4521 return;
4522 }
4523 // Codex continues an active goal on its own. The next turn joins the
4524 // run its previous turn belongs to.
4525 const goalHold = goalHolds.get(payload.threadId);
4526 if (goalHold !== undefined) {
4527 goalHolds.delete(payload.threadId);
4528 yield* emitProviderEvent({
4529 type: "provider_turn.updated",
4530 driver: CODEX_PROVIDER,
4531 threadId: goalHold.context.projectionThreadId,
4532 providerTurn: goalHold.completedTurn,
4533 });
4534 const next = yield* registerRootTurn({
4535 turnInput: {
4536 ...goalHold.context.input,
4537 providerTurnOrdinal: goalHold.context.providerTurnOrdinal + 1,
4538 },
4539 nativeTurnId: payload.turn.id,
4540 startedAt: codexTimestamp(payload.turn.startedAt),
4541 });
4542 const goalRun = goalRuns.get(goalHold.context.providerTurnId) ?? [goalHold.context];
4543 goalRun.push(next);
4544 goalRuns.set(goalHold.context.providerTurnId, goalRun);
4545 goalRuns.set(next.providerTurnId, goalRun);
4546 yield* Deferred.succeed(goalHold.next, next);
4547 return;
4548 }
4549 // A goal turn with no run to own it (Codex continued after the run
4550 // settled, or raced a Stop): stop it and pause the goal rather than
4551 // work out of sight.
4552 if (
4553 rootProviderThreads.has(payload.threadId) &&
4554 (goalsByNativeThread.get(payload.threadId)?.status === "active" ||
4555 goalStops.has(payload.threadId))
4556 ) {
4557 yield* Effect.logWarning("orchestration-v2.codex-goal-turn-without-run", {
4558 nativeThreadId: payload.threadId,
4559 nativeTurnId: payload.turn.id,
4560 });
4561 yield* client
4562 .request("thread/goal/set", { threadId: payload.threadId, status: "paused" })
4563 .pipe(
4564 Effect.catch((cause) =>
4565 Effect.logWarning("orchestration-v2.codex-goal-pause-failed", { cause }),
4566 ),
4567 Effect.andThen(
4568 client.request("turn/interrupt", {
4569 threadId: payload.threadId,
4570 turnId: payload.turn.id,
4571 }),
4572 ),
4573 Effect.catch((cause) =>
4574 Effect.logWarning("orchestration-v2.codex-goal-turn-stop-failed", { cause }),
4575 ),
4576 Effect.forkIn(scope),
4577 );
4578 return;
4579 }
4580 yield* rememberSubagentTurnStarted({
4581 nativeThreadId: payload.threadId,
4582 nativeTurnId: payload.turn.id,
4583 startedAt: codexTimestamp(payload.turn.startedAt),
4584 });
4585 }).pipe(Effect.orDie, turnTerminalizationPermit.withPermits(1)),
4586 );
4587
4588 // A goal that stops being active ends any run held open for its next turn.
4589 yield* client.handleServerNotification("thread/goal/updated", (payload) =>
4590 Effect.gen(function* () {
4591 goalsByNativeThread.set(payload.threadId, providerGoalFromCodex(payload.goal));
4592 yield* emitGoalUpdate(payload.threadId);
4593 if (payload.goal.status !== "active") yield* releaseGoalHold(payload.threadId);
4594 }).pipe(turnTerminalizationPermit.withPermits(1)),
4595 );
4596 yield* client.handleServerNotification("thread/goal/cleared", (payload) =>
4597 Effect.gen(function* () {
4598 goalsByNativeThread.set(payload.threadId, null);
4599 yield* emitGoalUpdate(payload.threadId);
4600 yield* releaseGoalHold(payload.threadId);
4601 }).pipe(turnTerminalizationPermit.withPermits(1)),
4602 );
4603
4604 yield* client.handleServerNotification("error", (payload) =>
4605 Effect.gen(function* () {
4606 const context = yield* awaitActiveTurn(payload.turnId);
4607 if (context === undefined) {
4608 return;
4609 }
4610 const notificationCode = codexErrorInfoCode(payload.error.codexErrorInfo);
4611 if (!payload.willRetry) {
4612 context.latestProviderFailure = {
4613 nativeMessage: payload.error.message,
4614 failure: makeProviderFailure({
4615 message: payload.error.additionalDetails?.trim() || payload.error.message,
4616 code: notificationCode,
4617 class:
4618 notificationCode === "usageLimitExceeded" ||
4619 notificationCode === "rateLimitExceeded"
4620 ? "usage_limit"
4621 : "provider_error",
4622 }),
4623 };
4624 return;
4625 }
4626 const updatedAt = yield* DateTime.now;
4627 const previous = (yield* Ref.get(providerRetries)).get(context.providerTurnId);
4628 const progress = parseCodexRetryProgress(payload.error.message);
4629 const retry: OrchestrationV2ProviderRetry = {
4630 attempt: progress?.attempt ?? (previous?.retry.attempt ?? 0) + 1,
4631 maxAttempts: progress?.maxAttempts ?? previous?.retry.maxAttempts ?? null,
4632 retryDelayMs: null,
4633 };
4634 const code = codexErrorInfoCode(payload.error.codexErrorInfo);
4635 const additionalDetails = payload.error.additionalDetails?.trim();
4636 const failure = makeProviderFailure({
4637 message:
4638 additionalDetails === undefined || additionalDetails.length === 0
4639 ? payload.error.message
4640 : additionalDetails,
4641 code,
4642 class:
4643 code === "usageLimitExceeded" || code === "rateLimitExceeded"
4644 ? "usage_limit"
4645 : code?.startsWith("http") === true || code?.startsWith("responseStream") === true
4646 ? "transport_error"
4647 : "provider_error",
4648 retryable: true,
4649 });
4650 const itemOrdinal =
4651 previous?.itemOrdinal ??
4652 (yield* resolveItemOrdinal(context, `terminal-failure:${context.providerTurnId}`));
4653 const state: ActiveCodexProviderRetry = {
4654 nativeMessage: payload.error.message,
4655 retry,
4656 failure,
4657 startedAt: previous?.startedAt ?? updatedAt,
4658 itemOrdinal,
4659 };
4660 yield* Ref.update(providerRetries, (current) => {
4661 const updated = new Map(current);
4662 updated.set(context.providerTurnId, state);
4663 return updated;
4664 });
4665 yield* emitProviderEvent({
4666 type: "turn_item.updated",
4667 driver: CODEX_PROVIDER,
4668 turnItem: makeProviderRetryTurnItem({
4669 driver: CODEX_PROVIDER,
4670 threadId: context.projectionThreadId,
4671 runId: context.projectionRunId,
4672 nodeId: context.providerNodeId,
4673 providerThreadId: context.providerThread.id,
4674 providerTurnId: context.providerTurnId,
4675 itemOrdinal,
4676 failure,
4677 retry,
4678 status: "running",
4679 startedAt: state.startedAt,
4680 updatedAt,
4681 }),
4682 });
4683 }).pipe(Effect.orDie),
4684 );
4685
4686 const emitCompactionItem = Effect.fn("CodexAdapterV2.emitCompactionItem")(function* (
4687 context: ActiveCodexTurnContext,
4688 nativeItemId: string,
4689 status: "running" | "completed",
4690 nativeStartedAt?: DateTime.Utc,
4691 ) {
4692 const now = yield* DateTime.now;
4693 const { ordinal, startedAt } = yield* resolveItemPosition(
4694 context,
4695 nativeItemId,
4696 nativeStartedAt,
4697 );
4698 yield* emitProviderEvent({
4699 type: "turn_item.updated",
4700 driver: CODEX_PROVIDER,
4701 turnItem: {
4702 id: idAllocator.derive.turnItemFromProviderItem({
4703 driver: CODEX_PROVIDER,
4704 nativeItemId,
4705 }),
4706 threadId: context.projectionThreadId,
4707 runId: context.projectionRunId,
4708 nodeId: context.providerNodeId,
4709 providerThreadId: context.providerThread.id,
4710 providerTurnId: context.providerTurnId,
4711 nativeItemRef: codexNativeItemRef(nativeItemId),
4712 parentItemId: null,
4713 ordinal,
4714 type: "compaction",
4715 driver: CODEX_PROVIDER,
4716 status,
4717 title: status === "completed" ? "Context compacted" : "Compacting context",
4718 startedAt,
4719 completedAt: status === "completed" ? now : null,
4720 updatedAt: now,
4721 },
4722 });
4723 });
4724
4725 yield* client.handleServerNotification("item/started", (payload) =>
4726 Effect.gen(function* () {
4727 const context = yield* awaitActiveTurn(payload.turnId);
4728 if (context === undefined) {
4729 return;
4730 }
4731
4732 if (payload.item.type === "contextCompaction") {
4733 yield* emitCompactionItem(
4734 context,
4735 payload.item.id,
4736 "running",
4737 DateTime.makeUnsafe(payload.startedAtMs),
4738 );
4739 return;
4740 }
4741
4742 if (payload.item.type === "userMessage") {
4743 yield* emitSubagentUserMessage(
4744 context,
4745 payload.item,
4746 DateTime.makeUnsafe(payload.startedAtMs),
4747 );
4748 return;
4749 }
4750
4751 if (payload.item.type === "subAgentActivity") {
4752 markSubagentUsage(context);
4753 yield* registerSubagentActivity({
4754 context,
4755 item: payload.item,
4756 });
4757 return;
4758 }
4759
4760 yield* completeProviderRetry(context, yield* DateTime.now);
4761 // Reasoning streams per part, so its parts take their own first-seen times.
4762 if (payload.item.type !== "reasoning") {
4763 yield* resolveItemPosition(
4764 context,
4765 payload.item.id,
4766 DateTime.makeUnsafe(payload.startedAtMs),
4767 );
4768 }
4769
4770 if (payload.item.type === "agentMessage") {
4771 if (payload.item.phase !== "commentary") {
4772 yield* Ref.update(finalAnswerItemIdsByTurn, (current) => {
4773 const updated = new Map(current);
4774 const itemIds = new Set(updated.get(payload.turnId) ?? []);
4775 itemIds.add(payload.item.id);
4776 updated.set(payload.turnId, itemIds);
4777 return updated;
4778 });
4779 }
4780 return;
4781 }
4782
4783 if (payload.item.type === "commandExecution") {
4784 if (!codexItemStatus(payload.item.status).completed) {
4785 yield* trackRunningCommandItem(payload.turnId, {
4786 id: payload.item.id,
4787 command: payload.item.command,
4788 ...(payload.item.aggregatedOutput === null ||
4789 payload.item.aggregatedOutput === undefined
4790 ? {}
4791 : { aggregatedOutput: payload.item.aggregatedOutput }),
4792 ...(typeof payload.item.processId === "string"
4793 ? { processId: payload.item.processId }
4794 : {}),
4795 });
4796 }
4797 const artifacts = yield* buildCommandExecutionArtifacts(context, payload.item);
4798 yield* emitProviderEvent({
4799 type: "node.updated",
4800 driver: CODEX_PROVIDER,
4801 node: artifacts.node,
4802 });
4803 yield* emitProviderEvent({
4804 type: "turn_item.updated",
4805 driver: CODEX_PROVIDER,
4806 turnItem: artifacts.turnItem,
4807 });
4808 return;
4809 }
4810
4811 if (payload.item.type === "mcpToolCall" || payload.item.type === "dynamicToolCall") {
4812 if (!codexItemStatus(payload.item.status).completed) {
4813 yield* trackRunningDynamicTool(payload.turnId, payload.item);
4814 }
4815 const artifacts = yield* buildDynamicToolArtifacts(context, payload.item);
4816 yield* emitProviderEvent({
4817 type: "node.updated",
4818 driver: CODEX_PROVIDER,
4819 node: artifacts.node,
4820 });
4821 yield* emitProviderEvent({
4822 type: "turn_item.updated",
4823 driver: CODEX_PROVIDER,
4824 turnItem: artifacts.turnItem,
4825 });
4826 return;
4827 }
4828
4829 if (payload.item.type !== "webSearch") {
4830 return;
4831 }
4832
4833 const artifacts = yield* buildWebSearchArtifacts({
4834 context,
4835 item: payload.item,
4836 completed: false,
4837 });
4838 yield* emitProviderEvent({
4839 type: "node.updated",
4840 driver: CODEX_PROVIDER,
4841 node: artifacts.node,
4842 });
4843 yield* emitProviderEvent({
4844 type: "turn_item.updated",
4845 driver: CODEX_PROVIDER,
4846 turnItem: artifacts.turnItem,
4847 });
4848 }).pipe(Effect.orDie, turnTerminalizationPermit.withPermits(1)),
4849 );
4850
4851 yield* client.handleServerNotification("item/completed", (payload) =>
4852 Effect.gen(function* () {
4853 if (payload.item.type === "contextCompaction")
4854 // The notification callback runs on the input reader. Let it read
4855 // the injection response while the session-scoped request waits.
4856 yield* restoreAdditionalContext(payload.threadId).pipe(
4857 Effect.forkIn(scope, { startImmediately: true }),
4858 );
4859 if (
4860 (payload.item.type === "subAgentActivity" ||
4861 payload.item.type === "collabAgentToolCall") &&
4862 (yield* Ref.get(terminalizedNonCompletedNativeTurns)).has(payload.turnId)
4863 )
4864 return;
4865 // Child lifecycle notifications outlive the parent's turn context.
4866 if (payload.item.type === "subAgentActivity" && payload.item.kind !== "started") {
4867 yield* registerSubagentActivity({ item: payload.item });
4868 return;
4869 }
4870 if (payload.item.type === "collabAgentToolCall" && payload.item.tool !== "spawnAgent") {
4871 yield* updateSubagentStates({ item: payload.item });
4872 return;
4873 }
4874 const resolved = yield* resolveItemEventContext(payload.turnId);
4875 if (resolved === undefined) {
4876 return;
4877 }
4878 const { context, settled } = resolved;
4879
4880 if (payload.item.type === "reasoning") {
4881 yield* completeReasoning(payload.turnId, payload.item);
4882 return;
4883 }
4884
4885 if (payload.item.type === "contextCompaction") {
4886 yield* emitCompactionItem(context, payload.item.id, "completed");
4887 return;
4888 }
4889
4890 if (payload.item.type === "userMessage") {
4891 if (yield* emitSubagentUserMessage(context, payload.item)) {
4892 return;
4893 }
4894 }
4895
4896 if (payload.item.type === "commandExecution") {
4897 const turnDrained = yield* clearRunningCommandItem(payload.turnId, payload.item.id);
4898 const artifacts = yield* buildCommandExecutionArtifacts(context, payload.item);
4899 yield* emitProviderEvent({
4900 type: "node.updated",
4901 driver: CODEX_PROVIDER,
4902 node: artifacts.node,
4903 });
4904 yield* emitProviderEvent({
4905 type: "turn_item.updated",
4906 driver: CODEX_PROVIDER,
4907 turnItem: artifacts.turnItem,
4908 });
4909 if (settled) {
4910 if (
4911 context.subagent === null &&
4912 continuationRequests !== undefined &&
4913 !(yield* Ref.get(interruptingNativeTurns)).has(payload.turnId)
4914 ) {
4915 const alreadyOffered = yield* Ref.modify(
4916 offeredContinuationItemsByTurn,
4917 (current) => {
4918 const items = current.get(payload.turnId);
4919 if (items !== undefined && items.has(payload.item.id)) {
4920 return [true, current] as const;
4921 }
4922 const updated = new Map(current);
4923 const updatedItems = new Set(items ?? []);
4924 updatedItems.add(payload.item.id);
4925 updated.set(payload.turnId, updatedItems);
4926 return [false, updated] as const;
4927 },
4928 );
4929 if (!alreadyOffered) {
4930 yield* continuationRequests.offer({
4931 threadId: context.projectionThreadId,
4932 providerThreadId: context.providerThread.id,
4933 driver: CODEX_PROVIDER,
4934 detail: codexBackgroundCommandDetail(payload.item),
4935 notification: {
4936 ...backgroundWorkNotification([
4937 {
4938 kind: "command",
4939 label: payload.item.command,
4940 outcome:
4941 payload.item.exitCode === 0
4942 ? "completed"
4943 : payload.item.exitCode == null
4944 ? "unknown"
4945 : "failed",
4946 exitCode: payload.item.exitCode ?? undefined,
4947 },
4948 ]),
4949 detail: payload.item.command,
4950 },
4951 });
4952 }
4953 }
4954 if (turnDrained) {
4955 yield* releaseSettledTurnIfIdle(payload.turnId);
4956 }
4957 }
4958 return;
4959 }
4960
4961 if (payload.item.type === "mcpToolCall" || payload.item.type === "dynamicToolCall") {
4962 yield* clearRunningDynamicTool(payload.turnId, payload.item.id);
4963 const appResourceUri = codexMcpAppResourceUri(payload.item);
4964 if (appResourceUri !== undefined && payload.item.type === "mcpToolCall") {
4965 yield* captureMcpApp(context, payload.turnId, payload.item, appResourceUri);
4966 return;
4967 }
4968 const artifacts = yield* buildDynamicToolArtifacts(context, payload.item);
4969 yield* emitProviderEvent({
4970 type: "node.updated",
4971 driver: CODEX_PROVIDER,
4972 node: artifacts.node,
4973 });
4974 yield* emitProviderEvent({
4975 type: "turn_item.updated",
4976 driver: CODEX_PROVIDER,
4977 turnItem: artifacts.turnItem,
4978 });
4979 if (settled) {
4980 yield* releaseSettledTurnIfIdle(payload.turnId);
4981 }
4982 return;
4983 }
4984
4985 if (payload.item.type === "fileChange") {
4986 const artifacts = yield* buildFileChangeArtifacts(context, payload.item);
4987 if (artifacts === null) {
4988 return;
4989 }
4990 yield* emitProviderEvent({
4991 type: "node.updated",
4992 driver: CODEX_PROVIDER,
4993 node: artifacts.node,
4994 });
4995 yield* emitProviderEvent({
4996 type: "turn_item.updated",
4997 driver: CODEX_PROVIDER,
4998 turnItem: artifacts.turnItem,
4999 });
5000 return;
5001 }
5002
5003 if (payload.item.type === "webSearch") {
5004 const artifacts = yield* buildWebSearchArtifacts({
5005 context,
5006 item: payload.item,
5007 completed: true,
5008 });
5009 yield* emitProviderEvent({
5010 type: "node.updated",
5011 driver: CODEX_PROVIDER,
5012 node: artifacts.node,
5013 });
5014 yield* emitProviderEvent({
5015 type: "turn_item.updated",
5016 driver: CODEX_PROVIDER,
5017 turnItem: artifacts.turnItem,
5018 });
5019 return;
5020 }
5021
5022 if (payload.item.type === "plan") {
5023 yield* planDeltas.complete({
5024 turnId: payload.turnId,
5025 itemId: payload.item.id,
5026 ...(payload.item.text.length > 0 ? { finalText: payload.item.text } : {}),
5027 });
5028 return;
5029 }
5030
5031 if (payload.item.type === "collabAgentToolCall") {
5032 if (payload.item.tool === "spawnAgent") markSubagentUsage(context);
5033 yield* registerSubagentThreads({
5034 context,
5035 item: payload.item,
5036 });
5037 yield* updateSubagentStates({
5038 item: payload.item,
5039 });
5040 return;
5041 }
5042
5043 if (payload.item.type === "subAgentActivity") {
5044 markSubagentUsage(context);
5045 yield* registerSubagentActivity({
5046 context,
5047 item: payload.item,
5048 });
5049 return;
5050 }
5051
5052 if (payload.item.type !== "agentMessage") {
5053 return;
5054 }
5055
5056 if (payload.item.delivery === "async" && payload.item.questions?.length) {
5057 const artifacts = yield* buildUserInputRequestArtifacts({
5058 context,
5059 nativeItemId: payload.item.id,
5060 nativeRequestId: `async:${payload.item.id}`,
5061 responseMode: "message",
5062 questions: payload.item.questions.map((question, index) => ({
5063 id: String(index),
5064 header: "Question",
5065 question: question.title,
5066 options: (question.options ?? []).map((label) => ({ label, description: "" })),
5067 })),
5068 });
5069 yield* emitProviderEvent({
5070 type: "node.updated",
5071 driver: CODEX_PROVIDER,
5072 node: artifacts.node,
5073 });
5074 yield* emitProviderEvent({
5075 type: "turn_item.updated",
5076 driver: CODEX_PROVIDER,
5077 turnItem: artifacts.turnItem,
5078 });
5079 yield* emitProviderEvent({
5080 type: "runtime_request.updated",
5081 driver: CODEX_PROVIDER,
5082 threadId: artifacts.node.threadId,
5083 runtimeRequest: artifacts.request,
5084 });
5085 yield* Ref.update(finalAnswerItemIdsByTurn, (current) => {
5086 const ids = current.get(payload.turnId);
5087 if (!ids?.has(payload.item.id)) return current;
5088 const next = new Map(current);
5089 const remaining = new Set(ids);
5090 remaining.delete(payload.item.id);
5091 if (remaining.size === 0) next.delete(payload.turnId);
5092 else next.set(payload.turnId, remaining);
5093 return next;
5094 });
5095 return;
5096 }
5097
5098 const finalAnswer = payload.item.phase !== "commentary";
5099 if (finalAnswer) {
5100 yield* Ref.update(finalAnswerItemIdsByTurn, (current) => {
5101 const updated = new Map(current);
5102 const itemIds = new Set(updated.get(payload.turnId) ?? []);
5103 itemIds.add(payload.item.id);
5104 updated.set(payload.turnId, itemIds);
5105 return updated;
5106 });
5107 }
5108 const completedTextsBefore =
5109 (yield* Ref.get(completedFinalAnswerTextsByTurn)).get(payload.turnId) ??
5110 new Set<string>();
5111 const text = yield* agentMessageDeltas.complete({
5112 turnId: payload.turnId,
5113 itemId: payload.item.id,
5114 finalText: payload.item.text,
5115 });
5116 yield* Ref.update(finalAnswerItemIdsByTurn, (current) => {
5117 const itemIds = current.get(payload.turnId);
5118 if (itemIds === undefined || !itemIds.has(payload.item.id)) {
5119 return current;
5120 }
5121 const updated = new Map(current);
5122 const remainingItemIds = new Set(itemIds);
5123 remainingItemIds.delete(payload.item.id);
5124 if (remainingItemIds.size === 0) {
5125 updated.delete(payload.turnId);
5126 } else {
5127 updated.set(payload.turnId, remainingItemIds);
5128 }
5129 return updated;
5130 });
5131 const emitted =
5132 !finalAnswer ||
5133 completedTextsBefore.size === 0 ||
5134 (text.length > 0 && !completedTextsBefore.has(text));
5135 if (emitted && context.subagent !== null && finalAnswer) {
5136 yield* emitSubagentTaskUpdate({
5137 subagent: context.subagent,
5138 status: context.subagent.task.status,
5139 result: text,
5140 });
5141 }
5142 }).pipe(Effect.orDie, turnTerminalizationPermit.withPermits(1)),
5143 );
5144
5145 yield* client.handleServerRequest("item/commandExecution/requestApproval", (payload) =>
5146 Effect.gen(function* () {
5147 const context = yield* awaitActiveTurn(payload.turnId);
5148 if (context === undefined) {
5149 return yield* toProtocolError(
5150 `No active Codex turn context for approval turn ${payload.turnId}.`,
5151 payload,
5152 );
5153 }
5154 const allowance = sessionAllowance(payload.threadId, context);
5155 if (allowance.has("command")) {
5156 return {
5157 decision: "acceptForSession",
5158 } satisfies CodexSchema.CommandExecutionRequestApprovalResponse;
5159 }
5160
5161 const nativeRequestId = payload.approvalId ?? payload.itemId;
5162 const artifacts = yield* buildApprovalRequestArtifacts({
5163 context,
5164 nativeItemId: payload.itemId,
5165 nativeRequestId,
5166 requestKind: "command",
5167 ...((payload.reason ?? payload.command) === undefined
5168 ? {}
5169 : { prompt: payload.reason ?? payload.command }),
5170 });
5171 const decision = yield* Deferred.make<ProviderApprovalDecision, never>();
5172 yield* Ref.update(pendingRuntimeRequests, (current) => {
5173 const updated = new Map(current);
5174 updated.set(String(artifacts.request.id), {
5175 type: "approval",
5176 requestId: artifacts.request.id,
5177 requestKind: "command",
5178 decision,
5179 });
5180 return updated;
5181 });
5182 yield* emitProviderEvent({
5183 type: "node.updated",
5184 driver: CODEX_PROVIDER,
5185 node: artifacts.node,
5186 });
5187 yield* emitProviderEvent({
5188 type: "turn_item.updated",
5189 driver: CODEX_PROVIDER,
5190 turnItem: artifacts.turnItem,
5191 });
5192 yield* emitProviderEvent({
5193 type: "runtime_request.updated",
5194 driver: CODEX_PROVIDER,
5195 threadId: artifacts.node.threadId,
5196 runtimeRequest: artifacts.request,
5197 });
5198
5199 const resolved = yield* Deferred.await(decision).pipe(
5200 Effect.ensuring(
5201 Ref.update(pendingRuntimeRequests, (current) => {
5202 const updated = new Map(current);
5203 updated.delete(String(artifacts.request.id));
5204 return updated;
5205 }),
5206 ),
5207 );
5208 if (resolved === "acceptForSession" || resolved === "acceptAlways") {
5209 allowance.add("command");
5210 }
5211 return {
5212 decision: resolved === "acceptAlways" ? "acceptForSession" : resolved,
5213 } satisfies CodexSchema.CommandExecutionRequestApprovalResponse;
5214 }).pipe(Effect.orDie),
5215 );
5216
5217 yield* client.handleServerRequest("item/fileChange/requestApproval", (payload) =>
5218 Effect.gen(function* () {
5219 const context = yield* awaitActiveTurn(payload.turnId);
5220 if (context === undefined) {
5221 return yield* toProtocolError(
5222 `No active Codex turn context for file change approval turn ${payload.turnId}.`,
5223 payload,
5224 );
5225 }
5226 const allowance = sessionAllowance(payload.threadId, context);
5227 if (allowance.has("file-change")) {
5228 return {
5229 decision: "acceptForSession",
5230 } satisfies CodexSchema.FileChangeRequestApprovalResponse;
5231 }
5232
5233 const artifacts = yield* buildApprovalRequestArtifacts({
5234 context,
5235 nativeItemId: payload.itemId,
5236 nativeRequestId: payload.itemId,
5237 requestKind: "file-change",
5238 prompt: codexFileChangeApprovalPrompt(payload) ?? null,
5239 });
5240 const decision = yield* Deferred.make<ProviderApprovalDecision, never>();
5241 yield* Ref.update(pendingRuntimeRequests, (current) => {
5242 const updated = new Map(current);
5243 updated.set(String(artifacts.request.id), {
5244 type: "approval",
5245 requestId: artifacts.request.id,
5246 requestKind: "file-change",
5247 decision,
5248 });
5249 return updated;
5250 });
5251 yield* emitProviderEvent({
5252 type: "node.updated",
5253 driver: CODEX_PROVIDER,
5254 node: artifacts.node,
5255 });
5256 yield* emitProviderEvent({
5257 type: "turn_item.updated",
5258 driver: CODEX_PROVIDER,
5259 turnItem: artifacts.turnItem,
5260 });
5261 yield* emitProviderEvent({
5262 type: "runtime_request.updated",
5263 driver: CODEX_PROVIDER,
5264 threadId: artifacts.node.threadId,
5265 runtimeRequest: artifacts.request,
5266 });
5267
5268 const resolved = yield* Deferred.await(decision).pipe(
5269 Effect.ensuring(
5270 Ref.update(pendingRuntimeRequests, (current) => {
5271 const updated = new Map(current);
5272 updated.delete(String(artifacts.request.id));
5273 return updated;
5274 }),
5275 ),
5276 );
5277 if (resolved === "acceptForSession" || resolved === "acceptAlways") {
5278 allowance.add("file-change");
5279 }
5280 return {
5281 decision: resolved === "acceptAlways" ? "acceptForSession" : resolved,
5282 } satisfies CodexSchema.FileChangeRequestApprovalResponse;
5283 }).pipe(Effect.orDie),
5284 );
5285
5286 yield* client.handleServerRequest("item/permissions/requestApproval", (payload) =>
5287 Effect.gen(function* () {
5288 const context = yield* awaitActiveTurn(payload.turnId);
5289 if (context === undefined) {
5290 return yield* toProtocolError(
5291 `No active Codex turn context for permissions approval turn ${payload.turnId}.`,
5292 payload,
5293 );
5294 }
5295
5296 const requestKind = providerRequestKindFromPermissions(payload.permissions);
5297 const artifacts = yield* buildApprovalRequestArtifacts({
5298 context,
5299 nativeItemId: payload.itemId,
5300 nativeRequestId: payload.itemId,
5301 requestKind,
5302 ...(payload.reason === undefined ? {} : { prompt: payload.reason }),
5303 });
5304 const decision = yield* Deferred.make<ProviderApprovalDecision, never>();
5305 yield* Ref.update(pendingRuntimeRequests, (current) => {
5306 const updated = new Map(current);
5307 updated.set(String(artifacts.request.id), {
5308 type: "approval",
5309 requestId: artifacts.request.id,
5310 requestKind,
5311 decision,
5312 });
5313 return updated;
5314 });
5315 yield* emitProviderEvent({
5316 type: "node.updated",
5317 driver: CODEX_PROVIDER,
5318 node: artifacts.node,
5319 });
5320 yield* emitProviderEvent({
5321 type: "turn_item.updated",
5322 driver: CODEX_PROVIDER,
5323 turnItem: artifacts.turnItem,
5324 });
5325 yield* emitProviderEvent({
5326 type: "runtime_request.updated",
5327 driver: CODEX_PROVIDER,
5328 threadId: artifacts.node.threadId,
5329 runtimeRequest: artifacts.request,
5330 });
5331
5332 const resolved = yield* Deferred.await(decision).pipe(
5333 Effect.ensuring(
5334 Ref.update(pendingRuntimeRequests, (current) => {
5335 const updated = new Map(current);
5336 updated.delete(String(artifacts.request.id));
5337 return updated;
5338 }),
5339 ),
5340 );
5341 return permissionsResponseFromDecision({
5342 decision: resolved,
5343 permissions: payload.permissions,
5344 });
5345 }).pipe(Effect.orDie),
5346 );
5347
5348 yield* client.handleServerRequest("mcpServer/elicitation/request", (payload) =>
5349 Effect.gen(function* () {
5350 // Unsupported elicitation shapes cannot express an approval, so
5351 // decline instead of presenting a request the user cannot answer.
5352 if (toMcpElicitationResponse(payload, "accept").action !== "accept") {
5353 yield* Effect.logWarning("Declined an unsupported MCP elicitation.", {
5354 serverName: payload.serverName,
5355 mode: payload.mode,
5356 });
5357 return {
5358 action: "decline",
5359 } satisfies CodexSchema.McpServerElicitationRequestResponse;
5360 }
5361 const context =
5362 payload.turnId === undefined || payload.turnId === null
5363 ? undefined
5364 : yield* awaitActiveTurn(payload.turnId);
5365 if (context === undefined) {
5366 yield* Effect.logWarning(
5367 "Declined an MCP elicitation without an active Codex turn context.",
5368 { serverName: payload.serverName },
5369 );
5370 return {
5371 action: "decline",
5372 } satisfies CodexSchema.McpServerElicitationRequestResponse;
5373 }
5374
5375 const nativeRequestId =
5376 payload.mode === "url"
5377 ? payload.elicitationId
5378 : `mcp-elicitation:${payload.serverName}`;
5379 const described = describeMcpElicitation(payload);
5380 const artifacts = yield* buildApprovalRequestArtifacts({
5381 context,
5382 nativeItemId: nativeRequestId,
5383 nativeRequestId,
5384 requestKind: "mcp-elicitation",
5385 prompt: payload.message,
5386 appName: described.appName,
5387 options: described.options,
5388 });
5389 const decision = yield* Deferred.make<ProviderApprovalDecision, never>();
5390 yield* Ref.update(pendingRuntimeRequests, (current) => {
5391 const updated = new Map(current);
5392 updated.set(String(artifacts.request.id), {
5393 type: "approval",
5394 requestId: artifacts.request.id,
5395 requestKind: "mcp-elicitation",
5396 decision,
5397 });
5398 return updated;
5399 });
5400 yield* emitProviderEvent({
5401 type: "node.updated",
5402 driver: CODEX_PROVIDER,
5403 node: artifacts.node,
5404 });
5405 yield* emitProviderEvent({
5406 type: "turn_item.updated",
5407 driver: CODEX_PROVIDER,
5408 turnItem: artifacts.turnItem,
5409 });
5410 yield* emitProviderEvent({
5411 type: "runtime_request.updated",
5412 driver: CODEX_PROVIDER,
5413 threadId: artifacts.node.threadId,
5414 runtimeRequest: artifacts.request,
5415 });
5416
5417 const resolved = yield* Deferred.await(decision).pipe(
5418 Effect.ensuring(
5419 Ref.update(pendingRuntimeRequests, (current) => {
5420 const updated = new Map(current);
5421 updated.delete(String(artifacts.request.id));
5422 return updated;
5423 }),
5424 ),
5425 );
5426 return toMcpElicitationResponse(payload, resolved);
5427 }).pipe(Effect.orDie),
5428 );
5429
5430 yield* client.handleServerRequest("execCommandApproval", (payload) =>
5431 Effect.gen(function* () {
5432 const context = yield* findActiveTurnByNativeThreadId(payload.conversationId);
5433 if (context === undefined) {
5434 return yield* toProtocolError(
5435 `No active Codex turn context for exec approval thread ${payload.conversationId}.`,
5436 payload,
5437 );
5438 }
5439 const allowance = sessionAllowance(payload.conversationId, context);
5440 if (allowance.has("command")) {
5441 return {
5442 decision: "approved_for_session",
5443 } satisfies CodexSchema.ExecCommandApprovalResponse;
5444 }
5445
5446 const nativeRequestId = payload.approvalId ?? payload.callId;
5447 const artifacts = yield* buildApprovalRequestArtifacts({
5448 context,
5449 nativeItemId: payload.callId,
5450 nativeRequestId,
5451 requestKind: "command",
5452 prompt: payload.reason ?? payload.command.join(" "),
5453 });
5454 const decision = yield* Deferred.make<ProviderApprovalDecision, never>();
5455 yield* Ref.update(pendingRuntimeRequests, (current) => {
5456 const updated = new Map(current);
5457 updated.set(String(artifacts.request.id), {
5458 type: "approval",
5459 requestId: artifacts.request.id,
5460 requestKind: "command",
5461 decision,
5462 });
5463 return updated;
5464 });
5465 yield* emitProviderEvent({
5466 type: "node.updated",
5467 driver: CODEX_PROVIDER,
5468 node: artifacts.node,
5469 });
5470 yield* emitProviderEvent({
5471 type: "turn_item.updated",
5472 driver: CODEX_PROVIDER,
5473 turnItem: artifacts.turnItem,
5474 });
5475 yield* emitProviderEvent({
5476 type: "runtime_request.updated",
5477 driver: CODEX_PROVIDER,
5478 threadId: artifacts.node.threadId,
5479 runtimeRequest: artifacts.request,
5480 });
5481
5482 const resolved = yield* Deferred.await(decision).pipe(
5483 Effect.ensuring(
5484 Ref.update(pendingRuntimeRequests, (current) => {
5485 const updated = new Map(current);
5486 updated.delete(String(artifacts.request.id));
5487 return updated;
5488 }),
5489 ),
5490 );
5491 if (resolved === "acceptForSession" || resolved === "acceptAlways") {
5492 allowance.add("command");
5493 }
5494 return {
5495 decision: approvalDecisionToLegacyReviewDecision(resolved),
5496 } satisfies CodexSchema.ExecCommandApprovalResponse;
5497 }).pipe(Effect.orDie),
5498 );
5499
5500 yield* client.handleServerRequest("applyPatchApproval", (payload) =>
5501 Effect.gen(function* () {
5502 const context = yield* findActiveTurnByNativeThreadId(payload.conversationId);
5503 if (context === undefined) {
5504 return yield* toProtocolError(
5505 `No active Codex turn context for apply patch approval thread ${payload.conversationId}.`,
5506 payload,
5507 );
5508 }
5509 const allowance = sessionAllowance(payload.conversationId, context);
5510 if (allowance.has("file-change")) {
5511 return {
5512 decision: "approved_for_session",
5513 } satisfies CodexSchema.ApplyPatchApprovalResponse;
5514 }
5515
5516 const artifacts = yield* buildApprovalRequestArtifacts({
5517 context,
5518 nativeItemId: payload.callId,
5519 nativeRequestId: payload.callId,
5520 requestKind: "file-change",
5521 prompt: codexFileChangeApprovalPrompt(payload) ?? null,
5522 });
5523 const decision = yield* Deferred.make<ProviderApprovalDecision, never>();
5524 yield* Ref.update(pendingRuntimeRequests, (current) => {
5525 const updated = new Map(current);
5526 updated.set(String(artifacts.request.id), {
5527 type: "approval",
5528 requestId: artifacts.request.id,
5529 requestKind: "file-change",
5530 decision,
5531 });
5532 return updated;
5533 });
5534 yield* emitProviderEvent({
5535 type: "node.updated",
5536 driver: CODEX_PROVIDER,
5537 node: artifacts.node,
5538 });
5539 yield* emitProviderEvent({
5540 type: "turn_item.updated",
5541 driver: CODEX_PROVIDER,
5542 turnItem: artifacts.turnItem,
5543 });
5544 yield* emitProviderEvent({
5545 type: "runtime_request.updated",
5546 driver: CODEX_PROVIDER,
5547 threadId: artifacts.node.threadId,
5548 runtimeRequest: artifacts.request,
5549 });
5550
5551 const resolved = yield* Deferred.await(decision).pipe(
5552 Effect.ensuring(
5553 Ref.update(pendingRuntimeRequests, (current) => {
5554 const updated = new Map(current);
5555 updated.delete(String(artifacts.request.id));
5556 return updated;
5557 }),
5558 ),
5559 );
5560 if (resolved === "acceptForSession" || resolved === "acceptAlways") {
5561 allowance.add("file-change");
5562 }
5563 return {
5564 decision: approvalDecisionToLegacyReviewDecision(resolved),
5565 } satisfies CodexSchema.ApplyPatchApprovalResponse;
5566 }).pipe(Effect.orDie),
5567 );
5568
5569 yield* client.handleServerRequest("item/tool/requestUserInput", (payload) =>
5570 Effect.gen(function* () {
5571 const context = yield* awaitActiveTurn(payload.turnId);
5572 if (context === undefined) {
5573 return yield* toProtocolError(
5574 `No active Codex turn context for user input request turn ${payload.turnId}.`,
5575 payload,
5576 );
5577 }
5578
5579 const artifacts = yield* buildUserInputRequestArtifacts({
5580 context,
5581 nativeItemId: payload.itemId,
5582 nativeRequestId: payload.itemId,
5583 questions: payload.questions,
5584 });
5585 const answers = yield* Deferred.make<ProviderUserInputAnswers, never>();
5586 yield* Ref.update(pendingRuntimeRequests, (current) => {
5587 const updated = new Map(current);
5588 updated.set(String(artifacts.request.id), {
5589 type: "user_input",
5590 requestId: artifacts.request.id,
5591 answers,
5592 });
5593 return updated;
5594 });
5595 yield* emitProviderEvent({
5596 type: "node.updated",
5597 driver: CODEX_PROVIDER,
5598 node: artifacts.node,
5599 });
5600 yield* emitProviderEvent({
5601 type: "turn_item.updated",
5602 driver: CODEX_PROVIDER,
5603 turnItem: artifacts.turnItem,
5604 });
5605 yield* emitProviderEvent({
5606 type: "runtime_request.updated",
5607 driver: CODEX_PROVIDER,
5608 threadId: artifacts.node.threadId,
5609 runtimeRequest: artifacts.request,
5610 });
5611
5612 const resolved = yield* Deferred.await(answers).pipe(
5613 Effect.ensuring(
5614 Ref.update(pendingRuntimeRequests, (current) => {
5615 const updated = new Map(current);
5616 updated.delete(String(artifacts.request.id));
5617 return updated;
5618 }),
5619 ),
5620 );
5621 return {
5622 answers: toCodexUserInputAnswers(
5623 resolved,
5624 new Set(payload.questions.map((question) => question.id)),
5625 ),
5626 } satisfies CodexSchema.ToolRequestUserInputResponse;
5627 }).pipe(Effect.orDie),
5628 );
5629
5630 const makeRootTerminalEvent = Effect.fn("CodexAdapterV2.makeRootTerminalEvent")(
5631 function* (input: {
5632 readonly context: ActiveCodexTurnContext;
5633 readonly status: OrchestrationV2ProviderTurn["status"];
5634 readonly failureMessage?: string;
5635 readonly failureCode?: string | null;
5636 readonly providerRetry?: ActiveCodexProviderRetry;
5637 }): Effect.fn.Return<CodexRootTerminalEvent> {
5638 const terminalStatus = providerTurnStatusToTerminal(input.status);
5639 if (terminalStatus === "failed") {
5640 const previousFailure = input.context.latestProviderFailure ?? input.providerRetry;
5641 const failure =
5642 previousFailure !== undefined &&
5643 (input.failureMessage === undefined ||
5644 input.failureMessage === previousFailure.nativeMessage) &&
5645 (input.failureCode === undefined ||
5646 input.failureCode === previousFailure.failure.code)
5647 ? previousFailure.failure
5648 : makeProviderFailure({
5649 message: input.failureMessage,
5650 code: input.failureCode,
5651 class:
5652 input.failureCode === "usageLimitExceeded" ||
5653 input.failureCode === "rateLimitExceeded"
5654 ? "usage_limit"
5655 : "provider_error",
5656 });
5657 return {
5658 type: "turn.terminal",
5659 driver: CODEX_PROVIDER,
5660 providerThreadId: input.context.providerThread.id,
5661 providerTurnId: input.context.providerTurnId,
5662 runOrdinal: input.context.input.runOrdinal,
5663 failureItemOrdinal: yield* resolveItemOrdinal(
5664 input.context,
5665 `terminal-failure:${input.context.providerTurnId}`,
5666 ),
5667 status: terminalStatus,
5668 failure:
5669 failure.class === "usage_limit"
5670 ? {
5671 ...failure,
5672 resetAt: codexUsageLimitResetAt(yield* Ref.get(rateLimitSnapshot)),
5673 }
5674 : failure,
5675 ...(input.providerRetry === undefined
5676 ? {}
5677 : {
5678 retry: input.providerRetry.retry,
5679 retryStartedAt: input.providerRetry.startedAt,
5680 }),
5681 threadDisposition: "reusable",
5682 };
5683 }
5684 return {
5685 type: "turn.terminal",
5686 driver: CODEX_PROVIDER,
5687 providerThreadId: input.context.providerThread.id,
5688 providerTurnId: input.context.providerTurnId,
5689 runOrdinal: input.context.input.runOrdinal,
5690 status: terminalStatus,
5691 failure: null,
5692 threadDisposition: "reusable",
5693 };
5694 },
5695 );
5696
5697 const emitRootTerminal = Effect.fnUntraced(function* (
5698 context: ActiveCodexTurnContext,
5699 event: CodexRootTerminalEvent,
5700 ) {
5701 const current =
5702 event.status === "failed" && event.failure.class === "usage_limit"
5703 ? {
5704 ...event,
5705 failure: {
5706 ...event.failure,
5707 resetAt:
5708 event.failure.resetAt ??
5709 codexUsageLimitResetAt(yield* Ref.get(rateLimitSnapshot)),
5710 },
5711 }
5712 : event;
5713 yield* emitProviderEvent(current);
5714 for (const turn of goalRuns.get(context.providerTurnId) ?? []) {
5715 goalRuns.delete(turn.providerTurnId);
5716 }
5717 // Later goal updates must not mark the settled thread active again.
5718 const nativeThreadId = context.providerThread.nativeThreadRef?.nativeId;
5719 const rootProviderThread =
5720 nativeThreadId == null ? undefined : rootProviderThreads.get(nativeThreadId);
5721 if (nativeThreadId != null && rootProviderThread !== undefined) {
5722 rootProviderThreads.set(nativeThreadId, { ...rootProviderThread, status: "idle" });
5723 }
5724 if (current.status === "failed" && current.failure.class === "usage_limit") {
5725 const item = makeProviderFailureTurnItem({
5726 driver: CODEX_PROVIDER,
5727 threadId: context.input.threadId,
5728 runId: context.input.runId,
5729 nodeId: context.input.rootNodeId,
5730 providerThreadId: current.providerThreadId,
5731 providerTurnId: current.providerTurnId,
5732 itemOrdinal: current.failureItemOrdinal,
5733 failure: current.failure,
5734 occurredAt: yield* DateTime.now,
5735 });
5736 yield* Ref.update(limitedTurnItems, (items) =>
5737 new Map(items).set(context.providerThread.id, item),
5738 );
5739 }
5740 });
5741
5742 const emitOrDeferRootTerminal = Effect.fn("CodexAdapterV2.emitOrDeferRootTerminal")(
5743 function* (input: {
5744 readonly context: ActiveCodexTurnContext;
5745 readonly nativeTurnId: string;
5746 readonly status: OrchestrationV2ProviderTurn["status"];
5747 readonly failureMessage?: string;
5748 readonly failureCode?: string | null;
5749 readonly providerRetry?: ActiveCodexProviderRetry;
5750 readonly goalHoldTurn?: OrchestrationV2ProviderTurn;
5751 }) {
5752 const event = yield* makeRootTerminalEvent(input);
5753 const hasActiveDescendants = Array.from((yield* Ref.get(activeTurns)).values()).some(
5754 (candidate) => isDescendantCodexTurn(candidate, input.context),
5755 );
5756 if (event.status !== "completed" && hasActiveDescendants) {
5757 yield* Ref.update(deferredRootTerminals, (current) => {
5758 const updated = new Map(current);
5759 updated.set(input.nativeTurnId, { context: input.context, event });
5760 return updated;
5761 });
5762 return;
5763 }
5764 const nativeThreadId = input.context.providerThread.nativeThreadRef?.nativeId;
5765 if (
5766 input.goalHoldTurn !== undefined &&
5767 event.status === "completed" &&
5768 nativeThreadId != null
5769 ) {
5770 goalHolds.set(nativeThreadId, {
5771 context: input.context,
5772 event,
5773 completedTurn: input.goalHoldTurn,
5774 next: yield* Deferred.make<ActiveCodexTurnContext | undefined>(),
5775 });
5776 yield* Effect.sleep(CODEX_GOAL_CONTINUATION_GRACE).pipe(
5777 Effect.andThen(
5778 turnTerminalizationPermit.withPermits(1)(releaseGoalHold(nativeThreadId, event)),
5779 ),
5780 Effect.forkIn(scope),
5781 );
5782 return;
5783 }
5784 yield* emitRootTerminal(input.context, event);
5785 },
5786 );
5787
5788 /**
5789 * Settles a held goal run with its last turn. Call with the
5790 * terminalization permit, after removing the hold from `goalHolds`.
5791 */
5792 const settleGoalHold = Effect.fnUntraced(function* (
5793 hold: CodexGoalHold,
5794 status: "completed" | "interrupted",
5795 ) {
5796 yield* emitProviderEvent({
5797 type: "provider_turn.updated",
5798 driver: CODEX_PROVIDER,
5799 threadId: hold.context.projectionThreadId,
5800 providerTurn: hold.completedTurn,
5801 });
5802 yield* emitRootTerminal(hold.context, { ...hold.event, status });
5803 yield* Deferred.succeed(hold.next, undefined);
5804 });
5805
5806 /** Settles a held goal run unless Stop owns it or a newer hold replaced it. */
5807 const releaseGoalHold = Effect.fnUntraced(function* (
5808 nativeThreadId: string,
5809 expected?: CodexRootTerminalEvent,
5810 ) {
5811 const hold = goalHolds.get(nativeThreadId);
5812 if (hold === undefined || goalStops.has(nativeThreadId)) return;
5813 if (expected !== undefined && hold.event !== expected) return;
5814 goalHolds.delete(nativeThreadId);
5815 yield* settleGoalHold(hold, "completed");
5816 });
5817
5818 const flushReadyRootTerminals = Effect.fn("CodexAdapterV2.flushReadyRootTerminals")(
5819 function* () {
5820 const activeTurnContexts = Array.from((yield* Ref.get(activeTurns)).values());
5821 const readyEvents = yield* Ref.modify(deferredRootTerminals, (current) => {
5822 const updated = new Map(current);
5823 const ready: Array<{
5824 context: ActiveCodexTurnContext;
5825 event: CodexRootTerminalEvent;
5826 }> = [];
5827 for (const [nativeTurnId, deferred] of current) {
5828 if (
5829 !activeTurnContexts.some((candidate) =>
5830 isDescendantCodexTurn(candidate, deferred.context),
5831 )
5832 ) {
5833 updated.delete(nativeTurnId);
5834 ready.push(deferred);
5835 }
5836 }
5837 return [ready, updated] as const;
5838 });
5839 for (const ready of readyEvents) {
5840 yield* emitRootTerminal(ready.context, ready.event);
5841 }
5842 },
5843 );
5844
5845 const finalizeCodexTurn = (input: {
5846 readonly context: ActiveCodexTurnContext;
5847 readonly nativeTurnId: string;
5848 readonly status: OrchestrationV2ProviderTurn["status"];
5849 readonly completedAt: DateTime.Utc;
5850 readonly failureMessage?: string;
5851 readonly failureCode?: string | null;
5852 }) =>
5853 turnTerminalizationPermit.withPermits(1)(
5854 Effect.gen(function* () {
5855 const current = (yield* Ref.get(activeTurns)).get(input.nativeTurnId);
5856 if (current !== input.context) {
5857 return false;
5858 }
5859 const providerRetry = yield* Ref.modify(providerRetries, (current) => {
5860 const retry = current.get(input.context.providerTurnId);
5861 if (retry === undefined) {
5862 return [undefined, current] as const;
5863 }
5864 const updated = new Map(current);
5865 updated.delete(input.context.providerTurnId);
5866 return [retry, updated] as const;
5867 });
5868 if (
5869 providerRetry !== undefined &&
5870 (input.status !== "failed" || input.context.subagent !== null)
5871 ) {
5872 yield* emitProviderEvent({
5873 type: "turn_item.updated",
5874 driver: CODEX_PROVIDER,
5875 turnItem: makeProviderRetryTurnItem({
5876 driver: CODEX_PROVIDER,
5877 threadId: input.context.projectionThreadId,
5878 runId: input.context.projectionRunId,
5879 nodeId: input.context.providerNodeId,
5880 providerThreadId: input.context.providerThread.id,
5881 providerTurnId: input.context.providerTurnId,
5882 itemOrdinal: providerRetry.itemOrdinal,
5883 failure: providerRetry.failure,
5884 retry: providerRetry.retry,
5885 status: providerTurnStatusToTerminal(input.status),
5886 startedAt: providerRetry.startedAt,
5887 updatedAt: input.completedAt,
5888 }),
5889 });
5890 }
5891 if (input.status !== "completed") {
5892 yield* Ref.update(terminalizedNonCompletedNativeTurns, (current) => {
5893 const updated = new Set(current);
5894 updated.add(input.nativeTurnId);
5895 return updated;
5896 });
5897 }
5898 yield* agentMessageDeltas.flushTurn(input.nativeTurnId);
5899 yield* reasoningDeltas.flushTurn(input.nativeTurnId);
5900 yield* planDeltas.flushTurn(input.nativeTurnId);
5901 for (const [key, part] of reasoningParts) {
5902 if (part.turnId === input.nativeTurnId) reasoningParts.delete(key);
5903 }
5904 const nativeThreadId = input.context.providerThread.nativeThreadRef?.nativeId;
5905 // Codex continues an active goal with another turn, so the run stays
5906 // open and this turn reads as running until that turn starts.
5907 const activation =
5908 nativeThreadId == null ? undefined : goalActivations.get(nativeThreadId);
5909 const holdsForGoal =
5910 input.context.subagent === null &&
5911 input.status === "completed" &&
5912 nativeThreadId != null &&
5913 (goalsByNativeThread.get(nativeThreadId)?.status === "active" ||
5914 (activation !== undefined && !activation.stopped));
5915 const completedTurn: OrchestrationV2ProviderTurn = {
5916 id: input.context.providerTurnId,
5917 providerThreadId: input.context.providerThread.id,
5918 nodeId: input.context.providerNodeId,
5919 runAttemptId:
5920 input.context.subagent === null ? input.context.input.attemptId : null,
5921 nativeTurnRef: {
5922 driver: CODEX_PROVIDER,
5923 nativeId: input.nativeTurnId,
5924 strength: "strong",
5925 },
5926 ordinal: input.context.providerTurnOrdinal,
5927 status: input.status,
5928 startedAt: input.context.startedAt,
5929 completedAt: input.completedAt,
5930 turnTokenUsage: completeCodexTurnTokenUsage(
5931 usageStateForThread(
5932 input.context.providerThread.nativeThreadRef?.nativeId ??
5933 String(input.context.providerThread.id),
5934 ),
5935 input.nativeTurnId,
5936 input.status === "completed",
5937 ),
5938 };
5939 if (!holdsForGoal) {
5940 yield* emitProviderEvent({
5941 type: "provider_turn.updated",
5942 driver: CODEX_PROVIDER,
5943 threadId: input.context.projectionThreadId,
5944 providerTurn: completedTurn,
5945 });
5946 }
5947 if (input.context.subagent !== null) {
5948 yield* emitProviderEvent({
5949 type: "node.updated",
5950 driver: CODEX_PROVIDER,
5951 node: {
5952 id: input.context.providerNodeId,
5953 threadId: input.context.projectionThreadId,
5954 runId: null,
5955 parentNodeId: null,
5956 rootNodeId: input.context.rootNodeId,
5957 kind: "root_turn",
5958 status: input.status,
5959 countsForRun: false,
5960 providerThreadId: input.context.providerThread.id,
5961 providerTurnId: input.context.providerTurnId,
5962 nativeItemRef: input.context.subagent.task.nativeTaskRef,
5963 runtimeRequestId: null,
5964 checkpointScopeId: null,
5965 startedAt: input.context.providerNodeStartedAt,
5966 completedAt: input.completedAt,
5967 },
5968 });
5969 yield* emitProviderEvent({
5970 type: "provider_thread.updated",
5971 driver: CODEX_PROVIDER,
5972 providerThread: {
5973 ...input.context.providerThread,
5974 status: "idle",
5975 updatedAt: input.completedAt,
5976 },
5977 });
5978 yield* emitProviderEvent({
5979 type: "node.updated",
5980 driver: CODEX_PROVIDER,
5981 node: {
5982 id: input.context.subagent.subagentNodeId,
5983 threadId: input.context.subagent.parentContext.projectionThreadId,
5984 runId: input.context.subagent.parentContext.projectionRunId,
5985 parentNodeId: input.context.subagent.parentContext.itemParentNodeId,
5986 rootNodeId: input.context.subagent.parentContext.rootNodeId,
5987 kind: "subagent",
5988 status: input.status,
5989 countsForRun: false,
5990 providerThreadId: input.context.providerThread.id,
5991 providerTurnId: input.context.subagent.parentContext.providerTurnId,
5992 nativeItemRef: input.context.subagent.task.nativeTaskRef,
5993 runtimeRequestId: null,
5994 checkpointScopeId: null,
5995 startedAt: input.context.subagent.startedAt,
5996 completedAt: input.completedAt,
5997 },
5998 });
5999 yield* emitSubagentTaskUpdate({
6000 subagent: input.context.subagent,
6001 status: input.status,
6002 completedAt: input.completedAt,
6003 });
6004 }
6005 if (input.status === "interrupted" || input.status === "failed") {
6006 yield* terminalizeRunningCommandItems(
6007 input.context,
6008 input.nativeTurnId,
6009 input.status,
6010 input.completedAt,
6011 );
6012 yield* terminalizeMcpAppCaptures(input.context, input.nativeTurnId);
6013 }
6014 const dynamicToolStatus: "cancelled" | "interrupted" | "failed" =
6015 input.status === "interrupted" || input.status === "failed"
6016 ? input.status
6017 : "cancelled";
6018 yield* terminalizeRunningDynamicTools(
6019 input.context,
6020 input.nativeTurnId,
6021 dynamicToolStatus,
6022 input.completedAt,
6023 input.status === "interrupted" || input.status === "failed",
6024 );
6025 if (input.context.subagent === null) {
6026 yield* emitOrDeferRootTerminal({
6027 ...input,
6028 ...(providerRetry === undefined ? {} : { providerRetry }),
6029 ...(holdsForGoal ? { goalHoldTurn: completedTurn } : {}),
6030 });
6031 }
6032 const waiter = (yield* Ref.get(turnWaiters)).get(input.nativeTurnId);
6033 if (waiter !== undefined) {
6034 yield* Deferred.succeed(waiter, undefined);
6035 }
6036 const interruptInProgress = (yield* Ref.get(interruptingNativeTurns)).has(
6037 input.nativeTurnId,
6038 );
6039 // Completed turns can retain late background command context and
6040 // leftover persistent dynamic tools. Interrupted and failed turns
6041 // never wake from late item events.
6042 const retainSettledContext =
6043 input.status === "completed" &&
6044 (yield* turnHasRetainedBackgroundWork(input.nativeTurnId));
6045 if (retainSettledContext) {
6046 yield* Ref.update(settledTurns, (current) => {
6047 const updated = new Map(current);
6048 updated.set(input.nativeTurnId, input.context);
6049 return updated;
6050 });
6051 // Background work that finished between the check above and
6052 // the turn being recorded as settled saw no settled turn to
6053 // release, so it is re-checked once recorded.
6054 yield* releaseSettledTurnIfIdle(input.nativeTurnId);
6055 }
6056 yield* Ref.update(activeTurns, (current) => {
6057 const updated = new Map(current);
6058 updated.delete(input.nativeTurnId);
6059 return updated;
6060 });
6061 if (input.context.nativeStartReady !== undefined) {
6062 yield* Deferred.succeed(input.context.nativeStartReady, undefined);
6063 }
6064 yield* flushReadyRootTerminals();
6065 if (!retainSettledContext && !interruptInProgress) {
6066 yield* Ref.update(runningCommandItemsByTurn, (current) => {
6067 if (!current.has(input.nativeTurnId)) {
6068 return current;
6069 }
6070 const updated = new Map(current);
6071 updated.delete(input.nativeTurnId);
6072 return updated;
6073 });
6074 yield* Ref.update(runningDynamicToolsByTurn, (current) => {
6075 if (!current.has(input.nativeTurnId)) {
6076 return current;
6077 }
6078 const updated = new Map(current);
6079 updated.delete(input.nativeTurnId);
6080 return updated;
6081 });
6082 }
6083 if (!retainSettledContext) {
6084 yield* Ref.update(completedFinalAnswerTextsByTurn, (current) => {
6085 if (!current.has(input.nativeTurnId)) {
6086 return current;
6087 }
6088 const updated = new Map(current);
6089 updated.delete(input.nativeTurnId);
6090 return updated;
6091 });
6092 yield* Ref.update(finalAnswerItemIdsByTurn, (current) => {
6093 if (!current.has(input.nativeTurnId)) {
6094 return current;
6095 }
6096 const updated = new Map(current);
6097 updated.delete(input.nativeTurnId);
6098 return updated;
6099 });
6100 }
6101 return true;
6102 }),
6103 );
6104
6105 yield* client.handleServerNotification("turn/completed", (payload) =>
6106 Effect.gen(function* () {
6107 const context = (yield* Ref.get(activeTurns)).get(payload.turn.id);
6108 if (context === undefined) {
6109 return;
6110 }
6111 const nativeStatus = mapCodexTurnStatus(payload.turn.status);
6112 const status =
6113 nativeStatus === "completed" &&
6114 (yield* Ref.get(interruptingNativeTurns)).has(payload.turn.id)
6115 ? "interrupted"
6116 : nativeStatus;
6117 yield* finalizeCodexTurn({
6118 context,
6119 nativeTurnId: payload.turn.id,
6120 status,
6121 completedAt: codexTimestamp(payload.turn.completedAt),
6122 ...(payload.turn.error?.message === undefined
6123 ? {}
6124 : {
6125 failureMessage: payload.turn.error.message,
6126 ...(payload.turn.error.codexErrorInfo == null
6127 ? {}
6128 : {
6129 failureCode: codexErrorInfoCode(payload.turn.error.codexErrorInfo),
6130 }),
6131 }),
6132 });
6133 }),
6134 );
6135
6136 /**
6137 * Settles a `/goal` run that started no Codex turn. Its reply is plain
6138 * assistant text, as Claude's own `/goal` output is.
6139 */
6140 const completeGoalCommandTurn = Effect.fnUntraced(function* (
6141 turnInput: ProviderAdapter.ProviderAdapterV2TurnInput,
6142 reply: string,
6143 ) {
6144 const now = yield* DateTime.now;
6145 const nativeId = `goal-command:${turnInput.attemptId}`;
6146 const context: ActiveCodexTurnContext = {
6147 input: turnInput,
6148 projectionAppThread: turnInput.appThread,
6149 projectionThreadId: turnInput.threadId,
6150 projectionRunId: turnInput.runId,
6151 nativeTurnId: nativeId,
6152 providerThread: turnInput.providerThread,
6153 providerTurnId: idAllocator.derive.providerTurn({
6154 driver: CODEX_PROVIDER,
6155 nativeTurnId: nativeId,
6156 }),
6157 providerTurnOrdinal: turnInput.providerTurnOrdinal,
6158 providerNodeId: turnInput.rootNodeId,
6159 providerNodeKind: "root_turn",
6160 providerNodeStartedAt: now,
6161 itemParentNodeId: turnInput.rootNodeId,
6162 rootNodeId: turnInput.rootNodeId,
6163 subagent: null,
6164 startedAt: now,
6165 itemPositions: new Map(),
6166 };
6167 const providerTurn = {
6168 id: context.providerTurnId,
6169 providerThreadId: turnInput.providerThread.id,
6170 nodeId: turnInput.rootNodeId,
6171 runAttemptId: turnInput.attemptId,
6172 nativeTurnRef: null,
6173 ordinal: turnInput.providerTurnOrdinal,
6174 status: "running",
6175 startedAt: now,
6176 completedAt: null,
6177 } satisfies OrchestrationV2ProviderTurn;
6178 yield* emitProviderEvent({
6179 type: "provider_turn.updated",
6180 driver: CODEX_PROVIDER,
6181 threadId: turnInput.threadId,
6182 providerTurn,
6183 });
6184 yield* rememberRootProviderThread(turnInput.providerThread);
6185 const artifacts = yield* buildAgentMessageArtifacts(
6186 context,
6187 { id: nativeId, text: reply },
6188 true,
6189 );
6190 yield* emitProviderEvent({
6191 type: "node.updated",
6192 driver: CODEX_PROVIDER,
6193 node: artifacts.node,
6194 });
6195 yield* emitProviderEvent({
6196 type: "message.updated",
6197 driver: CODEX_PROVIDER,
6198 message: artifacts.message,
6199 });
6200 yield* emitProviderEvent({
6201 type: "turn_item.updated",
6202 driver: CODEX_PROVIDER,
6203 turnItem: artifacts.turnItem,
6204 });
6205 yield* emitProviderEvent({
6206 type: "provider_turn.updated",
6207 driver: CODEX_PROVIDER,
6208 threadId: turnInput.threadId,
6209 providerTurn: { ...providerTurn, status: "completed", completedAt: now },
6210 });
6211 yield* emitRootTerminal(context, {
6212 type: "turn.terminal",
6213 driver: CODEX_PROVIDER,
6214 providerThreadId: turnInput.providerThread.id,
6215 providerTurnId: context.providerTurnId,
6216 runOrdinal: turnInput.runOrdinal,
6217 status: "completed",
6218 failure: null,
6219 threadDisposition: "reusable",
6220 });
6221 });
6222
6223 /** The goal hold whose run still reports this provider turn as running. */
6224 const heldGoalTurn = (
6225 providerThread: OrchestrationV2ProviderThread,
6226 providerTurnId: ProviderTurnId,
6227 ) => {
6228 const nativeThreadId = providerThread.nativeThreadRef?.nativeId;
6229 const hold = nativeThreadId == null ? undefined : goalHolds.get(nativeThreadId);
6230 return hold?.context.providerTurnId === providerTurnId && nativeThreadId != null
6231 ? { nativeThreadId, hold }
6232 : undefined;
6233 };
6234
6235 /**
6236 * Like the Codex TUI, Stop pauses an active goal first so Codex starts
6237 * no further goal turn. Codex can end or continue the turn while the
6238 * pause is in flight, so the target resolves after it: a held run
6239 * settles here and leaves only its retained work to stop, and a
6240 * continued run moves Stop to its newest turn. `goalTurns` lists the
6241 * run's root turns, seen before and after the pause, whose descendants
6242 * Stop also reaches.
6243 */
6244 const resolveGoalStopTarget = Effect.fnUntraced(function* (
6245 turnInput: ProviderAdapter.ProviderAdapterV2InterruptInput,
6246 ) {
6247 const goalThreadId = turnInput.providerThread.nativeThreadRef?.nativeId;
6248 if (goalThreadId == null) {
6249 return { turnInput, goalTurns: [] as ReadonlyArray<ActiveCodexTurnContext> };
6250 }
6251 goalStops.set(goalThreadId, (goalStops.get(goalThreadId) ?? 0) + 1);
6252 return yield* Effect.gen(function* () {
6253 // The run can settle while the pause is in flight, which drops its record.
6254 const requestedTurnId = latestGoalTurnId(turnInput.providerTurnId);
6255 const heldBefore = goalHolds.get(goalThreadId)?.context;
6256 const turnsBefore = [
6257 ...(goalRuns.get(requestedTurnId) ?? []),
6258 ...Array.from((yield* Ref.get(activeTurns)).values()).filter(
6259 (context) => context.providerTurnId === requestedTurnId,
6260 ),
6261 ...(heldBefore?.providerTurnId === requestedTurnId ? [heldBefore] : []),
6262 ];
6263 const activation = goalActivations.get(goalThreadId);
6264 if (activation !== undefined) activation.stopped = true;
6265 if (goalsByNativeThread.get(goalThreadId)?.status === "active") {
6266 yield* client
6267 .request("thread/goal/set", { threadId: goalThreadId, status: "paused" })
6268 .pipe(
6269 Effect.tap(({ goal }) =>
6270 Effect.sync(() =>
6271 goalsByNativeThread.set(goalThreadId, providerGoalFromCodex(goal)),
6272 ),
6273 ),
6274 Effect.catch((cause) =>
6275 Effect.logWarning("orchestration-v2.codex-goal-pause-failed", { cause }),
6276 ),
6277 );
6278 }
6279 return yield* turnTerminalizationPermit.withPermits(1)(
6280 Effect.gen(function* () {
6281 const providerTurnId = latestGoalTurnId(turnInput.providerTurnId);
6282 const hold = goalHolds.get(goalThreadId);
6283 const held = hold !== undefined && hold.context.providerTurnId === providerTurnId;
6284 const goalTurns: ReadonlyArray<ActiveCodexTurnContext> = Array.from(
6285 new Set([
6286 ...turnsBefore,
6287 ...(goalRuns.get(providerTurnId) ?? []),
6288 ...(held ? [hold.context] : []),
6289 ]),
6290 );
6291 if (!held) return { turnInput: { ...turnInput, providerTurnId }, goalTurns };
6292 goalHolds.delete(goalThreadId);
6293 yield* settleGoalHold(hold, "interrupted");
6294 return {
6295 turnInput: { ...turnInput, providerTurnId, requestRuntimeRestart: true },
6296 goalTurns,
6297 };
6298 }),
6299 );
6300 }).pipe(
6301 Effect.ensuring(
6302 Effect.sync(() => {
6303 const stops = (goalStops.get(goalThreadId) ?? 1) - 1;
6304 if (stops > 0) goalStops.set(goalThreadId, stops);
6305 else goalStops.delete(goalThreadId);
6306 }),
6307 ),
6308 );
6309 });
6310
6311 /** Starts a native turn with this run's full turn configuration. */
6312 const startNativeTurn = (
6313 turnInput: ProviderAdapter.ProviderAdapterV2TurnInput,
6314 codexInput: ReadonlyArray<CodexSchema.V2TurnStartParams__UserInput>,
6315 ) =>
6316 Effect.gen(function* () {
6317 const threadId = yield* getNativeThreadId(turnInput.providerThread);
6318 const mcpSession = yield* mcpSessions.read(turnInput.threadId);
6319 const turnStartParams = yield* buildCodexTurnStartParams({
6320 nativeThreadId: threadId,
6321 codexInput,
6322 runtimePolicy: turnInput.runtimePolicy,
6323 modelSelection: turnInput.modelSelection,
6324 hasT3Mcp: mcpSession !== undefined,
6325 browserToolsAvailable: mcpSession?.browserToolsAvailable ?? true,
6326 deviceToolsAvailable: mcpSession?.capabilities?.has("device") ?? false,
6327 omitServiceTier: adapterOptions.resolveRuntime !== undefined,
6328 ...(turnInput.appContext === undefined ? {} : { appContext: turnInput.appContext }),
6329 });
6330 yield* Ref.update(pendingRootTurns, (current) => {
6331 const updated = new Map(current);
6332 updated.set(threadId, turnInput);
6333 return updated;
6334 });
6335 yield* Ref.update(additionalContextByThread, (current) => {
6336 const next = new Map(current);
6337 if (turnStartParams.additionalContext)
6338 next.set(threadId, turnStartParams.additionalContext);
6339 else next.delete(threadId);
6340 return next;
6341 });
6342 const started = yield* client.request("turn/start", turnStartParams);
6343 yield* registerRootTurn({
6344 turnInput,
6345 nativeTurnId: started.turn.id,
6346 startedAt: codexTimestamp(started.turn.startedAt),
6347 waitForNativeStart: started.turn.startedAt === null,
6348 });
6349 }).pipe(
6350 Effect.ensuring(
6351 Effect.flatMap(getNativeThreadId(turnInput.providerThread), (threadId) =>
6352 Ref.update(pendingRootTurns, (current) => {
6353 const updated = new Map(current);
6354 updated.delete(threadId);
6355 return updated;
6356 }),
6357 ).pipe(Effect.ignore),
6358 ),
6359 );
6360
6361 /**
6362 * Runs `/goal` through Codex's native goal API, as the Codex TUI does.
6363 * Setting or resuming a goal starts the first goal turn with this run's
6364 * turn configuration (approvals, sandbox, model, plan mode), since a
6365 * turn Codex starts on its own reuses the last turn's settings. Codex
6366 * continues later goal turns itself. Other commands settle with a reply.
6367 */
6368 const runGoalCommand = (
6369 turnInput: ProviderAdapter.ProviderAdapterV2TurnInput,
6370 command: CodexGoalCommand,
6371 ) =>
6372 Effect.gen(function* () {
6373 const threadId = yield* getNativeThreadId(turnInput.providerThread);
6374 // Goal notifications during the command belong to this run's snapshot.
6375 rootProviderThreads.set(threadId, turnInput.providerThread);
6376 const readGoal = client
6377 .request("thread/goal/get", { threadId })
6378 .pipe(
6379 Effect.map((response) =>
6380 response.goal == null ? null : providerGoalFromCodex(response.goal),
6381 ),
6382 );
6383 const current = yield* readGoal;
6384 goalsByNativeThread.set(threadId, current);
6385 if (command.type === "show") {
6386 return yield* completeGoalCommandTurn(
6387 turnInput,
6388 current === null ? "No goal is set." : describeCodexGoal(current),
6389 );
6390 }
6391 if (command.type === "clear") {
6392 const { cleared } = yield* client.request("thread/goal/clear", { threadId });
6393 goalsByNativeThread.set(threadId, null);
6394 return yield* completeGoalCommandTurn(
6395 turnInput,
6396 cleared ? "Goal cleared." : "No goal to clear.",
6397 );
6398 }
6399 if (current === null && command.type !== "set") {
6400 return yield* completeGoalCommandTurn(turnInput, "No goal is set.");
6401 }
6402 if (command.type === "pause") {
6403 const { goal } = yield* client.request("thread/goal/set", {
6404 threadId,
6405 status: "paused",
6406 });
6407 goalsByNativeThread.set(threadId, providerGoalFromCodex(goal));
6408 return yield* completeGoalCommandTurn(
6409 turnInput,
6410 "Goal paused. Send /goal resume to continue.",
6411 );
6412 }
6413 if (command.type === "set") {
6414 // A new objective replaces the goal and its accounting, like the TUI.
6415 if (current !== null) yield* client.request("thread/goal/clear", { threadId });
6416 // Paused until our turn runs, so Codex does not start one first.
6417 const { goal } = yield* client.request("thread/goal/set", {
6418 threadId,
6419 objective: command.objective,
6420 status: "paused",
6421 });
6422 goalsByNativeThread.set(threadId, providerGoalFromCodex(goal));
6423 }
6424 // Until the goal is active, a fast first turn still holds the run,
6425 // and Stop cancels the activation.
6426 const activation = { stopped: false };
6427 goalActivations.set(threadId, activation);
6428 yield* Effect.gen(function* () {
6429 // The first goal turn carries the objective; a resume continues from history.
6430 yield* startNativeTurn(
6431 turnInput,
6432 command.type === "set"
6433 ? yield* toCodexInput({
6434 ...turnInput,
6435 message: { ...turnInput.message, text: command.objective },
6436 })
6437 : [],
6438 );
6439 if (activation.stopped) return;
6440 const { goal } = yield* client.request("thread/goal/set", {
6441 threadId,
6442 status: "active",
6443 });
6444 goalsByNativeThread.set(threadId, providerGoalFromCodex(goal));
6445 if (activation.stopped) {
6446 const paused = yield* client.request("thread/goal/set", {
6447 threadId,
6448 status: "paused",
6449 });
6450 goalsByNativeThread.set(threadId, providerGoalFromCodex(paused.goal));
6451 }
6452 }).pipe(
6453 Effect.ensuring(
6454 turnTerminalizationPermit.withPermits(1)(
6455 Effect.gen(function* () {
6456 if (goalActivations.get(threadId) === activation) {
6457 goalActivations.delete(threadId);
6458 }
6459 // A run held for an activation that did not happen settles now.
6460 if (goalsByNativeThread.get(threadId)?.status !== "active") {
6461 yield* releaseGoalHold(threadId);
6462 }
6463 }),
6464 ),
6465 ),
6466 );
6467 }).pipe(
6468 Effect.mapError(
6469 (cause) =>
6470 new ProviderAdapter.ProviderAdapterTurnStartError({
6471 driver: CODEX_PROVIDER,
6472 threadId: turnInput.threadId,
6473 providerThreadId: turnInput.providerThread.id,
6474 runId: turnInput.runId,
6475 cause,
6476 }),
6477 ),
6478 );
6479
6480 // MCP Apps reach their server through Codex's own MCP client, so any
6481 // server the user configured for Codex works without T3 connecting to it.
6482 const mcpAppsError = (detail: string) => (cause: unknown) =>
6483 new ProviderAdapter.ProviderAdapterProtocolError({
6484 driver: CODEX_PROVIDER,
6485 detail,
6486 payload: cause,
6487 });
6488 const mcpApps: ProviderAdapter.ProviderAdapterV2McpApps = {
6489 listTools: (toolsInput) =>
6490 Effect.gen(function* () {
6491 const threadId = yield* getNativeThreadId(toolsInput.providerThread);
6492 const response = yield* ensureInitialized.pipe(
6493 Effect.andThen(
6494 client.request("mcpServerStatus/list", {
6495 threadId,
6496 serverName: toolsInput.server,
6497 detail: "toolsAndAuthOnly",
6498 }),
6499 ),
6500 );
6501 const server = response.data.find((entry) => entry.name === toolsInput.server);
6502 return Object.values(server?.tools ?? {});
6503 }).pipe(Effect.mapError(mcpAppsError("Failed to list Codex MCP tools."))),
6504 callTool: (callInput) =>
6505 Effect.gen(function* () {
6506 const threadId = yield* getNativeThreadId(callInput.providerThread);
6507 const response = yield* ensureInitialized.pipe(
6508 Effect.andThen(
6509 client.request("mcpServer/tool/call", {
6510 threadId,
6511 server: callInput.server,
6512 tool: callInput.tool,
6513 arguments: callInput.arguments as Schema.Json,
6514 }),
6515 ),
6516 );
6517 return {
6518 content: response.content,
6519 ...(response.structuredContent === undefined
6520 ? {}
6521 : { structuredContent: response.structuredContent }),
6522 ...(response.isError === true ? { isError: true } : {}),
6523 ...(response._meta === undefined ? {} : { _meta: response._meta }),
6524 };
6525 }).pipe(Effect.mapError(mcpAppsError("Codex MCP tool call failed."))),
6526 readResource: (readInput) =>
6527 Effect.gen(function* () {
6528 const threadId = yield* getNativeThreadId(readInput.providerThread);
6529 const response = yield* ensureInitialized.pipe(
6530 Effect.andThen(
6531 client.request("mcpServer/resource/read", {
6532 threadId,
6533 server: readInput.server,
6534 uri: readInput.uri,
6535 }),
6536 ),
6537 );
6538 return { contents: response.contents };
6539 }).pipe(Effect.mapError(mcpAppsError("Codex MCP resource read failed."))),
6540 };
6541
6542 const runtime: ProviderAdapter.ProviderAdapterV2SessionRuntime = {
6543 instanceId: adapterOptions.instanceId,
6544 driver: CODEX_PROVIDER,
6545 providerSessionId: input.providerSessionId,
6546 providerSession: session,
6547 events: Stream.fromEffectRepeat(Queue.take(events)),
6548 canReuseContextUsage: canReuseCodexContextUsage,
6549 // Known gap: a subagent that Codex resumes later reads as completed
6550 // (not pending) between turns, so idle release can win the race
6551 // against a long-delayed resume. Codex emits no resume-expected
6552 // signal to pin on.
6553 hasPendingBackgroundWork: Effect.gen(function* () {
6554 for (const items of (yield* Ref.get(runningCommandItemsByTurn)).values()) {
6555 if (items.size > 0) {
6556 return true;
6557 }
6558 }
6559 for (const items of (yield* Ref.get(runningDynamicToolsByTurn)).values()) {
6560 if (items.size > 0) {
6561 return true;
6562 }
6563 }
6564 if ((yield* Ref.get(pendingMcpAppCaptures)).size > 0) {
6565 return true;
6566 }
6567 for (const subagent of (yield* Ref.get(subagentThreads)).values()) {
6568 if (subagent.task.status === "running") {
6569 return true;
6570 }
6571 }
6572 return false;
6573 }),
6574 hasPendingBackgroundWorkForThread: (providerThread) =>
6575 Effect.gen(function* () {
6576 const contexts = [
6577 ...(yield* Ref.get(activeTurns)).values(),
6578 ...(yield* Ref.get(settledTurns)).values(),
6579 ];
6580 const roots = contexts.filter(
6581 (context) => context.providerThread.id === providerThread.id,
6582 );
6583 for (const context of contexts) {
6584 if (!roots.some((root) => context === root || isDescendantCodexTurn(context, root)))
6585 continue;
6586 if (yield* turnHasRetainedBackgroundWork(context.nativeTurnId)) return true;
6587 if (
6588 context.subagent !== null &&
6589 (yield* Ref.get(activeTurns)).has(context.nativeTurnId)
6590 )
6591 return true;
6592 }
6593 return false;
6594 }),
6595 ensureThread: (threadInput) =>
6596 ensureInitialized.pipe(
6597 Effect.andThen(mcpSessions.read(threadInput.threadId)),
6598 Effect.flatMap((mcpSession) =>
6599 Effect.gen(function* () {
6600 return yield* client.request(
6601 "thread/start",
6602 codexThreadRuntimeParams({
6603 mcpSession,
6604 scriptProgressConfig: yield* readScriptProgressConfig(
6605 threadInput.threadId,
6606 threadInput.runtimePolicy?.cwd,
6607 ),
6608 modelSelection: threadInput.modelSelection,
6609 runtimePolicy: threadInput.runtimePolicy,
6610 }),
6611 );
6612 }),
6613 ),
6614 Effect.map((response): OrchestrationV2ProviderThread =>
6615 providerThreadFromCodexThread({
6616 appThreadId: threadInput.threadId,
6617 idAllocator,
6618 ownerNodeId: null,
6619 providerSessionId: input.providerSessionId,
6620 providerInstanceId: adapterOptions.instanceId,
6621 thread: response.thread,
6622 }),
6623 ),
6624 Effect.mapError(
6625 (cause) =>
6626 new ProviderAdapter.ProviderAdapterEnsureThreadError({
6627 driver: CODEX_PROVIDER,
6628 threadId: threadInput.threadId,
6629 cause: normalizeCodexCause(cause),
6630 }),
6631 ),
6632 ),
6633 resumeThread: (threadInput) =>
6634 Effect.gen(function* () {
6635 const nativeThreadId = yield* getNativeThreadId(threadInput.providerThread);
6636 // excludeTurns is not in the generated request schema yet.
6637 const resume = client.raw.request("thread/resume", {
6638 threadId: nativeThreadId,
6639 excludeTurns: true,
6640 ...codexThreadRuntimeParams({
6641 mcpSession: yield* readMcpSession(
6642 threadInput.threadId ?? threadInput.providerThread.appThreadId,
6643 ),
6644 scriptProgressConfig: yield* readScriptProgressConfig(
6645 threadInput.threadId ?? threadInput.providerThread.appThreadId,
6646 threadInput.runtimePolicy?.cwd ?? input.runtimePolicy.cwd,
6647 ),
6648 ...(threadInput.modelSelection === undefined
6649 ? {}
6650 : { modelSelection: threadInput.modelSelection }),
6651 ...(threadInput.runtimePolicy === undefined
6652 ? {}
6653 : { runtimePolicy: threadInput.runtimePolicy }),
6654 }),
6655 });
6656 const response = yield* ensureInitialized.pipe(
6657 Effect.andThen(
6658 resume.pipe(
6659 Effect.catchTags({
6660 CodexAppServerRequestError: (cause) => {
6661 if (
6662 !/\bsession \S+ is archived\b|\bcodex unarchive\b/i.test(
6663 cause.errorMessage,
6664 )
6665 ) {
6666 return Effect.fail(cause);
6667 }
6668 // Keep the session's history without decoding the unarchive response.
6669 return client.raw
6670 .request("thread/unarchive", { threadId: nativeThreadId })
6671 .pipe(Effect.andThen(resume));
6672 },
6673 }),
6674 ),
6675 ),
6676 Effect.flatMap(decodeCodexResumeMetadata),
6677 );
6678 // Codex follows a resume with a goal snapshot notification; the
6679 // run's first turn writes it if it differs from the stored goal.
6680 return {
6681 ...threadInput.providerThread,
6682 providerSessionId: input.providerSessionId,
6683 providerInstanceId: adapterOptions.instanceId,
6684 status: "idle",
6685 nativeThreadRef: {
6686 driver: CODEX_PROVIDER,
6687 nativeId: response.thread.id,
6688 strength: "strong",
6689 },
6690 nativeConversationHeadRef: threadInput.providerThread.nativeConversationHeadRef,
6691 updatedAt: codexTimestamp(response.thread.updatedAt),
6692 } satisfies OrchestrationV2ProviderThread;
6693 }).pipe(
6694 Effect.mapError(
6695 (cause) =>
6696 new ProviderAdapter.ProviderAdapterResumeThreadError({
6697 driver: CODEX_PROVIDER,
6698 providerSessionId: input.providerSessionId,
6699 providerThreadId: threadInput.providerThread.id,
6700 cause: normalizeCodexCause(cause),
6701 }),
6702 ),
6703 ),
6704 compactThread: (turnInput) =>
6705 Effect.gen(function* () {
6706 const threadId = yield* getNativeThreadId(turnInput.providerThread);
6707 yield* Ref.update(pendingRootTurns, (current) =>
6708 new Map(current).set(threadId, turnInput),
6709 );
6710 // Cleared on interrupt too, as startTurn does: an interrupted start
6711 // must not adopt the native turn that a late turn/started reports.
6712 yield* client.request("thread/compact/start", { threadId }).pipe(
6713 Effect.onError(() =>
6714 Ref.update(pendingRootTurns, (current) => {
6715 const next = new Map(current);
6716 next.delete(threadId);
6717 return next;
6718 }),
6719 ),
6720 );
6721 }).pipe(
6722 Effect.mapError(
6723 (cause) =>
6724 new ProviderAdapter.ProviderAdapterTurnStartError({
6725 driver: CODEX_PROVIDER,
6726 threadId: turnInput.threadId,
6727 providerThreadId: turnInput.providerThread.id,
6728 runId: turnInput.runId,
6729 cause,
6730 }),
6731 ),
6732 ),
6733 injectHistory: (input) =>
6734 Effect.gen(function* () {
6735 const threadId = yield* getNativeThreadId(input.providerThread);
6736 return yield* client
6737 .request("thread/inject_items", {
6738 threadId,
6739 items: historyResponseItems(input.messages, input.context),
6740 })
6741 .pipe(
6742 Effect.as(true),
6743 // Older app servers reject unknown methods before mutating history.
6744 // Transport errors and invalid payloads are ambiguous and must not
6745 // fall through to a second delivery in the current user message.
6746 Effect.catchTags({
6747 CodexAppServerRequestError: (error) =>
6748 error.code === -32601 ? Effect.succeed(false) : Effect.fail(error),
6749 }),
6750 );
6751 }).pipe(
6752 Effect.mapError(
6753 (cause) =>
6754 new ProviderAdapter.ProviderAdapterProtocolError({
6755 driver: CODEX_PROVIDER,
6756 detail: "Failed to inject historical context",
6757 cause,
6758 }),
6759 ),
6760 ),
6761 startTurn: (turnInput) => {
6762 const goalCommand =
6763 turnInput.message.attachments.length === 0 &&
6764 turnInput.restartContinuationOfRunId === undefined
6765 ? parseCodexGoalCommand(turnInput.message.text)
6766 : null;
6767 if (goalCommand !== null) return runGoalCommand(turnInput, goalCommand);
6768 return Effect.gen(function* () {
6769 yield* startNativeTurn(
6770 turnInput,
6771 turnInput.restartContinuationOfRunId === undefined
6772 ? yield* toCodexInput(turnInput)
6773 : [],
6774 );
6775 }).pipe(
6776 Effect.mapError(
6777 (cause) =>
6778 new ProviderAdapter.ProviderAdapterTurnStartError({
6779 driver: CODEX_PROVIDER,
6780 threadId: turnInput.threadId,
6781 providerThreadId: turnInput.providerThread.id,
6782 runId: turnInput.runId,
6783 cause,
6784 }),
6785 ),
6786 );
6787 },
6788 steerTurn: (turnInput) =>
6789 Effect.gen(function* () {
6790 const threadId = yield* getNativeThreadId(turnInput.providerThread);
6791 // Between goal turns, steer the turn Codex continues with.
6792 const latestTurnId = latestGoalTurnId(turnInput.providerTurnId);
6793 const held = heldGoalTurn(turnInput.providerThread, latestTurnId);
6794 const providerTurnId =
6795 held === undefined
6796 ? latestTurnId
6797 : ((yield* Deferred.await(held.hold.next))?.providerTurnId ?? latestTurnId);
6798 const activeTurn = Array.from((yield* Ref.get(activeTurns)).values()).find(
6799 (candidate) => candidate.providerTurnId === providerTurnId,
6800 );
6801 if (activeTurn === undefined) {
6802 return yield* toProtocolError(
6803 `Provider turn ${turnInput.providerTurnId} is not active and cannot be steered.`,
6804 );
6805 }
6806
6807 const codexInput = yield* toCodexInput(turnInput);
6808 yield* client.request("turn/steer", {
6809 expectedTurnId: activeTurn.nativeTurnId,
6810 input: codexInput,
6811 threadId,
6812 });
6813 }).pipe(
6814 Effect.mapError(
6815 (cause) =>
6816 new ProviderAdapter.ProviderAdapterSteerRunError({
6817 driver: CODEX_PROVIDER,
6818 providerThreadId: turnInput.providerThread.id,
6819 providerTurnId: turnInput.providerTurnId,
6820 cause,
6821 }),
6822 ),
6823 ),
6824 // Dropping this connection's subscription lets the shared app-server
6825 // shut the native thread (and its MCP servers) down once it is idle.
6826 // `notLoaded` / `notSubscribed` mean there is nothing left to unload.
6827 unloadThread: (unloadInput) =>
6828 Effect.gen(function* () {
6829 const nativeThreadId = yield* getNativeThreadId(unloadInput.providerThread);
6830 sessionApprovals.delete(nativeThreadId);
6831 yield* client.request("thread/unsubscribe", { threadId: nativeThreadId });
6832 }).pipe(
6833 Effect.mapError((cause) =>
6834 cause._tag === "ProviderAdapterProtocolError"
6835 ? cause
6836 : new ProviderAdapter.ProviderAdapterProtocolError({
6837 driver: CODEX_PROVIDER,
6838 detail: `Failed to unload Codex thread for provider thread ${unloadInput.providerThread.id}`,
6839 cause: normalizeCodexCause(cause),
6840 }),
6841 ),
6842 ),
6843 interruptTurn: (requestedInput) =>
6844 Effect.gen(function* () {
6845 const { turnInput, goalTurns } = yield* resolveGoalStopTarget(requestedInput);
6846 const [activeTurnContexts, settledTurnContexts] =
6847 yield* turnTerminalizationPermit.withPermits(1)(
6848 Effect.gen(function* () {
6849 return [
6850 Array.from((yield* Ref.get(activeTurns)).values()),
6851 Array.from((yield* Ref.get(settledTurns)).values()),
6852 ] as const;
6853 }),
6854 );
6855 const activeTurn =
6856 activeTurnContexts.find(
6857 (candidate) => candidate.providerTurnId === turnInput.providerTurnId,
6858 ) ??
6859 (turnInput.requestRuntimeRestart === true
6860 ? settledTurnContexts.find(
6861 (candidate) => candidate.providerThread.id === turnInput.providerThread.id,
6862 )
6863 : undefined);
6864 // A goal run spans several root turns, and work an earlier one
6865 // started can outlive it.
6866 const lineageRoots = Array.from(
6867 new Set([...(activeTurn === undefined ? [] : [activeTurn]), ...goalTurns]),
6868 );
6869 const inLineage = (context: ActiveCodexTurnContext) =>
6870 lineageRoots.some(
6871 (root) => context === root || isDescendantCodexTurn(context, root),
6872 );
6873 const interruptTargetContexts = [
6874 ...(activeTurn === undefined ? [] : [activeTurn]),
6875 ...activeTurnContexts.filter(
6876 (candidate) => candidate !== activeTurn && inLineage(candidate),
6877 ),
6878 ...settledTurnContexts.filter(
6879 (candidate) =>
6880 candidate !== activeTurn &&
6881 ((turnInput.requestRuntimeRestart === true &&
6882 candidate.providerThread.id === turnInput.providerThread.id) ||
6883 inLineage(candidate)),
6884 ),
6885 ];
6886 if (interruptTargetContexts.length === 0) {
6887 // Stop on a settled turn this process retains nothing for
6888 // (released, restarted, or every command already reported).
6889 if (turnInput.requestRuntimeRestart === true) return;
6890 return yield* toProtocolError(
6891 `Provider turn ${turnInput.providerTurnId} is not active and cannot be interrupted.`,
6892 );
6893 }
6894 const interruptTargets: Array<{
6895 readonly context: ActiveCodexTurnContext;
6896 readonly completion: Deferred.Deferred<void, never>;
6897 }> = [];
6898 for (const context of interruptTargetContexts) {
6899 interruptTargets.push({ context, completion: yield* Deferred.make<void>() });
6900 }
6901 yield* Ref.update(turnWaiters, (current) => {
6902 const updated = new Map(current);
6903 for (const target of interruptTargets) {
6904 updated.set(target.context.nativeTurnId, target.completion);
6905 }
6906 return updated;
6907 });
6908 yield* Ref.update(interruptingNativeTurns, (current) => {
6909 const updated = new Set(current);
6910 for (const target of interruptTargets) {
6911 updated.add(target.context.nativeTurnId);
6912 }
6913 return updated;
6914 });
6915 const registeredActiveTurns = yield* Ref.get(activeTurns);
6916 for (const target of interruptTargets) {
6917 if (registeredActiveTurns.get(target.context.nativeTurnId) !== target.context) {
6918 yield* Deferred.succeed(target.completion, undefined);
6919 }
6920 }
6921
6922 const cleanupInterruptState = Effect.gen(function* () {
6923 yield* Ref.update(turnWaiters, (current) => {
6924 const updated = new Map(current);
6925 for (const target of interruptTargets) {
6926 updated.delete(target.context.nativeTurnId);
6927 }
6928 return updated;
6929 });
6930 yield* Ref.update(interruptingNativeTurns, (current) => {
6931 const updated = new Set(current);
6932 for (const target of interruptTargets) {
6933 updated.delete(target.context.nativeTurnId);
6934 }
6935 return updated;
6936 });
6937 yield* Ref.update(runningCommandItemsByTurn, (current) => {
6938 const updated = new Map(current);
6939 for (const target of interruptTargets) {
6940 if (settledTurnContexts.includes(target.context)) continue;
6941 updated.delete(target.context.nativeTurnId);
6942 }
6943 return updated;
6944 });
6945 });
6946
6947 const trackedInterruptNativeTurnIds = new Set(
6948 interruptTargets.map((target) => target.context.nativeTurnId),
6949 );
6950 const finalizeRemainingInterruptLineage = Effect.gen(function* () {
6951 while (true) {
6952 const newlyDiscovered: Array<ActiveCodexTurnContext> = [];
6953 const activeLineage = yield* turnTerminalizationPermit.withPermits(1)(
6954 Effect.gen(function* () {
6955 const lineage = Array.from((yield* Ref.get(activeTurns)).values()).filter(
6956 inLineage,
6957 );
6958 for (const context of lineage) {
6959 if (trackedInterruptNativeTurnIds.has(context.nativeTurnId)) {
6960 continue;
6961 }
6962 const completion = yield* Deferred.make<void>();
6963 interruptTargets.push({ context, completion });
6964 trackedInterruptNativeTurnIds.add(context.nativeTurnId);
6965 newlyDiscovered.push(context);
6966 yield* Ref.update(turnWaiters, (current) => {
6967 const updated = new Map(current);
6968 updated.set(context.nativeTurnId, completion);
6969 return updated;
6970 });
6971 yield* Ref.update(interruptingNativeTurns, (current) => {
6972 const updated = new Set(current);
6973 updated.add(context.nativeTurnId);
6974 return updated;
6975 });
6976 }
6977 return lineage;
6978 }),
6979 );
6980 if (activeLineage.length === 0) {
6981 return;
6982 }
6983 if (newlyDiscovered.length > 0) {
6984 const completed = yield* Effect.forEach(
6985 newlyDiscovered,
6986 (context) =>
6987 Effect.flatMap(getNativeThreadId(context.providerThread), (threadId) =>
6988 client.request("turn/interrupt", {
6989 threadId,
6990 turnId: context.nativeTurnId,
6991 }),
6992 ).pipe(
6993 Effect.catch((cause) =>
6994 Effect.logWarning(
6995 "orchestration-v2.codex-remaining-lineage-interrupt-failed",
6996 {
6997 nativeTurnId: context.nativeTurnId,
6998 providerSessionId: input.providerSessionId,
6999 cause,
7000 },
7001 ),
7002 ),
7003 ),
7004 { concurrency: "unbounded", discard: true },
7005 ).pipe(Effect.timeoutOption("10 seconds"));
7006 if (Option.isNone(completed)) {
7007 yield* Effect.logWarning(
7008 "orchestration-v2.codex-remaining-lineage-interrupt-timeout",
7009 {
7010 nativeTurnIds: newlyDiscovered.map((context) => context.nativeTurnId),
7011 providerSessionId: input.providerSessionId,
7012 },
7013 );
7014 }
7015 }
7016 const completedAt = yield* DateTime.now;
7017 for (const context of activeLineage) {
7018 yield* finalizeCodexTurn({
7019 context,
7020 nativeTurnId: context.nativeTurnId,
7021 status: "interrupted",
7022 completedAt,
7023 });
7024 }
7025 }
7026 });
7027
7028 const interruptLateDescendants = Effect.gen(function* () {
7029 const activeLineage = yield* turnTerminalizationPermit.withPermits(1)(
7030 Effect.gen(function* () {
7031 const lineage = Array.from((yield* Ref.get(activeTurns)).values()).filter(
7032 (context) =>
7033 !lineageRoots.includes(context) &&
7034 inLineage(context) &&
7035 !trackedInterruptNativeTurnIds.has(context.nativeTurnId),
7036 );
7037 for (const context of lineage) {
7038 const completion = yield* Deferred.make<void>();
7039 interruptTargets.push({ context, completion });
7040 trackedInterruptNativeTurnIds.add(context.nativeTurnId);
7041 yield* Ref.update(turnWaiters, (current) => {
7042 const updated = new Map(current);
7043 updated.set(context.nativeTurnId, completion);
7044 return updated;
7045 });
7046 yield* Ref.update(interruptingNativeTurns, (current) => {
7047 const updated = new Set(current);
7048 updated.add(context.nativeTurnId);
7049 return updated;
7050 });
7051 }
7052 return lineage;
7053 }),
7054 );
7055 const completed = yield* Effect.forEach(
7056 activeLineage,
7057 (context) =>
7058 Effect.flatMap(getNativeThreadId(context.providerThread), (threadId) =>
7059 client.request("turn/interrupt", {
7060 threadId,
7061 turnId: context.nativeTurnId,
7062 }),
7063 ),
7064 { concurrency: "unbounded", discard: true },
7065 ).pipe(Effect.timeoutOption("10 seconds"));
7066 if (Option.isNone(completed)) {
7067 yield* Effect.logWarning(
7068 "orchestration-v2.codex-late-descendant-interrupt-timeout",
7069 {
7070 nativeTurnIds: activeLineage.map((context) => context.nativeTurnId),
7071 providerSessionId: input.providerSessionId,
7072 },
7073 );
7074 }
7075 });
7076
7077 yield* Effect.gen(function* () {
7078 for (const target of interruptTargets) {
7079 const context = target.context;
7080 // A null start timestamp acknowledges a queued turn; Codex cannot interrupt it
7081 // until turn/started confirms that the native task exists.
7082 if (context.nativeStartReady !== undefined) {
7083 const ready = yield* Deferred.await(context.nativeStartReady).pipe(
7084 Effect.timeoutOption("10 seconds"),
7085 );
7086 if (Option.isNone(ready)) {
7087 return yield* toProtocolError(
7088 "Codex did not start the queued turn within 10 seconds; Stop could not be delivered.",
7089 );
7090 }
7091 }
7092 if ((yield* Ref.get(activeTurns)).get(context.nativeTurnId) !== context) {
7093 continue;
7094 }
7095 yield* client.request("turn/interrupt", {
7096 threadId: yield* getNativeThreadId(context.providerThread),
7097 turnId: context.nativeTurnId,
7098 });
7099 }
7100 const containedTerminalKeys = new Set<string>();
7101 const attemptedTerminalKeys = new Set<string>();
7102 const collectTrackedTerminals = (targets: typeof interruptTargets) =>
7103 Effect.gen(function* () {
7104 const trackedTerminals = new Map<
7105 string,
7106 { readonly nativeThreadId: string; readonly processId: string }
7107 >();
7108 const runningItems = yield* Ref.get(runningCommandItemsByTurn);
7109 for (const target of targets) {
7110 const nativeThreadId = yield* getNativeThreadId(
7111 target.context.providerThread,
7112 );
7113 const items = runningItems.get(target.context.nativeTurnId);
7114 for (const item of items?.values() ?? []) {
7115 if (item.processId !== undefined) {
7116 trackedTerminals.set(`${nativeThreadId}:${item.processId}`, {
7117 nativeThreadId,
7118 processId: item.processId,
7119 });
7120 }
7121 }
7122 }
7123 return trackedTerminals;
7124 });
7125 const terminateTrackedTerminals = (targets: typeof interruptTargets) =>
7126 Effect.gen(function* () {
7127 const trackedTerminals = yield* collectTrackedTerminals(targets);
7128 const pendingTerminals = Array.from(trackedTerminals.entries())
7129 .filter(([key]) => !containedTerminalKeys.has(key))
7130 .sort(
7131 ([leftKey], [rightKey]) =>
7132 Number(attemptedTerminalKeys.has(leftKey)) -
7133 Number(attemptedTerminalKeys.has(rightKey)),
7134 );
7135 return yield* Effect.forEach(
7136 pendingTerminals,
7137 ([key, { nativeThreadId, processId }]) =>
7138 Effect.gen(function* () {
7139 attemptedTerminalKeys.add(key);
7140 yield* terminateBackgroundTerminal(nativeThreadId, processId);
7141 containedTerminalKeys.add(key);
7142 }).pipe(
7143 Effect.as({ success: true as const }),
7144 Effect.catch((error) =>
7145 Effect.succeed({ success: false as const, error }),
7146 ),
7147 ),
7148 );
7149 });
7150
7151 const [, completed] = yield* Effect.all(
7152 [
7153 terminateTrackedTerminals(interruptTargets),
7154 Effect.forEach(
7155 interruptTargets,
7156 (target) => Deferred.await(target.completion),
7157 { concurrency: "unbounded", discard: true },
7158 ).pipe(Effect.timeoutOption("10 seconds")),
7159 ],
7160 { concurrency: "unbounded" },
7161 );
7162 if (Option.isNone(completed)) {
7163 for (const target of interruptTargets) {
7164 const context = target.context;
7165 if ((yield* Ref.get(activeTurns)).get(context.nativeTurnId) !== context) {
7166 continue;
7167 }
7168 yield* Effect.logWarning("orchestration-v2.codex-interrupt-timeout", {
7169 providerSessionId: input.providerSessionId,
7170 providerThreadId: context.providerThread.id,
7171 providerTurnId: context.providerTurnId,
7172 nativeTurnId: context.nativeTurnId,
7173 });
7174 }
7175 }
7176
7177 yield* interruptLateDescendants;
7178 yield* finalizeRemainingInterruptLineage;
7179
7180 const terminationResults = yield* terminateTrackedTerminals(interruptTargets);
7181 const failedTermination = terminationResults.find((result) => !result.success);
7182 if (failedTermination !== undefined && !failedTermination.success) {
7183 return yield* Effect.fail(failedTermination.error);
7184 }
7185 // Stop the retained items without rewriting the completed root turn.
7186 yield* turnTerminalizationPermit.withPermits(1)(
7187 Effect.gen(function* () {
7188 const completedAt = yield* DateTime.now;
7189 for (const target of interruptTargets) {
7190 const context = target.context;
7191 if ((yield* Ref.get(settledTurns)).get(context.nativeTurnId) !== context)
7192 continue;
7193 yield* terminalizeRunningCommandItems(
7194 context,
7195 context.nativeTurnId,
7196 "interrupted",
7197 completedAt,
7198 );
7199 yield* terminalizeRunningDynamicTools(
7200 context,
7201 context.nativeTurnId,
7202 "interrupted",
7203 completedAt,
7204 true,
7205 );
7206 yield* terminalizeMcpAppCaptures(context, context.nativeTurnId);
7207 yield* Ref.update(runningCommandItemsByTurn, (current) => {
7208 const updated = new Map(current);
7209 updated.delete(context.nativeTurnId);
7210 return updated;
7211 });
7212 yield* releaseSettledTurnIfIdle(context.nativeTurnId);
7213 }
7214 }),
7215 );
7216 }).pipe(
7217 Effect.onError(() => finalizeRemainingInterruptLineage),
7218 Effect.ensuring(cleanupInterruptState),
7219 );
7220 }).pipe(
7221 Effect.mapError(
7222 (cause) =>
7223 new ProviderAdapter.ProviderAdapterInterruptError({
7224 driver: CODEX_PROVIDER,
7225 providerThreadId: requestedInput.providerThread.id,
7226 providerTurnId: requestedInput.providerTurnId,
7227 cause,
7228 }),
7229 ),
7230 ),
7231 respondToRuntimeRequest: (requestInput) =>
7232 Effect.gen(function* () {
7233 const pending = (yield* Ref.get(pendingRuntimeRequests)).get(
7234 String(requestInput.requestId),
7235 );
7236 if (pending === undefined) {
7237 return yield* new ProviderAdapter.ProviderAdapterRuntimeRequestResponseError({
7238 driver: CODEX_PROVIDER,
7239 requestId: requestInput.requestId,
7240 cause: toProtocolError(
7241 `No pending Codex runtime request ${requestInput.requestId}.`,
7242 ),
7243 });
7244 }
7245 if (pending.type === "user_input") {
7246 if (requestInput.answers === undefined) {
7247 return yield* new ProviderAdapter.ProviderAdapterRuntimeRequestResponseError({
7248 driver: CODEX_PROVIDER,
7249 requestId: requestInput.requestId,
7250 cause: toProtocolError(
7251 `Codex user input request ${requestInput.requestId} requires answers.`,
7252 ),
7253 });
7254 }
7255 yield* Deferred.succeed(pending.answers, requestInput.answers);
7256 return;
7257 }
7258 if (requestInput.decision === undefined) {
7259 return yield* new ProviderAdapter.ProviderAdapterRuntimeRequestResponseError({
7260 driver: CODEX_PROVIDER,
7261 requestId: requestInput.requestId,
7262 cause: toProtocolError(
7263 `Codex ${pending.requestKind} request ${requestInput.requestId} requires an approval decision.`,
7264 ),
7265 });
7266 }
7267 yield* Deferred.succeed(pending.decision, requestInput.decision);
7268 }).pipe(
7269 Effect.mapError((cause) =>
7270 isProviderAdapterRuntimeRequestResponseError(cause)
7271 ? cause
7272 : new ProviderAdapter.ProviderAdapterRuntimeRequestResponseError({
7273 driver: CODEX_PROVIDER,
7274 requestId: requestInput.requestId,
7275 cause,
7276 }),
7277 ),
7278 ),
7279 uploadFeedback: (feedbackInput) =>
7280 Effect.gen(function* () {
7281 const threadId = yield* getNativeThreadId(feedbackInput.providerThread);
7282 const response = yield* ensureInitialized.pipe(
7283 Effect.andThen(
7284 client.request("feedback/upload", {
7285 classification: "bug",
7286 includeLogs: true,
7287 ...(feedbackInput.reason ? { reason: feedbackInput.reason } : {}),
7288 threadId,
7289 }),
7290 ),
7291 );
7292 return { feedbackId: response.threadId };
7293 }).pipe(
7294 Effect.mapError(
7295 (cause) =>
7296 new ProviderAdapter.ProviderAdapterProtocolError({
7297 driver: CODEX_PROVIDER,
7298 detail: "Failed to upload Codex thread feedback.",
7299 payload: cause,
7300 }),
7301 ),
7302 ),
7303 mcpApps,
7304 readThreadSnapshot: (threadInput) =>
7305 Effect.gen(function* () {
7306 const threadId = yield* getNativeThreadId(threadInput.providerThread);
7307 const response = yield* ensureInitialized.pipe(
7308 Effect.andThen(client.request("thread/read", { threadId, includeTurns: true })),
7309 );
7310 return {
7311 providerThread: {
7312 ...threadInput.providerThread,
7313 nativeThreadRef: {
7314 driver: CODEX_PROVIDER,
7315 nativeId: response.thread.id,
7316 strength: "strong" as const,
7317 },
7318 nativeConversationHeadRef: threadInput.providerThread.nativeConversationHeadRef,
7319 updatedAt: codexTimestamp(response.thread.updatedAt),
7320 },
7321 providerTurns: [],
7322 messages: [],
7323 runtimeRequests: [],
7324 providerPayload: response.thread,
7325 };
7326 }).pipe(
7327 Effect.mapError(
7328 (cause) =>
7329 new ProviderAdapter.ProviderAdapterReadThreadSnapshotError({
7330 driver: CODEX_PROVIDER,
7331 providerThreadId: threadInput.providerThread.id,
7332 cause,
7333 }),
7334 ),
7335 ),
7336 rollbackThread: (threadInput) =>
7337 Effect.gen(function* () {
7338 const threadId = yield* getNativeThreadId(threadInput.providerThread);
7339 const numTurns = yield* resolveCodexRollbackTurnCount(threadInput);
7340 const nativeConversationHeadRef =
7341 threadInput.target.type === "provider_turn"
7342 ? threadInput.target.providerTurn.nativeTurnRef
7343 : null;
7344 if (numTurns === 0) {
7345 return {
7346 providerThread: {
7347 ...threadInput.providerThread,
7348 nativeConversationHeadRef,
7349 status: "idle" as const,
7350 },
7351 providerTurns: [],
7352 messages: [],
7353 runtimeRequests: [],
7354 };
7355 }
7356 // Codex 0.156 can revert paginated history at a turn boundary.
7357 // Legacy history no longer has a rollback endpoint.
7358 const { historyMode, loaded } = yield* ensureInitialized.pipe(
7359 Effect.andThen(readCodexThreadHistoryMetadata(client.raw, threadId)),
7360 );
7361 if (historyMode !== "paginated") {
7362 return yield* new ProviderAdapter.ProviderAdapterRollbackThreadError({
7363 driver: CODEX_PROVIDER,
7364 providerThreadId: threadInput.providerThread.id,
7365 cause: `Cannot roll back Codex thread ${threadId}: the thread uses legacy history, which Codex 0.156 cannot revert.`,
7366 });
7367 }
7368 // `thread/revert` only acts on a thread loaded in this app-server
7369 // process. After a restart or idle release, load it the same way
7370 // the next turn would before reverting.
7371 if (!loaded) {
7372 yield* client.raw.request("thread/resume", {
7373 threadId,
7374 excludeTurns: true,
7375 ...codexThreadRuntimeParams({
7376 mcpSession: yield* readMcpSession(threadInput.providerThread.appThreadId),
7377 scriptProgressConfig: yield* readScriptProgressConfig(
7378 threadInput.providerThread.appThreadId,
7379 input.runtimePolicy.cwd,
7380 ),
7381 modelSelection: input.modelSelection,
7382 runtimePolicy: input.runtimePolicy,
7383 }),
7384 });
7385 }
7386 const response = yield* ensureInitialized.pipe(
7387 Effect.andThen(revertCodexThread(client, threadId, numTurns)),
7388 );
7389 turnTokenUsageByThread.delete(threadId);
7390 return {
7391 providerThread: {
7392 ...threadInput.providerThread,
7393 nativeThreadRef: {
7394 driver: CODEX_PROVIDER,
7395 nativeId: response.thread.id,
7396 strength: "strong" as const,
7397 },
7398 nativeConversationHeadRef,
7399 status: "idle" as const,
7400 updatedAt: codexTimestamp(response.thread.updatedAt),
7401 },
7402 providerTurns: [],
7403 messages: [],
7404 runtimeRequests: [],
7405 providerPayload: response.thread,
7406 };
7407 }).pipe(
7408 Effect.mapError(
7409 (cause) =>
7410 new ProviderAdapter.ProviderAdapterRollbackThreadError({
7411 driver: CODEX_PROVIDER,
7412 providerThreadId: threadInput.providerThread.id,
7413 cause: normalizeCodexCause(cause),
7414 }),
7415 ),
7416 ),
7417 forkThread: (threadInput) =>
7418 Effect.gen(function* () {
7419 const threadId = yield* getNativeThreadId(threadInput.sourceProviderThread);
7420 const boundary = yield* resolveCodexForkBoundary(threadInput);
7421 const response = yield* ensureInitialized.pipe(
7422 Effect.andThen(
7423 client.request("thread/fork", {
7424 threadId,
7425 ...(boundary.lastTurnId === undefined
7426 ? {}
7427 : { lastTurnId: boundary.lastTurnId }),
7428 ...codexThreadRuntimeParams({
7429 mcpSession: yield* mcpSessions.read(threadInput.targetThreadId),
7430 scriptProgressConfig: yield* readScriptProgressConfig(
7431 threadInput.targetThreadId,
7432 threadInput.runtimePolicy?.cwd ?? input.runtimePolicy.cwd,
7433 ),
7434 ...(threadInput.modelSelection === undefined
7435 ? {}
7436 : { modelSelection: threadInput.modelSelection }),
7437 ...(threadInput.runtimePolicy === undefined
7438 ? {}
7439 : { runtimePolicy: threadInput.runtimePolicy }),
7440 }),
7441 }),
7442 ),
7443 Effect.mapError(
7444 (cause) =>
7445 new ProviderAdapter.ProviderAdapterForkThreadError({
7446 driver: CODEX_PROVIDER,
7447 providerThreadId: threadInput.sourceProviderThread.id,
7448 cause: normalizeCodexCause(cause),
7449 }),
7450 ),
7451 );
7452 let forkedThread = response.thread;
7453 if (boundary.rollbackTurnCount > 0) {
7454 // Reached only when the selected source turn has no native
7455 // turn reference, so the fork had to be taken at head and then
7456 // trimmed with the paginated history API.
7457 const { historyMode } = yield* ensureInitialized.pipe(
7458 Effect.andThen(readCodexThreadHistoryMetadata(client.raw, response.thread.id)),
7459 );
7460 if (historyMode !== "paginated") {
7461 return yield* new ProviderAdapter.ProviderAdapterForkThreadError({
7462 driver: CODEX_PROVIDER,
7463 providerThreadId: threadInput.sourceProviderThread.id,
7464 cause: `Cannot fork Codex thread ${threadId} at provider turn ${threadInput.providerTurnId}: the source turn has no native Codex turn reference, and the forked thread uses legacy history which Codex 0.156 cannot revert.`,
7465 });
7466 }
7467 forkedThread = (yield* ensureInitialized.pipe(
7468 Effect.andThen(
7469 revertCodexThread(client, response.thread.id, boundary.rollbackTurnCount),
7470 ),
7471 Effect.mapError(
7472 (cause) =>
7473 new ProviderAdapter.ProviderAdapterForkThreadError({
7474 driver: CODEX_PROVIDER,
7475 providerThreadId: threadInput.sourceProviderThread.id,
7476 cause: normalizeCodexCause(cause),
7477 }),
7478 ),
7479 )).thread;
7480 }
7481 return providerThreadFromCodexThread({
7482 appThreadId: threadInput.targetThreadId,
7483 idAllocator,
7484 ownerNodeId: threadInput.ownerNodeId ?? null,
7485 providerSessionId: input.providerSessionId,
7486 providerInstanceId: adapterOptions.instanceId,
7487 thread: forkedThread,
7488 forkedFrom: {
7489 providerThreadId: threadInput.sourceProviderThread.id,
7490 ...(threadInput.providerTurnId === undefined
7491 ? {}
7492 : { providerTurnId: threadInput.providerTurnId }),
7493 },
7494 });
7495 }).pipe(
7496 Effect.mapError(
7497 (cause) =>
7498 new ProviderAdapter.ProviderAdapterForkThreadError({
7499 driver: CODEX_PROVIDER,
7500 providerThreadId: threadInput.sourceProviderThread.id,
7501 cause: normalizeCodexCause(cause),
7502 }),
7503 ),
7504 ),
7505 };
7506 return runtime;
7507 }).pipe(
7508 Effect.mapError(
7509 (cause) =>
7510 new ProviderAdapter.ProviderAdapterOpenSessionError({
7511 driver: CODEX_PROVIDER,
7512 providerSessionId: input.providerSessionId,
7513 cause,
7514 }),
7515 ),
7516 ),
7517 });
7518});