1423 lines · 48.2 KB · TypeScript
RawDownload
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.