import { ApprovalRequestId, DEFAULT_MODEL, EventId, ProviderDriverKind, ProviderItemId, type ProviderInstanceId, type ProviderApprovalDecision, type ProviderEvent, type ProviderInteractionMode, type ProviderRequestKind, type ProviderSession, type ProviderTurnStartResult, type ProviderUserInputAnswers, RuntimeMode, ThreadId, TurnId,} from "@t3tools/contracts";import { resolveSpawnCommand } from "@t3tools/shared/shell";import { normalizeModelSlug } from "@t3tools/shared/model";import * as Crypto from "effect/Crypto";import * as DateTime from "effect/DateTime";import * as Deferred from "effect/Deferred";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Layer from "effect/Layer";import * as Queue from "effect/Queue";import * as Ref from "effect/Ref";import * as Schema from "effect/Schema";import * as Scope from "effect/Scope";import * as Stream from "effect/Stream";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";import * as CodexClient from "effect-codex-app-server/client";import * as CodexErrors from "effect-codex-app-server/errors";import * as CodexRpc from "effect-codex-app-server/rpc";import * as EffectCodexSchema from "effect-codex-app-server/schema"; import { buildCodexInitializeParams } from "./CodexProvider.ts";import { codexSessionAppServerArgs } from "./codexLaunchArgs.ts";import { expandHomePath } from "../../pathExpansion.ts";import { buildCodexDeveloperInstructions } from "../CodexDeveloperInstructions.ts";const decodeV2TurnStartResponse = Schema.decodeUnknownEffect(EffectCodexSchema.V2TurnStartResponse); const PROVIDER = ProviderDriverKind.make("codex"); const ANSI_ESCAPE_CHAR = String.fromCharCode(27);const ANSI_ESCAPE_REGEX = new RegExp(`${ANSI_ESCAPE_CHAR}\\[[0-9;]*m`, "g");const CODEX_STDERR_LOG_REGEX = /^\d{4}-\d{2}-\d{2}T\S+\s+(TRACE|DEBUG|INFO|WARN|ERROR)\s+\S+:\s+(.*)$/;const BENIGN_ERROR_LOG_SNIPPETS = [ "state db missing rollout path for thread", "state db record_discrepancy: find_thread_path_by_id_str_in_subdir, falling_back",];const CODEX_APP_SERVER_FORCE_KILL_AFTER = "2 seconds" as const;const RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS = [ "not found", "missing thread", "no such thread", "unknown thread", "does not exist",]; export function hasConfiguredMcpServer(appServerArgs: ReadonlyArray<string> | undefined): boolean { return appServerArgs?.some((argument) => argument.includes("mcp_servers.")) === true;} export const CodexResumeCursorSchema = Schema.Struct({ threadId: Schema.String,});const CodexUserInputAnswerObject = Schema.Struct({ answers: Schema.Array(Schema.String),});const isCodexResumeCursorSchema = Schema.is(CodexResumeCursorSchema);const isCodexUserInputAnswerObject = Schema.is(CodexUserInputAnswerObject); // TODO: Verify `packages/effect-codex-app-server/scripts/generate.ts` so the generated// `V2TurnStartParams` schema includes `collaborationMode` directly.const CodexTurnStartParamsWithCollaborationMode = EffectCodexSchema.V2TurnStartParams.pipe( Schema.fieldsAssign({ collaborationMode: Schema.optionalKey(EffectCodexSchema.V2TurnStartParams__CollaborationMode), }),);const decodeCodexTurnStartParamsWithCollaborationMode = Schema.decodeUnknownEffect( CodexTurnStartParamsWithCollaborationMode,); export type CodexTurnStartParamsWithCollaborationMode = typeof CodexTurnStartParamsWithCollaborationMode.Type; export type CodexResumeCursor = typeof CodexResumeCursorSchema.Type;type CodexServiceTier = NonNullable<EffectCodexSchema.V2ThreadStartParams["serviceTier"]>;type CodexThreadItem = | EffectCodexSchema.V2ThreadReadResponse["thread"]["turns"][number]["items"][number] | EffectCodexSchema.V2ThreadRollbackResponse["thread"]["turns"][number]["items"][number]; export interface CodexSessionRuntimeOptions { readonly threadId: ThreadId; readonly providerInstanceId?: ProviderInstanceId; readonly binaryPath: string; readonly homePath?: string; readonly launchArgs?: string; readonly environment?: NodeJS.ProcessEnv; readonly cwd: string; readonly runtimeMode: RuntimeMode; readonly model?: string; readonly serviceTier?: CodexServiceTier | undefined; readonly resumeCursor?: CodexResumeCursor; readonly appServerArgs?: ReadonlyArray<string>;} export interface CodexSessionRuntimeSendTurnInput { readonly input?: string; readonly attachments?: ReadonlyArray<{ readonly type: "image"; readonly url: string; }>; readonly model?: string; readonly serviceTier?: CodexServiceTier | undefined; readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort | undefined; readonly interactionMode?: ProviderInteractionMode;} export interface CodexThreadTurnSnapshot { readonly id: TurnId; readonly items: ReadonlyArray<CodexThreadItem>;} export interface CodexThreadSnapshot { readonly threadId: string; readonly turns: ReadonlyArray<CodexThreadTurnSnapshot>;} export interface CodexSessionRuntimeShape { readonly start: () => Effect.Effect<ProviderSession, CodexSessionRuntimeError>; readonly getSession: Effect.Effect<ProviderSession>; readonly sendTurn: ( input: CodexSessionRuntimeSendTurnInput, ) => Effect.Effect<ProviderTurnStartResult, CodexSessionRuntimeError>; readonly interruptTurn: (turnId?: TurnId) => Effect.Effect<void, CodexSessionRuntimeError>; readonly readThread: Effect.Effect<CodexThreadSnapshot, CodexSessionRuntimeError>; readonly rollbackThread: ( numTurns: number, ) => Effect.Effect<CodexThreadSnapshot, CodexSessionRuntimeError>; readonly respondToRequest: ( requestId: ApprovalRequestId, decision: ProviderApprovalDecision, ) => Effect.Effect<void, CodexSessionRuntimeError>; readonly respondToUserInput: ( requestId: ApprovalRequestId, answers: ProviderUserInputAnswers, ) => Effect.Effect<void, CodexSessionRuntimeError>; readonly events: Stream.Stream<ProviderEvent, never>; readonly close: Effect.Effect<void>;} export type CodexSessionRuntimeError = | CodexErrors.CodexAppServerError | CodexSessionRuntimePendingApprovalNotFoundError | CodexSessionRuntimePendingUserInputNotFoundError | CodexSessionRuntimeInvalidUserInputAnswersError | CodexSessionRuntimeThreadIdMissingError; export class CodexSessionRuntimePendingApprovalNotFoundError extends Schema.TaggedErrorClass<CodexSessionRuntimePendingApprovalNotFoundError>()( "CodexSessionRuntimePendingApprovalNotFoundError", { requestId: Schema.String, },) { override get message(): string { return `Unknown pending Codex approval request: ${this.requestId}`; }} export class CodexSessionRuntimePendingUserInputNotFoundError extends Schema.TaggedErrorClass<CodexSessionRuntimePendingUserInputNotFoundError>()( "CodexSessionRuntimePendingUserInputNotFoundError", { requestId: Schema.String, },) { override get message(): string { return `Unknown pending Codex user input request: ${this.requestId}`; }} export class CodexSessionRuntimeInvalidUserInputAnswersError extends Schema.TaggedErrorClass<CodexSessionRuntimeInvalidUserInputAnswersError>()( "CodexSessionRuntimeInvalidUserInputAnswersError", { questionId: Schema.String, },) { override get message(): string { return `Invalid Codex user input answers for question '${this.questionId}'`; }} export class CodexSessionRuntimeThreadIdMissingError extends Schema.TaggedErrorClass<CodexSessionRuntimeThreadIdMissingError>()( "CodexSessionRuntimeThreadIdMissingError", { threadId: Schema.String, },) { override get message(): string { return `Codex session is missing a provider thread id for ${this.threadId}`; }} interface PendingApproval { readonly requestId: ApprovalRequestId; readonly jsonRpcId: string; readonly requestKind: ProviderRequestKind; readonly turnId: TurnId | undefined; readonly itemId: ProviderItemId | undefined; readonly decision: Deferred.Deferred<ProviderApprovalDecision>;} interface ApprovalCorrelation { readonly requestId: ApprovalRequestId; readonly requestKind: ProviderRequestKind; readonly turnId: TurnId | undefined; readonly itemId: ProviderItemId | undefined;} interface PendingUserInput { readonly requestId: ApprovalRequestId; readonly turnId: TurnId | undefined; readonly itemId: ProviderItemId | undefined; readonly answers: Deferred.Deferred<ProviderUserInputAnswers>;} type CodexServerNotification = { readonly [M in CodexRpc.ServerNotificationMethod]: { readonly method: M; readonly params: CodexRpc.ServerNotificationParamsByMethod[M]; };}[CodexRpc.ServerNotificationMethod]; function makeCodexServerNotification<M extends CodexRpc.ServerNotificationMethod>( method: M, params: CodexRpc.ServerNotificationParamsByMethod[M],): CodexServerNotification { return { method, params } as CodexServerNotification;} function normalizeCodexModelSlug( model: string | undefined | null, preferredId?: string,): string | undefined { const normalized = normalizeModelSlug(model); if (!normalized) { return undefined; } if (preferredId?.endsWith("-codex") && preferredId !== normalized) { return preferredId; } return normalized;} function readResumeCursorThreadId( resumeCursor: ProviderSession["resumeCursor"],): string | undefined { return isCodexResumeCursorSchema(resumeCursor) ? resumeCursor.threadId : undefined;} function runtimeModeToThreadConfig(input: RuntimeMode): { readonly approvalPolicy: EffectCodexSchema.V2ThreadStartParams__AskForApproval; readonly sandbox: EffectCodexSchema.V2ThreadStartParams__SandboxMode; // Always explicit: omitting the field on resume keeps the thread's previous // reviewer, which would leave auto_review sticky after switching modes. readonly approvalsReviewer: EffectCodexSchema.V2ThreadStartParams__ApprovalsReviewer;} { switch (input) { case "approval-required": return { approvalPolicy: "untrusted", sandbox: "read-only", approvalsReviewer: "user", }; case "auto-accept-edits": return { approvalPolicy: "on-request", sandbox: "workspace-write", approvalsReviewer: "user", }; case "auto": return { approvalPolicy: "on-request", sandbox: "workspace-write", approvalsReviewer: "auto_review", }; case "full-access": default: return { approvalPolicy: "never", sandbox: "danger-full-access", approvalsReviewer: "user", }; }} function buildThreadStartParams(input: { readonly cwd: string; readonly runtimeMode: RuntimeMode; readonly model: string | undefined; readonly serviceTier: CodexServiceTier | undefined;}): EffectCodexSchema.V2ThreadStartParams { const config = runtimeModeToThreadConfig(input.runtimeMode); return { cwd: input.cwd, approvalPolicy: config.approvalPolicy, sandbox: config.sandbox, approvalsReviewer: config.approvalsReviewer, ...(input.model ? { model: input.model } : {}), ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}), };} function runtimeModeToTurnSandboxPolicy( input: RuntimeMode,): EffectCodexSchema.V2TurnStartParams__SandboxPolicy { switch (input) { case "approval-required": return { type: "readOnly", }; case "auto-accept-edits": case "auto": return { type: "workspaceWrite", }; case "full-access": default: return { type: "dangerFullAccess", }; }} function buildCodexCollaborationMode(input: { readonly interactionMode?: ProviderInteractionMode; readonly model?: string; readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort;}): EffectCodexSchema.V2TurnStartParams__CollaborationMode | undefined { if (input.interactionMode === undefined) { return undefined; } const model = normalizeCodexModelSlug(input.model) ?? DEFAULT_MODEL; const reasoningEffort = input.effort ?? "medium"; return { mode: input.interactionMode, settings: { model, reasoning_effort: reasoningEffort, developer_instructions: buildCodexDeveloperInstructions(input.interactionMode, { model, reasoningEffort, }), }, };} export function buildTurnStartParams(input: { readonly threadId: string; readonly runtimeMode: RuntimeMode; readonly prompt?: string; readonly attachments?: ReadonlyArray<{ readonly type: "image"; readonly url: string; }>; readonly model?: string; readonly serviceTier?: CodexServiceTier; readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort; readonly interactionMode?: ProviderInteractionMode;}): Effect.Effect< CodexTurnStartParamsWithCollaborationMode, CodexErrors.CodexAppServerProtocolParseError> { const turnInput: Array<EffectCodexSchema.V2TurnStartParams__UserInput> = []; if (input.prompt) { turnInput.push({ type: "text", text: input.prompt, }); } for (const attachment of input.attachments ?? []) { turnInput.push(attachment); } const config = runtimeModeToThreadConfig(input.runtimeMode); const collaborationMode = buildCodexCollaborationMode({ ...(input.interactionMode ? { interactionMode: input.interactionMode } : {}), ...(input.model ? { model: input.model } : {}), ...(input.effort ? { effort: input.effort } : {}), }); return decodeCodexTurnStartParamsWithCollaborationMode({ threadId: input.threadId, input: turnInput, approvalPolicy: config.approvalPolicy, approvalsReviewer: config.approvalsReviewer, sandboxPolicy: runtimeModeToTurnSandboxPolicy(input.runtimeMode), ...(input.model ? { model: input.model } : {}), ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}), ...(input.effort ? { effort: input.effort } : {}), ...(collaborationMode ? { collaborationMode } : {}), }).pipe( Effect.mapError((cause) => CodexErrors.CodexAppServerProtocolParseError.fromSchemaError( "decode-request-payload", cause, { method: "turn/start" }, ), ), );} function classifyCodexStderrLine(rawLine: string): { readonly message: string } | null { const line = rawLine.replaceAll(ANSI_ESCAPE_REGEX, "").trim(); if (!line) { return null; } const match = line.match(CODEX_STDERR_LOG_REGEX); if (match) { const level = match[1]; if (level && level !== "ERROR") { return null; } if (BENIGN_ERROR_LOG_SNIPPETS.some((snippet) => line.includes(snippet))) { return null; } } return { message: line };} export function isRecoverableThreadResumeError(error: unknown): boolean { const message = (error instanceof Error ? error.message : String(error)).toLowerCase(); if (!message.includes("thread")) { return false; } return RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS.some((snippet) => message.includes(snippet));} type CodexThreadOpenResponse = | CodexRpc.ClientRequestResponsesByMethod["thread/start"] | CodexRpc.ClientRequestResponsesByMethod["thread/resume"]; type CodexThreadOpenMethod = "thread/start" | "thread/resume"; interface CodexThreadOpenClient { readonly request: <M extends CodexThreadOpenMethod>( method: M, payload: CodexRpc.ClientRequestParamsByMethod[M], ) => Effect.Effect<CodexRpc.ClientRequestResponsesByMethod[M], CodexErrors.CodexAppServerError>;} export const openCodexThread = (input: { readonly client: CodexThreadOpenClient; readonly threadId: ThreadId; readonly runtimeMode: RuntimeMode; readonly cwd: string; readonly requestedModel: string | undefined; readonly serviceTier: CodexServiceTier | undefined; readonly resumeThreadId: string | undefined;}): Effect.Effect<CodexThreadOpenResponse, CodexErrors.CodexAppServerError> => { const resumeThreadId = input.resumeThreadId; const startParams = buildThreadStartParams({ cwd: input.cwd, runtimeMode: input.runtimeMode, model: input.requestedModel, serviceTier: input.serviceTier, }); if (resumeThreadId === undefined) { return input.client.request("thread/start", startParams); } return input.client .request("thread/resume", { threadId: resumeThreadId, ...startParams, }) .pipe( Effect.catchIf(isRecoverableThreadResumeError, (error) => Effect.logWarning("codex app-server thread resume fell back to fresh start", { threadId: input.threadId, requestedRuntimeMode: input.runtimeMode, resumeThreadId, recoverable: true, cause: error, }).pipe(Effect.andThen(input.client.request("thread/start", startParams))), ), );}; function readNotificationThreadId(notification: CodexServerNotification): string | undefined { switch (notification.method) { case "thread/started": return notification.params.thread.id; case "error":Lines 1–500 of 1423. Load more
import { ApprovalRequestId, DEFAULT_MODEL, EventId, ProviderDriverKind, ProviderItemId, type ProviderInstanceId, type ProviderApprovalDecision, type ProviderEvent, type ProviderInteractionMode, type ProviderRequestKind, type ProviderSession, type ProviderTurnStartResult, type ProviderUserInputAnswers, RuntimeMode, ThreadId, TurnId, } from "@t3tools/contracts"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; import { normalizeModelSlug } from "@t3tools/shared/model"; import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Layer from "effect/Layer"; import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import * as CodexClient from "effect-codex-app-server/client"; import * as CodexErrors from "effect-codex-app-server/errors"; import * as CodexRpc from "effect-codex-app-server/rpc"; import * as EffectCodexSchema from "effect-codex-app-server/schema"; import { buildCodexInitializeParams } from "./CodexProvider.ts"; import { codexSessionAppServerArgs } from "./codexLaunchArgs.ts"; import { expandHomePath } from "../../pathExpansion.ts"; import { buildCodexDeveloperInstructions } from "../CodexDeveloperInstructions.ts"; const decodeV2TurnStartResponse = Schema.decodeUnknownEffect(EffectCodexSchema.V2TurnStartResponse); const PROVIDER = ProviderDriverKind.make("codex"); const ANSI_ESCAPE_CHAR = String.fromCharCode(27); const ANSI_ESCAPE_REGEX = new RegExp(`${ANSI_ESCAPE_CHAR}\\[[0-9;]*m`, "g"); const CODEX_STDERR_LOG_REGEX = /^\d{4}-\d{2}-\d{2}T\S+\s+(TRACE|DEBUG|INFO|WARN|ERROR)\s+\S+:\s+(.*)$/; const BENIGN_ERROR_LOG_SNIPPETS = [ "state db missing rollout path for thread", "state db record_discrepancy: find_thread_path_by_id_str_in_subdir, falling_back", ]; const CODEX_APP_SERVER_FORCE_KILL_AFTER = "2 seconds" as const; const RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS = [ "not found", "missing thread", "no such thread", "unknown thread", "does not exist", ]; export function hasConfiguredMcpServer(appServerArgs: ReadonlyArray<string> | undefined): boolean { return appServerArgs?.some((argument) => argument.includes("mcp_servers.")) === true; } export const CodexResumeCursorSchema = Schema.Struct({ threadId: Schema.String, }); const CodexUserInputAnswerObject = Schema.Struct({ answers: Schema.Array(Schema.String), }); const isCodexResumeCursorSchema = Schema.is(CodexResumeCursorSchema); const isCodexUserInputAnswerObject = Schema.is(CodexUserInputAnswerObject); // TODO: Verify `packages/effect-codex-app-server/scripts/generate.ts` so the generated // `V2TurnStartParams` schema includes `collaborationMode` directly. const CodexTurnStartParamsWithCollaborationMode = EffectCodexSchema.V2TurnStartParams.pipe( Schema.fieldsAssign({ collaborationMode: Schema.optionalKey(EffectCodexSchema.V2TurnStartParams__CollaborationMode), }), ); const decodeCodexTurnStartParamsWithCollaborationMode = Schema.decodeUnknownEffect( CodexTurnStartParamsWithCollaborationMode, ); export type CodexTurnStartParamsWithCollaborationMode = typeof CodexTurnStartParamsWithCollaborationMode.Type; export type CodexResumeCursor = typeof CodexResumeCursorSchema.Type; type CodexServiceTier = NonNullable<EffectCodexSchema.V2ThreadStartParams["serviceTier"]>; type CodexThreadItem = | EffectCodexSchema.V2ThreadReadResponse["thread"]["turns"][number]["items"][number] | EffectCodexSchema.V2ThreadRollbackResponse["thread"]["turns"][number]["items"][number]; export interface CodexSessionRuntimeOptions { readonly threadId: ThreadId; readonly providerInstanceId?: ProviderInstanceId; readonly binaryPath: string; readonly homePath?: string; readonly launchArgs?: string; readonly environment?: NodeJS.ProcessEnv; readonly cwd: string; readonly runtimeMode: RuntimeMode; readonly model?: string; readonly serviceTier?: CodexServiceTier | undefined; readonly resumeCursor?: CodexResumeCursor; readonly appServerArgs?: ReadonlyArray<string>; } export interface CodexSessionRuntimeSendTurnInput { readonly input?: string; readonly attachments?: ReadonlyArray<{ readonly type: "image"; readonly url: string; }>; readonly model?: string; readonly serviceTier?: CodexServiceTier | undefined; readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort | undefined; readonly interactionMode?: ProviderInteractionMode; } export interface CodexThreadTurnSnapshot { readonly id: TurnId; readonly items: ReadonlyArray<CodexThreadItem>; } export interface CodexThreadSnapshot { readonly threadId: string; readonly turns: ReadonlyArray<CodexThreadTurnSnapshot>; } export interface CodexSessionRuntimeShape { readonly start: () => Effect.Effect<ProviderSession, CodexSessionRuntimeError>; readonly getSession: Effect.Effect<ProviderSession>; readonly sendTurn: ( input: CodexSessionRuntimeSendTurnInput, ) => Effect.Effect<ProviderTurnStartResult, CodexSessionRuntimeError>; readonly interruptTurn: (turnId?: TurnId) => Effect.Effect<void, CodexSessionRuntimeError>; readonly readThread: Effect.Effect<CodexThreadSnapshot, CodexSessionRuntimeError>; readonly rollbackThread: ( numTurns: number, ) => Effect.Effect<CodexThreadSnapshot, CodexSessionRuntimeError>; readonly respondToRequest: ( requestId: ApprovalRequestId, decision: ProviderApprovalDecision, ) => Effect.Effect<void, CodexSessionRuntimeError>; readonly respondToUserInput: ( requestId: ApprovalRequestId, answers: ProviderUserInputAnswers, ) => Effect.Effect<void, CodexSessionRuntimeError>; readonly events: Stream.Stream<ProviderEvent, never>; readonly close: Effect.Effect<void>; } export type CodexSessionRuntimeError = | CodexErrors.CodexAppServerError | CodexSessionRuntimePendingApprovalNotFoundError | CodexSessionRuntimePendingUserInputNotFoundError | CodexSessionRuntimeInvalidUserInputAnswersError | CodexSessionRuntimeThreadIdMissingError; export class CodexSessionRuntimePendingApprovalNotFoundError extends Schema.TaggedErrorClass<CodexSessionRuntimePendingApprovalNotFoundError>()( "CodexSessionRuntimePendingApprovalNotFoundError", { requestId: Schema.String, }, ) { override get message(): string { return `Unknown pending Codex approval request: ${this.requestId}`; } } export class CodexSessionRuntimePendingUserInputNotFoundError extends Schema.TaggedErrorClass<CodexSessionRuntimePendingUserInputNotFoundError>()( "CodexSessionRuntimePendingUserInputNotFoundError", { requestId: Schema.String, }, ) { override get message(): string { return `Unknown pending Codex user input request: ${this.requestId}`; } } export class CodexSessionRuntimeInvalidUserInputAnswersError extends Schema.TaggedErrorClass<CodexSessionRuntimeInvalidUserInputAnswersError>()( "CodexSessionRuntimeInvalidUserInputAnswersError", { questionId: Schema.String, }, ) { override get message(): string { return `Invalid Codex user input answers for question '${this.questionId}'`; } } export class CodexSessionRuntimeThreadIdMissingError extends Schema.TaggedErrorClass<CodexSessionRuntimeThreadIdMissingError>()( "CodexSessionRuntimeThreadIdMissingError", { threadId: Schema.String, }, ) { override get message(): string { return `Codex session is missing a provider thread id for ${this.threadId}`; } } interface PendingApproval { readonly requestId: ApprovalRequestId; readonly jsonRpcId: string; readonly requestKind: ProviderRequestKind; readonly turnId: TurnId | undefined; readonly itemId: ProviderItemId | undefined; readonly decision: Deferred.Deferred<ProviderApprovalDecision>; } interface ApprovalCorrelation { readonly requestId: ApprovalRequestId; readonly requestKind: ProviderRequestKind; readonly turnId: TurnId | undefined; readonly itemId: ProviderItemId | undefined; } interface PendingUserInput { readonly requestId: ApprovalRequestId; readonly turnId: TurnId | undefined; readonly itemId: ProviderItemId | undefined; readonly answers: Deferred.Deferred<ProviderUserInputAnswers>; } type CodexServerNotification = { readonly [M in CodexRpc.ServerNotificationMethod]: { readonly method: M; readonly params: CodexRpc.ServerNotificationParamsByMethod[M]; }; }[CodexRpc.ServerNotificationMethod]; function makeCodexServerNotification<M extends CodexRpc.ServerNotificationMethod>( method: M, params: CodexRpc.ServerNotificationParamsByMethod[M], ): CodexServerNotification { return { method, params } as CodexServerNotification; } function normalizeCodexModelSlug( model: string | undefined | null, preferredId?: string, ): string | undefined { const normalized = normalizeModelSlug(model); if (!normalized) { return undefined; } if (preferredId?.endsWith("-codex") && preferredId !== normalized) { return preferredId; } return normalized; } function readResumeCursorThreadId( resumeCursor: ProviderSession["resumeCursor"], ): string | undefined { return isCodexResumeCursorSchema(resumeCursor) ? resumeCursor.threadId : undefined; } function runtimeModeToThreadConfig(input: RuntimeMode): { readonly approvalPolicy: EffectCodexSchema.V2ThreadStartParams__AskForApproval; readonly sandbox: EffectCodexSchema.V2ThreadStartParams__SandboxMode; // Always explicit: omitting the field on resume keeps the thread's previous // reviewer, which would leave auto_review sticky after switching modes. readonly approvalsReviewer: EffectCodexSchema.V2ThreadStartParams__ApprovalsReviewer; } { switch (input) { case "approval-required": return { approvalPolicy: "untrusted", sandbox: "read-only", approvalsReviewer: "user", }; case "auto-accept-edits": return { approvalPolicy: "on-request", sandbox: "workspace-write", approvalsReviewer: "user", }; case "auto": return { approvalPolicy: "on-request", sandbox: "workspace-write", approvalsReviewer: "auto_review", }; case "full-access": default: return { approvalPolicy: "never", sandbox: "danger-full-access", approvalsReviewer: "user", }; } } function buildThreadStartParams(input: { readonly cwd: string; readonly runtimeMode: RuntimeMode; readonly model: string | undefined; readonly serviceTier: CodexServiceTier | undefined; }): EffectCodexSchema.V2ThreadStartParams { const config = runtimeModeToThreadConfig(input.runtimeMode); return { cwd: input.cwd, approvalPolicy: config.approvalPolicy, sandbox: config.sandbox, approvalsReviewer: config.approvalsReviewer, ...(input.model ? { model: input.model } : {}), ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}), }; } function runtimeModeToTurnSandboxPolicy( input: RuntimeMode, ): EffectCodexSchema.V2TurnStartParams__SandboxPolicy { switch (input) { case "approval-required": return { type: "readOnly", }; case "auto-accept-edits": case "auto": return { type: "workspaceWrite", }; case "full-access": default: return { type: "dangerFullAccess", }; } } function buildCodexCollaborationMode(input: { readonly interactionMode?: ProviderInteractionMode; readonly model?: string; readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort; }): EffectCodexSchema.V2TurnStartParams__CollaborationMode | undefined { if (input.interactionMode === undefined) { return undefined; } const model = normalizeCodexModelSlug(input.model) ?? DEFAULT_MODEL; const reasoningEffort = input.effort ?? "medium"; return { mode: input.interactionMode, settings: { model, reasoning_effort: reasoningEffort, developer_instructions: buildCodexDeveloperInstructions(input.interactionMode, { model, reasoningEffort, }), }, }; } export function buildTurnStartParams(input: { readonly threadId: string; readonly runtimeMode: RuntimeMode; readonly prompt?: string; readonly attachments?: ReadonlyArray<{ readonly type: "image"; readonly url: string; }>; readonly model?: string; readonly serviceTier?: CodexServiceTier; readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort; readonly interactionMode?: ProviderInteractionMode; }): Effect.Effect< CodexTurnStartParamsWithCollaborationMode, CodexErrors.CodexAppServerProtocolParseError > { const turnInput: Array<EffectCodexSchema.V2TurnStartParams__UserInput> = []; if (input.prompt) { turnInput.push({ type: "text", text: input.prompt, }); } for (const attachment of input.attachments ?? []) { turnInput.push(attachment); } const config = runtimeModeToThreadConfig(input.runtimeMode); const collaborationMode = buildCodexCollaborationMode({ ...(input.interactionMode ? { interactionMode: input.interactionMode } : {}), ...(input.model ? { model: input.model } : {}), ...(input.effort ? { effort: input.effort } : {}), }); return decodeCodexTurnStartParamsWithCollaborationMode({ threadId: input.threadId, input: turnInput, approvalPolicy: config.approvalPolicy, approvalsReviewer: config.approvalsReviewer, sandboxPolicy: runtimeModeToTurnSandboxPolicy(input.runtimeMode), ...(input.model ? { model: input.model } : {}), ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}), ...(input.effort ? { effort: input.effort } : {}), ...(collaborationMode ? { collaborationMode } : {}), }).pipe( Effect.mapError((cause) => CodexErrors.CodexAppServerProtocolParseError.fromSchemaError( "decode-request-payload", cause, { method: "turn/start" }, ), ), ); } function classifyCodexStderrLine(rawLine: string): { readonly message: string } | null { const line = rawLine.replaceAll(ANSI_ESCAPE_REGEX, "").trim(); if (!line) { return null; } const match = line.match(CODEX_STDERR_LOG_REGEX); if (match) { const level = match[1]; if (level && level !== "ERROR") { return null; } if (BENIGN_ERROR_LOG_SNIPPETS.some((snippet) => line.includes(snippet))) { return null; } } return { message: line }; } export function isRecoverableThreadResumeError(error: unknown): boolean { const message = (error instanceof Error ? error.message : String(error)).toLowerCase(); if (!message.includes("thread")) { return false; } return RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS.some((snippet) => message.includes(snippet)); } type CodexThreadOpenResponse = | CodexRpc.ClientRequestResponsesByMethod["thread/start"] | CodexRpc.ClientRequestResponsesByMethod["thread/resume"]; type CodexThreadOpenMethod = "thread/start" | "thread/resume"; interface CodexThreadOpenClient { readonly request: <M extends CodexThreadOpenMethod>( method: M, payload: CodexRpc.ClientRequestParamsByMethod[M], ) => Effect.Effect<CodexRpc.ClientRequestResponsesByMethod[M], CodexErrors.CodexAppServerError>; } export const openCodexThread = (input: { readonly client: CodexThreadOpenClient; readonly threadId: ThreadId; readonly runtimeMode: RuntimeMode; readonly cwd: string; readonly requestedModel: string | undefined; readonly serviceTier: CodexServiceTier | undefined; readonly resumeThreadId: string | undefined; }): Effect.Effect<CodexThreadOpenResponse, CodexErrors.CodexAppServerError> => { const resumeThreadId = input.resumeThreadId; const startParams = buildThreadStartParams({ cwd: input.cwd, runtimeMode: input.runtimeMode, model: input.requestedModel, serviceTier: input.serviceTier, }); if (resumeThreadId === undefined) { return input.client.request("thread/start", startParams); } return input.client .request("thread/resume", { threadId: resumeThreadId, ...startParams, }) .pipe( Effect.catchIf(isRecoverableThreadResumeError, (error) => Effect.logWarning("codex app-server thread resume fell back to fresh start", { threadId: input.threadId, requestedRuntimeMode: input.runtimeMode, resumeThreadId, recoverable: true, cause: error, }).pipe(Effect.andThen(input.client.request("thread/start", startParams))), ), ); }; function readNotificationThreadId(notification: CodexServerNotification): string | undefined { switch (notification.method) { case "thread/started": return notification.params.thread.id; case "error":