2743 lines · 89.8 KB · TypeScript
RawDownload
/** * CodexAdapterLive - Scoped live implementation for the Codex provider adapter. * * Wraps the typed Codex session runtime behind the `CodexAdapter` service * contract and maps runtime failures into the shared `ProviderAdapterError` * algebra. * * @module CodexAdapterLive */import {  EventId,  type CanonicalItemType,  type CanonicalRequestType,  type CodexSettings,  ProviderDriverKind,  type ProviderEvent,  ProviderInstanceId,  type ProviderRuntimeEvent,  type ProviderRequestKind,  type ThreadTokenUsageSnapshot,  type ToolActivityIcon,  type ToolActivityNativeAppReference,  type ToolActivitySource,  type ProviderUserInputAnswers,  RuntimeItemId,  RuntimeRequestId,  RuntimeTaskId,  type RuntimeTaskUsage,  type TurnTokenUsage,  ProviderApprovalDecision,  ThreadId,  ProviderSendTurnInput,} from "@t3tools/contracts";import * as Effect from "effect/Effect";import * as NodeCrypto from "node:crypto";import * as Crypto from "effect/Crypto";import * as Exit from "effect/Exit";import * as Fiber from "effect/Fiber";import * as FileSystem from "effect/FileSystem";import * as Queue from "effect/Queue";import * as Schema from "effect/Schema";import * as Scope from "effect/Scope";import * as Stream from "effect/Stream";import { ChildProcessSpawner } from "effect/unstable/process";import * as CodexErrors from "effect-codex-app-server/errors";import * as EffectCodexSchema from "effect-codex-app-server/schema"; import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";import { getCodexServiceTierOptionValue } from "../../codexModelOptions.ts";import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; import {  ProviderAdapterRequestError,  ProviderAdapterProcessError,  ProviderAdapterSessionClosedError,  ProviderAdapterSessionNotFoundError,  ProviderAdapterValidationError,  type ProviderAdapterError,} from "../Errors.ts";import { type CodexAdapterShape } from "../Services/CodexAdapter.ts";import { resolveAttachmentPath } from "../../attachmentStore.ts";import { ServerConfig } from "../../config.ts";import {  CodexResumeCursorSchema,  CodexSessionRuntimeThreadIdMissingError,  describeMcpElicitation,  makeCodexSessionRuntime,  type CodexSessionRuntimeError,  type CodexSessionRuntimeOptions,  type CodexSessionRuntimeShape,} from "./CodexSessionRuntime.ts";import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";import { resolveCodexLaunchArgs } from "./codexLaunchArgs.ts";import {  type CodexRateLimitSnapshot,  codexRateLimitsToUpdate,  codexUsageLimitMessage,  mergeCodexRateLimits,} from "./codexUsageLimits.ts";const isCodexAppServerProcessExitedError = Schema.is(CodexErrors.CodexAppServerProcessExitedError);const isCodexAppServerTransportError = Schema.is(CodexErrors.CodexAppServerTransportError);const isCodexSessionRuntimeThreadIdMissingError = Schema.is(  CodexSessionRuntimeThreadIdMissingError,);const isCodexResumeCursorSchema = Schema.is(CodexResumeCursorSchema); const PROVIDER = ProviderDriverKind.make("codex"); export interface CodexAdapterLiveOptions {  readonly instanceId?: ProviderInstanceId;  readonly environment?: NodeJS.ProcessEnv;  readonly makeRuntime?: (    options: CodexSessionRuntimeOptions,  ) => Effect.Effect<    CodexSessionRuntimeShape,    CodexSessionRuntimeError,    ChildProcessSpawner.ChildProcessSpawner | Scope.Scope  >;  readonly nativeEventLogPath?: string;  readonly nativeEventLogger?: EventNdjsonLogger;} interface CodexAdapterSessionContext {  readonly threadId: ThreadId;  readonly scope: Scope.Closeable;  readonly runtime: CodexSessionRuntimeShape;  readonly eventFiber: Fiber.Fiber<void, never>;  readonly turnTokenUsage: CodexTurnTokenUsageState;  stopped: boolean;} type CodexCumulativeTokenUsage = {  readonly inputTokens: number;  readonly cachedInputTokens: number;  readonly cacheCreationTokens?: number;  readonly outputTokens: number;  readonly reasoningTokens: number;}; interface CodexTurnTokenUsageAccumulator {  inputTokens: number;  cachedInputTokens: number;  cacheCreationTokens: number | undefined;  outputTokens: number;  reasoningTokens: number;  observed: boolean;  hasSubagents: boolean;} interface CodexTurnTokenUsageState {  baseline: CodexCumulativeTokenUsage | undefined;  activeTurnId: string | undefined;  readonly byTurnId: Map<string, CodexTurnTokenUsageAccumulator>;} function mapCodexRuntimeError(  threadId: ThreadId,  method: string,  error: CodexSessionRuntimeError,): ProviderAdapterError {  if (isCodexAppServerProcessExitedError(error) || isCodexAppServerTransportError(error)) {    return new ProviderAdapterSessionClosedError({      provider: PROVIDER,      threadId,      cause: error,    });  }   if (isCodexSessionRuntimeThreadIdMissingError(error)) {    return new ProviderAdapterSessionNotFoundError({      provider: PROVIDER,      threadId,      cause: error,    });  }   return new ProviderAdapterRequestError({    provider: PROVIDER,    method,    detail: error.message,    cause: error,  });} type CodexLifecycleItem =  | EffectCodexSchema.V2ItemStartedNotification["item"]  | EffectCodexSchema.V2ItemCompletedNotification["item"]; type CodexToolUserInputQuestion =  | EffectCodexSchema.ServerRequest__ToolRequestUserInputQuestion  | EffectCodexSchema.ToolRequestUserInputParams__ToolRequestUserInputQuestion; const ApprovalDecisionPayload = Schema.Struct({  decision: ProviderApprovalDecision,}); function readPayload<A>(  schema: Schema.Schema<A>,  payload: ProviderEvent["payload"],): A | undefined {  const isPayload = Schema.is(schema);  return isPayload(payload) ? payload : undefined;} function trimText(value: string | undefined | null): string | undefined {  const trimmed = value?.trim();  return trimmed && trimmed.length > 0 ? trimmed : undefined;} function asUnknownRecord(value: unknown): Record<string, unknown> | undefined {  return value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;} function normalizeMcpIntentTitle(value: unknown): string | undefined {  if (typeof value !== "string") return undefined;  const normalized = value.trim().replace(/\s+/gu, " ");  if (!normalized) return undefined;  const characters = Array.from(normalized);  return characters.length <= 80 ? normalized : `${characters.slice(0, 79).join("")}…`;} function normalizedHttpUrl(value: unknown): string | undefined {  if (typeof value !== "string" || value.length > 4096) return undefined;  try {    const url = new URL(value);    const href = url.href;    return (url.protocol === "http:" || url.protocol === "https:") && href.length <= 4096      ? href      : undefined;  } catch {    return undefined;  }} function normalizedImageUrl(value: unknown): string | undefined {  if (typeof value !== "string" || value.length > 4096) return undefined;  try {    const url = new URL(value);    return url.protocol === "http:" || url.protocol === "https:" || url.protocol === "data:"      ? url.href      : undefined;  } catch {    return undefined;  }} function normalizedAppId(value: unknown): string | undefined {  if (typeof value !== "string") return undefined;  const appId = value.trim();  return appId.length > 0 && appId.length <= 512 && /^[A-Za-z0-9._-]+$/u.test(appId)    ? appId    : undefined;} function normalizedDisplayName(value: unknown): string | undefined {  if (typeof value !== "string") return undefined;  const displayName = value.trim().replace(/\s+/gu, " ");  return displayName && displayName.length <= 160 ? displayName : undefined;} function normalizedSourceKeyPart(value: string): string {  return value.trim().toLowerCase();} function nativeAppSourceKey(appId: string): string {  const key = `native-app:${appId.toLowerCase()}`;  if (key.length <= 512) return key;  const digest = NodeCrypto.createHash("sha256").update(key).digest("hex");  return `${key.slice(0, 512 - digest.length - 1)}:${digest}`;} function browserDisplayName(value: unknown): string | undefined {  const normalized = normalizedDisplayName(value)?.toLowerCase();  if (!normalized) return undefined;  if (normalized.includes("chrome") || normalized === "chromium") return "Chrome";  if (normalized.includes("edge")) return "Microsoft Edge";  if (normalized.includes("firefox")) return "Firefox";  if (normalized.includes("safari")) return "Safari";  if (normalized.includes("arc")) return "Arc";  if (normalized === "iab" || normalized.includes("in-app")) return "Browser";  return normalizedDisplayName(value);} function browserNativeAppReference(name: string): ToolActivityNativeAppReference | undefined {  switch (name) {    case "Chrome":      return { _tag: "display-name", displayName: "Google Chrome" };    case "Microsoft Edge":    case "Firefox":    case "Safari":    case "Arc":      return { _tag: "display-name", displayName: name };    default:      return undefined;  }} function appDisplayNameFromId(appId: string): string | undefined {  const knownNames: Readonly<Record<string, string>> = {    "com.apple.finder": "Finder",    "com.apple.safari": "Safari",    "com.google.chrome": "Chrome",    "com.microsoft.edgemac": "Microsoft Edge",    "org.mozilla.firefox": "Firefox",    "company.thebrowser.browser": "Arc",  };  return knownNames[appId.toLowerCase()];} function nativeAppReference(value: unknown): ToolActivityNativeAppReference | undefined {  const app = asUnknownRecord(value);  if (app?.kind === "appId") {    const appId = normalizedAppId(app.appId);    return appId ? { _tag: "app-id", appId } : undefined;  }  if (app?.kind === "displayName") {    const displayName = normalizedDisplayName(app.displayName);    return displayName ? { _tag: "display-name", displayName } : undefined;  }  return undefined;} function themedLogoIcon(  ...records: ReadonlyArray<Record<string, unknown> | undefined>): ToolActivityIcon | undefined {  for (const record of records) {    const logoUrl = normalizedImageUrl(record?.logoUrl);    if (!logoUrl) continue;    const logoUrlDark = normalizedImageUrl(record?.logoUrlDark ?? record?.logoDarkUrl);    return {      _tag: "themed-logo",      logoUrl,      ...(logoUrlDark ? { logoUrlDark } : {}),    };  }  return undefined;} interface McpToolPresentation {  readonly toolSurface?: "browser" | "computer";  readonly toolIcon?: ToolActivityIcon;  readonly toolSource?: ToolActivitySource;} function mcpToolPresentation(  item: Extract<CodexLifecycleItem, { readonly type: "mcpToolCall" }>,): McpToolPresentation {  const result = asUnknownRecord(item.result);  const metadata = asUnknownRecord(result?._meta);  const surface = asUnknownRecord(metadata?.["codex/toolSurface"]);  const sourceMetadata = asUnknownRecord(metadata?.source);  const appContext = asUnknownRecord(item.appContext);  const sourceLogo = themedLogoIcon(surface, sourceMetadata, appContext);  if (surface?.kind === "browserUse") {    const screenshot = asUnknownRecord(surface.screenshot);    const browserUse = asUnknownRecord(metadata?.browser_use);    const openTabs = Array.isArray(surface.openTabs) ? surface.openTabs : [];    const latestOpenTab = openTabs      .toReversed()      .map(asUnknownRecord)      .find((tab) => normalizedHttpUrl(tab?.url) !== undefined);    const selectedPage = [      { record: screenshot, url: screenshot?.pageUrl },      { record: browserUse, url: browserUse?.url },      { record: latestOpenTab, url: latestOpenTab?.url },    ]      .map((candidate) => ({ ...candidate, pageUrl: normalizedHttpUrl(candidate.url) }))      .find((candidate) => candidate.pageUrl !== undefined);    const pageUrl = selectedPage?.pageUrl;    const faviconUrl = normalizedImageUrl(      selectedPage?.record?.faviconUrl ?? selectedPage?.record?.favIconUrl,    );    const faviconUrlDark = normalizedImageUrl(      selectedPage?.record?.faviconUrlDark ?? selectedPage?.record?.favIconUrlDark,    );    const name =      browserDisplayName(appContext?.appName) ??      browserDisplayName(surface.browserFamily) ??      browserDisplayName(surface.backend) ??      "Browser";    const nativeBrowserIcon = browserNativeAppReference(name);    const sourceIcon =      sourceLogo ??      (nativeBrowserIcon ? ({ _tag: "native-app", app: nativeBrowserIcon } as const) : undefined);    const sourceKeyPart = normalizedSourceKeyPart(name) || "browser";    return {      toolSurface: "browser",      ...(pageUrl        ? {            toolIcon: {              _tag: "website",              pageUrl,              ...(faviconUrl ? { faviconUrl } : {}),              ...(faviconUrlDark ? { faviconUrlDark } : {}),            } as const,          }        : {}),      toolSource: {        key: `browser-use:${sourceKeyPart}`,        name,        kind: name === "Browser" ? "browser" : "integration",        ...(sourceIcon ? { icon: sourceIcon } : {}),      },    };  }  if (surface?.kind === "computerUse") {    const app = nativeAppReference(surface.app);    const args = asUnknownRecord(item.arguments);    const argumentAppName =      normalizedDisplayName(args?.appName) ??      normalizedDisplayName(args?.application) ??      normalizedDisplayName(typeof args?.app === "string" ? args.app : undefined);    const name =      normalizedDisplayName(appContext?.appName) ??      argumentAppName ??      (app?._tag === "display-name" ? app.displayName : undefined) ??      (app?._tag === "app-id" ? appDisplayNameFromId(app.appId) : undefined) ??      "Computer Use";    const sourceIcon = sourceLogo ?? (app ? ({ _tag: "native-app", app } as const) : undefined);    const sourceKey = app      ? app._tag === "app-id"        ? nativeAppSourceKey(app.appId)        : `native-app-name:${normalizedSourceKeyPart(app.displayName)}`      : "computer-use";    return {      toolSurface: "computer",      ...(app ? { toolIcon: { _tag: "native-app", app } as const } : {}),      toolSource: {        key: sourceKey,        name,        kind: "computer",        ...(sourceIcon ? { icon: sourceIcon } : {}),      },    };  }   return {};} const FATAL_CODEX_STDERR_SNIPPETS = ["failed to connect to websocket"]; function isFatalCodexProcessStderrMessage(message: string): boolean {  const normalized = message.toLowerCase();  return FATAL_CODEX_STDERR_SNIPPETS.some((snippet) => normalized.includes(snippet));} function normalizeCodexTokenUsage(  usage: EffectCodexSchema.V2ThreadTokenUsageUpdatedNotification["tokenUsage"],): ThreadTokenUsageSnapshot | undefined {  const totalProcessedTokens = usage.total.totalTokens;  const usedTokens = usage.last.totalTokens;  if (usedTokens === undefined || usedTokens <= 0) {    return undefined;  }   const maxTokens = usage.modelContextWindow ?? undefined;  const inputTokens = usage.last.inputTokens;  const cachedInputTokens = usage.last.cachedInputTokens;  const outputTokens = usage.last.outputTokens;  const reasoningOutputTokens = usage.last.reasoningOutputTokens;   return {    usedTokens,    ...(totalProcessedTokens !== undefined && totalProcessedTokens > usedTokens      ? { totalProcessedTokens }      : {}),    ...(maxTokens !== undefined ? { maxTokens } : {}),    ...(inputTokens !== undefined ? { inputTokens } : {}),    ...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),    ...(outputTokens !== undefined ? { outputTokens } : {}),    ...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),    ...(usedTokens !== undefined ? { lastUsedTokens: usedTokens } : {}),    ...(inputTokens !== undefined ? { lastInputTokens: inputTokens } : {}),    ...(cachedInputTokens !== undefined ? { lastCachedInputTokens: cachedInputTokens } : {}),    ...(outputTokens !== undefined ? { lastOutputTokens: outputTokens } : {}),    ...(reasoningOutputTokens !== undefined      ? { lastReasoningOutputTokens: reasoningOutputTokens }      : {}),    compactsAutomatically: true,  };} function codexTokenUsageBreakdown(  usage: EffectCodexSchema.V2ThreadTokenUsageUpdatedNotification__TokenUsageBreakdown,): CodexCumulativeTokenUsage {  return {    inputTokens: usage.inputTokens,    cachedInputTokens: usage.cachedInputTokens,    ...(usage.cacheWriteInputTokens !== undefined      ? { cacheCreationTokens: usage.cacheWriteInputTokens }      : {}),    outputTokens: usage.outputTokens,    reasoningTokens: usage.reasoningOutputTokens,  };} function makeCodexTurnTokenUsageState(): CodexTurnTokenUsageState {  return {    baseline: undefined,    activeTurnId: undefined,    byTurnId: new Map(),  };} function getCodexTurnAccumulator(  state: CodexTurnTokenUsageState,  turnId: string,): CodexTurnTokenUsageAccumulator {  const existing = state.byTurnId.get(turnId);  if (existing) return existing;  const created: CodexTurnTokenUsageAccumulator = {    inputTokens: 0,    cachedInputTokens: 0,    cacheCreationTokens: 0,    outputTokens: 0,    reasoningTokens: 0,    observed: false,    hasSubagents: false,  };  state.byTurnId.set(turnId, created);

Lines 1–500 of 2743.