| import fs from "node:fs/promises"; |
| import path from "node:path"; |
| import { fileURLToPath } from "node:url"; |
| import { inferOpenAiCompatibleBiller, type AdapterExecutionContext, type AdapterExecutionResult } from "@paperclipai/adapter-utils"; |
| import { |
| asString, |
| asNumber, |
| asBoolean, |
| asStringArray, |
| parseObject, |
| buildPaperclipEnv, |
| redactEnvForLogs, |
| ensureAbsoluteDirectory, |
| ensureCommandResolvable, |
| ensurePaperclipSkillSymlink, |
| ensurePathInEnv, |
| readPaperclipRuntimeSkillEntries, |
| resolvePaperclipDesiredSkillNames, |
| renderTemplate, |
| joinPromptSections, |
| runChildProcess, |
| } from "@paperclipai/adapter-utils/server-utils"; |
| import { parseCodexJsonl, isCodexUnknownSessionError } from "./parse.js"; |
| import { pathExists, prepareManagedCodexHome, resolveManagedCodexHomeDir, resolveSharedCodexHomeDir } from "./codex-home.js"; |
| import { resolveCodexDesiredSkillNames } from "./skills.js"; |
|
|
| const __moduleDir = path.dirname(fileURLToPath(import.meta.url)); |
| const CODEX_ROLLOUT_NOISE_RE = |
| /^\d{4}-\d{2}-\d{2}T[^\s]+\s+ERROR\s+codex_core::rollout::list:\s+state db missing rollout path for thread\s+[a-z0-9-]+$/i; |
|
|
| function stripCodexRolloutNoise(text: string): string { |
| const parts = text.split(/\r?\n/); |
| const kept: string[] = []; |
| for (const part of parts) { |
| const trimmed = part.trim(); |
| if (!trimmed) { |
| kept.push(part); |
| continue; |
| } |
| if (CODEX_ROLLOUT_NOISE_RE.test(trimmed)) continue; |
| kept.push(part); |
| } |
| return kept.join("\n"); |
| } |
|
|
| function firstNonEmptyLine(text: string): string { |
| return ( |
| text |
| .split(/\r?\n/) |
| .map((line) => line.trim()) |
| .find(Boolean) ?? "" |
| ); |
| } |
|
|
| function hasNonEmptyEnvValue(env: Record<string, string>, key: string): boolean { |
| const raw = env[key]; |
| return typeof raw === "string" && raw.trim().length > 0; |
| } |
|
|
| function resolveCodexBillingType(env: Record<string, string>): "api" | "subscription" { |
| |
| return hasNonEmptyEnvValue(env, "OPENAI_API_KEY") ? "api" : "subscription"; |
| } |
|
|
| function resolveCodexBiller(env: Record<string, string>, billingType: "api" | "subscription"): string { |
| const openAiCompatibleBiller = inferOpenAiCompatibleBiller(env, "openai"); |
| if (openAiCompatibleBiller === "openrouter") return "openrouter"; |
| return billingType === "subscription" ? "chatgpt" : openAiCompatibleBiller ?? "openai"; |
| } |
|
|
| async function isLikelyPaperclipRepoRoot(candidate: string): Promise<boolean> { |
| const [hasWorkspace, hasPackageJson, hasServerDir, hasAdapterUtilsDir] = await Promise.all([ |
| pathExists(path.join(candidate, "pnpm-workspace.yaml")), |
| pathExists(path.join(candidate, "package.json")), |
| pathExists(path.join(candidate, "server")), |
| pathExists(path.join(candidate, "packages", "adapter-utils")), |
| ]); |
|
|
| return hasWorkspace && hasPackageJson && hasServerDir && hasAdapterUtilsDir; |
| } |
|
|
| async function isLikelyPaperclipRuntimeSkillPath( |
| candidate: string, |
| skillName: string, |
| options: { requireSkillMarkdown?: boolean } = {}, |
| ): Promise<boolean> { |
| if (path.basename(candidate) !== skillName) return false; |
| const skillsRoot = path.dirname(candidate); |
| if (path.basename(skillsRoot) !== "skills") return false; |
| if (options.requireSkillMarkdown !== false && !(await pathExists(path.join(candidate, "SKILL.md")))) { |
| return false; |
| } |
|
|
| let cursor = path.dirname(skillsRoot); |
| for (let depth = 0; depth < 6; depth += 1) { |
| if (await isLikelyPaperclipRepoRoot(cursor)) return true; |
| const parent = path.dirname(cursor); |
| if (parent === cursor) break; |
| cursor = parent; |
| } |
|
|
| return false; |
| } |
|
|
| async function pruneBrokenUnavailablePaperclipSkillSymlinks( |
| skillsHome: string, |
| allowedSkillNames: Iterable<string>, |
| onLog: AdapterExecutionContext["onLog"], |
| ) { |
| const allowed = new Set(Array.from(allowedSkillNames)); |
| const entries = await fs.readdir(skillsHome, { withFileTypes: true }).catch(() => []); |
|
|
| for (const entry of entries) { |
| if (allowed.has(entry.name) || !entry.isSymbolicLink()) continue; |
|
|
| const target = path.join(skillsHome, entry.name); |
| const linkedPath = await fs.readlink(target).catch(() => null); |
| if (!linkedPath) continue; |
|
|
| const resolvedLinkedPath = path.resolve(path.dirname(target), linkedPath); |
| if (await pathExists(resolvedLinkedPath)) continue; |
| if ( |
| !(await isLikelyPaperclipRuntimeSkillPath(resolvedLinkedPath, entry.name, { |
| requireSkillMarkdown: false, |
| })) |
| ) { |
| continue; |
| } |
|
|
| await fs.unlink(target).catch(() => {}); |
| await onLog( |
| "stdout", |
| `[paperclip] Removed stale Codex skill "${entry.name}" from ${skillsHome}\n`, |
| ); |
| } |
| } |
|
|
| function resolveCodexSkillsDir(codexHome: string): string { |
| return path.join(codexHome, "skills"); |
| } |
|
|
| type EnsureCodexSkillsInjectedOptions = { |
| skillsHome?: string; |
| skillsEntries?: Array<{ key: string; runtimeName: string; source: string }>; |
| desiredSkillNames?: string[]; |
| linkSkill?: (source: string, target: string) => Promise<void>; |
| }; |
|
|
| export async function ensureCodexSkillsInjected( |
| onLog: AdapterExecutionContext["onLog"], |
| options: EnsureCodexSkillsInjectedOptions = {}, |
| ) { |
| const allSkillsEntries = options.skillsEntries ?? await readPaperclipRuntimeSkillEntries({}, __moduleDir); |
| const desiredSkillNames = |
| options.desiredSkillNames ?? allSkillsEntries.map((entry) => entry.key); |
| const desiredSet = new Set(desiredSkillNames); |
| const skillsEntries = allSkillsEntries.filter((entry) => desiredSet.has(entry.key)); |
| if (skillsEntries.length === 0) return; |
|
|
| const skillsHome = options.skillsHome ?? resolveCodexSkillsDir(resolveSharedCodexHomeDir()); |
| await fs.mkdir(skillsHome, { recursive: true }); |
| const linkSkill = options.linkSkill; |
| for (const entry of skillsEntries) { |
| const target = path.join(skillsHome, entry.runtimeName); |
|
|
| try { |
| const existing = await fs.lstat(target).catch(() => null); |
| if (existing?.isSymbolicLink()) { |
| const linkedPath = await fs.readlink(target).catch(() => null); |
| const resolvedLinkedPath = linkedPath |
| ? path.resolve(path.dirname(target), linkedPath) |
| : null; |
| if ( |
| resolvedLinkedPath && |
| resolvedLinkedPath !== entry.source && |
| (await isLikelyPaperclipRuntimeSkillPath(resolvedLinkedPath, entry.runtimeName)) |
| ) { |
| await fs.unlink(target); |
| if (linkSkill) { |
| await linkSkill(entry.source, target); |
| } else { |
| await fs.symlink(entry.source, target); |
| } |
| await onLog( |
| "stdout", |
| `[paperclip] Repaired Codex skill "${entry.runtimeName}" into ${skillsHome}\n`, |
| ); |
| continue; |
| } |
| } |
|
|
| const result = await ensurePaperclipSkillSymlink(entry.source, target, linkSkill); |
| if (result === "skipped") continue; |
|
|
| await onLog( |
| "stdout", |
| `[paperclip] ${result === "repaired" ? "Repaired" : "Injected"} Codex skill "${entry.runtimeName}" into ${skillsHome}\n`, |
| ); |
| } catch (err) { |
| await onLog( |
| "stderr", |
| `[paperclip] Failed to inject Codex skill "${entry.key}" into ${skillsHome}: ${err instanceof Error ? err.message : String(err)}\n`, |
| ); |
| } |
| } |
|
|
| await pruneBrokenUnavailablePaperclipSkillSymlinks( |
| skillsHome, |
| skillsEntries.map((entry) => entry.runtimeName), |
| onLog, |
| ); |
| } |
|
|
| export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> { |
| const { runId, agent, runtime, config, context, onLog, onMeta, onSpawn, authToken } = ctx; |
|
|
| const promptTemplate = asString( |
| config.promptTemplate, |
| "You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work.", |
| ); |
| const command = asString(config.command, "codex"); |
| const model = asString(config.model, ""); |
| const modelReasoningEffort = asString( |
| config.modelReasoningEffort, |
| asString(config.reasoningEffort, ""), |
| ); |
| const search = asBoolean(config.search, false); |
| const bypass = asBoolean( |
| config.dangerouslyBypassApprovalsAndSandbox, |
| asBoolean(config.dangerouslyBypassSandbox, false), |
| ); |
|
|
| const workspaceContext = parseObject(context.paperclipWorkspace); |
| const workspaceCwd = asString(workspaceContext.cwd, ""); |
| const workspaceSource = asString(workspaceContext.source, ""); |
| const workspaceStrategy = asString(workspaceContext.strategy, ""); |
| const workspaceId = asString(workspaceContext.workspaceId, ""); |
| const workspaceRepoUrl = asString(workspaceContext.repoUrl, ""); |
| const workspaceRepoRef = asString(workspaceContext.repoRef, ""); |
| const workspaceBranch = asString(workspaceContext.branchName, ""); |
| const workspaceWorktreePath = asString(workspaceContext.worktreePath, ""); |
| const agentHome = asString(workspaceContext.agentHome, ""); |
| const workspaceHints = Array.isArray(context.paperclipWorkspaces) |
| ? context.paperclipWorkspaces.filter( |
| (value): value is Record<string, unknown> => typeof value === "object" && value !== null, |
| ) |
| : []; |
| const runtimeServiceIntents = Array.isArray(context.paperclipRuntimeServiceIntents) |
| ? context.paperclipRuntimeServiceIntents.filter( |
| (value): value is Record<string, unknown> => typeof value === "object" && value !== null, |
| ) |
| : []; |
| const runtimeServices = Array.isArray(context.paperclipRuntimeServices) |
| ? context.paperclipRuntimeServices.filter( |
| (value): value is Record<string, unknown> => typeof value === "object" && value !== null, |
| ) |
| : []; |
| const runtimePrimaryUrl = asString(context.paperclipRuntimePrimaryUrl, ""); |
| const configuredCwd = asString(config.cwd, ""); |
| const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd.length > 0; |
| const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd; |
| const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd(); |
| const envConfig = parseObject(config.env); |
| const configuredCodexHome = |
| typeof envConfig.CODEX_HOME === "string" && envConfig.CODEX_HOME.trim().length > 0 |
| ? path.resolve(envConfig.CODEX_HOME.trim()) |
| : null; |
| const codexSkillEntries = await readPaperclipRuntimeSkillEntries(config, __moduleDir); |
| const desiredSkillNames = resolveCodexDesiredSkillNames(config, codexSkillEntries); |
| await ensureAbsoluteDirectory(cwd, { createIfMissing: true }); |
| const preparedManagedCodexHome = |
| configuredCodexHome ? null : await prepareManagedCodexHome(process.env, onLog, agent.companyId); |
| const defaultCodexHome = resolveManagedCodexHomeDir(process.env, agent.companyId); |
| const effectiveCodexHome = configuredCodexHome ?? preparedManagedCodexHome ?? defaultCodexHome; |
| await fs.mkdir(effectiveCodexHome, { recursive: true }); |
| |
| |
| const codexSkillsDir = resolveCodexSkillsDir(effectiveCodexHome); |
| await ensureCodexSkillsInjected( |
| onLog, |
| { |
| skillsHome: codexSkillsDir, |
| skillsEntries: codexSkillEntries, |
| desiredSkillNames, |
| }, |
| ); |
| const hasExplicitApiKey = |
| typeof envConfig.PAPERCLIP_API_KEY === "string" && envConfig.PAPERCLIP_API_KEY.trim().length > 0; |
| const env: Record<string, string> = { ...buildPaperclipEnv(agent) }; |
| env.CODEX_HOME = effectiveCodexHome; |
| env.PAPERCLIP_RUN_ID = runId; |
| const wakeTaskId = |
| (typeof context.taskId === "string" && context.taskId.trim().length > 0 && context.taskId.trim()) || |
| (typeof context.issueId === "string" && context.issueId.trim().length > 0 && context.issueId.trim()) || |
| null; |
| const wakeReason = |
| typeof context.wakeReason === "string" && context.wakeReason.trim().length > 0 |
| ? context.wakeReason.trim() |
| : null; |
| const wakeCommentId = |
| (typeof context.wakeCommentId === "string" && context.wakeCommentId.trim().length > 0 && context.wakeCommentId.trim()) || |
| (typeof context.commentId === "string" && context.commentId.trim().length > 0 && context.commentId.trim()) || |
| null; |
| const approvalId = |
| typeof context.approvalId === "string" && context.approvalId.trim().length > 0 |
| ? context.approvalId.trim() |
| : null; |
| const approvalStatus = |
| typeof context.approvalStatus === "string" && context.approvalStatus.trim().length > 0 |
| ? context.approvalStatus.trim() |
| : null; |
| const linkedIssueIds = Array.isArray(context.issueIds) |
| ? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0) |
| : []; |
| if (wakeTaskId) { |
| env.PAPERCLIP_TASK_ID = wakeTaskId; |
| } |
| if (wakeReason) { |
| env.PAPERCLIP_WAKE_REASON = wakeReason; |
| } |
| if (wakeCommentId) { |
| env.PAPERCLIP_WAKE_COMMENT_ID = wakeCommentId; |
| } |
| if (approvalId) { |
| env.PAPERCLIP_APPROVAL_ID = approvalId; |
| } |
| if (approvalStatus) { |
| env.PAPERCLIP_APPROVAL_STATUS = approvalStatus; |
| } |
| if (linkedIssueIds.length > 0) { |
| env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(","); |
| } |
| if (effectiveWorkspaceCwd) { |
| env.PAPERCLIP_WORKSPACE_CWD = effectiveWorkspaceCwd; |
| } |
| if (workspaceSource) { |
| env.PAPERCLIP_WORKSPACE_SOURCE = workspaceSource; |
| } |
| if (workspaceStrategy) { |
| env.PAPERCLIP_WORKSPACE_STRATEGY = workspaceStrategy; |
| } |
| if (workspaceId) { |
| env.PAPERCLIP_WORKSPACE_ID = workspaceId; |
| } |
| if (workspaceRepoUrl) { |
| env.PAPERCLIP_WORKSPACE_REPO_URL = workspaceRepoUrl; |
| } |
| if (workspaceRepoRef) { |
| env.PAPERCLIP_WORKSPACE_REPO_REF = workspaceRepoRef; |
| } |
| if (workspaceBranch) { |
| env.PAPERCLIP_WORKSPACE_BRANCH = workspaceBranch; |
| } |
| if (workspaceWorktreePath) { |
| env.PAPERCLIP_WORKSPACE_WORKTREE_PATH = workspaceWorktreePath; |
| } |
| if (agentHome) { |
| env.AGENT_HOME = agentHome; |
| } |
| if (workspaceHints.length > 0) { |
| env.PAPERCLIP_WORKSPACES_JSON = JSON.stringify(workspaceHints); |
| } |
| if (runtimeServiceIntents.length > 0) { |
| env.PAPERCLIP_RUNTIME_SERVICE_INTENTS_JSON = JSON.stringify(runtimeServiceIntents); |
| } |
| if (runtimeServices.length > 0) { |
| env.PAPERCLIP_RUNTIME_SERVICES_JSON = JSON.stringify(runtimeServices); |
| } |
| if (runtimePrimaryUrl) { |
| env.PAPERCLIP_RUNTIME_PRIMARY_URL = runtimePrimaryUrl; |
| } |
| for (const [k, v] of Object.entries(envConfig)) { |
| if (typeof v === "string") env[k] = v; |
| } |
| if (!hasExplicitApiKey && authToken) { |
| env.PAPERCLIP_API_KEY = authToken; |
| } |
| const effectiveEnv = Object.fromEntries( |
| Object.entries({ ...process.env, ...env }).filter( |
| (entry): entry is [string, string] => typeof entry[1] === "string", |
| ), |
| ); |
| const billingType = resolveCodexBillingType(effectiveEnv); |
| const runtimeEnv = ensurePathInEnv(effectiveEnv); |
| await ensureCommandResolvable(command, cwd, runtimeEnv); |
|
|
| const timeoutSec = asNumber(config.timeoutSec, 0); |
| const graceSec = asNumber(config.graceSec, 20); |
| const extraArgs = (() => { |
| const fromExtraArgs = asStringArray(config.extraArgs); |
| if (fromExtraArgs.length > 0) return fromExtraArgs; |
| return asStringArray(config.args); |
| })(); |
|
|
| const runtimeSessionParams = parseObject(runtime.sessionParams); |
| const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? ""); |
| const runtimeSessionCwd = asString(runtimeSessionParams.cwd, ""); |
| const canResumeSession = |
| runtimeSessionId.length > 0 && |
| (runtimeSessionCwd.length === 0 || path.resolve(runtimeSessionCwd) === path.resolve(cwd)); |
| const sessionId = canResumeSession ? runtimeSessionId : null; |
| if (runtimeSessionId && !canResumeSession) { |
| await onLog( |
| "stdout", |
| `[paperclip] Codex session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${cwd}".\n`, |
| ); |
| } |
| const instructionsFilePath = asString(config.instructionsFilePath, "").trim(); |
| const instructionsDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : ""; |
| let instructionsPrefix = ""; |
| let instructionsChars = 0; |
| if (instructionsFilePath) { |
| try { |
| const instructionsContents = await fs.readFile(instructionsFilePath, "utf8"); |
| instructionsPrefix = |
| `${instructionsContents}\n\n` + |
| `The above agent instructions were loaded from ${instructionsFilePath}. ` + |
| `Resolve any relative file references from ${instructionsDir}.\n\n`; |
| instructionsChars = instructionsPrefix.length; |
| } catch (err) { |
| const reason = err instanceof Error ? err.message : String(err); |
| await onLog( |
| "stdout", |
| `[paperclip] Warning: could not read agent instructions file "${instructionsFilePath}": ${reason}\n`, |
| ); |
| } |
| } |
| const repoAgentsNote = |
| "Codex exec automatically applies repo-scoped AGENTS.md instructions from the current workspace; Paperclip does not currently suppress that discovery."; |
| const commandNotes = (() => { |
| if (!instructionsFilePath) { |
| return [repoAgentsNote]; |
| } |
| if (instructionsPrefix.length > 0) { |
| return [ |
| `Loaded agent instructions from ${instructionsFilePath}`, |
| `Prepended instructions + path directive to stdin prompt (relative references from ${instructionsDir}).`, |
| repoAgentsNote, |
| ]; |
| } |
| return [ |
| `Configured instructionsFilePath ${instructionsFilePath}, but file could not be read; continuing without injected instructions.`, |
| repoAgentsNote, |
| ]; |
| })(); |
| const bootstrapPromptTemplate = asString(config.bootstrapPromptTemplate, ""); |
| const templateData = { |
| agentId: agent.id, |
| companyId: agent.companyId, |
| runId, |
| company: { id: agent.companyId }, |
| agent, |
| run: { id: runId, source: "on_demand" }, |
| context, |
| }; |
| const renderedPrompt = renderTemplate(promptTemplate, templateData); |
| const renderedBootstrapPrompt = |
| !sessionId && bootstrapPromptTemplate.trim().length > 0 |
| ? renderTemplate(bootstrapPromptTemplate, templateData).trim() |
| : ""; |
| const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim(); |
| const prompt = joinPromptSections([ |
| instructionsPrefix, |
| renderedBootstrapPrompt, |
| sessionHandoffNote, |
| renderedPrompt, |
| ]); |
| const promptMetrics = { |
| promptChars: prompt.length, |
| instructionsChars, |
| bootstrapPromptChars: renderedBootstrapPrompt.length, |
| sessionHandoffChars: sessionHandoffNote.length, |
| heartbeatPromptChars: renderedPrompt.length, |
| }; |
|
|
| const buildArgs = (resumeSessionId: string | null) => { |
| const args = ["exec", "--json"]; |
| if (search) args.unshift("--search"); |
| if (bypass) args.push("--dangerously-bypass-approvals-and-sandbox"); |
| if (model) args.push("--model", model); |
| if (modelReasoningEffort) args.push("-c", `model_reasoning_effort=${JSON.stringify(modelReasoningEffort)}`); |
| if (extraArgs.length > 0) args.push(...extraArgs); |
| if (resumeSessionId) args.push("resume", resumeSessionId, "-"); |
| else args.push("-"); |
| return args; |
| }; |
|
|
| const runAttempt = async (resumeSessionId: string | null) => { |
| const args = buildArgs(resumeSessionId); |
| if (onMeta) { |
| await onMeta({ |
| adapterType: "codex_local", |
| command, |
| cwd, |
| commandNotes, |
| commandArgs: args.map((value, idx) => { |
| if (idx === args.length - 1 && value !== "-") return `<prompt ${prompt.length} chars>`; |
| return value; |
| }), |
| env: redactEnvForLogs(env), |
| prompt, |
| promptMetrics, |
| context, |
| }); |
| } |
|
|
| const proc = await runChildProcess(runId, command, args, { |
| cwd, |
| env, |
| stdin: prompt, |
| timeoutSec, |
| graceSec, |
| onSpawn, |
| onLog: async (stream, chunk) => { |
| if (stream !== "stderr") { |
| await onLog(stream, chunk); |
| return; |
| } |
| const cleaned = stripCodexRolloutNoise(chunk); |
| if (!cleaned.trim()) return; |
| await onLog(stream, cleaned); |
| }, |
| }); |
| const cleanedStderr = stripCodexRolloutNoise(proc.stderr); |
| return { |
| proc: { |
| ...proc, |
| stderr: cleanedStderr, |
| }, |
| rawStderr: proc.stderr, |
| parsed: parseCodexJsonl(proc.stdout), |
| }; |
| }; |
|
|
| const toResult = ( |
| attempt: { proc: { exitCode: number | null; signal: string | null; timedOut: boolean; stdout: string; stderr: string }; rawStderr: string; parsed: ReturnType<typeof parseCodexJsonl> }, |
| clearSessionOnMissingSession = false, |
| ): AdapterExecutionResult => { |
| if (attempt.proc.timedOut) { |
| return { |
| exitCode: attempt.proc.exitCode, |
| signal: attempt.proc.signal, |
| timedOut: true, |
| errorMessage: `Timed out after ${timeoutSec}s`, |
| clearSession: clearSessionOnMissingSession, |
| }; |
| } |
|
|
| const resolvedSessionId = attempt.parsed.sessionId ?? runtimeSessionId ?? runtime.sessionId ?? null; |
| const resolvedSessionParams = resolvedSessionId |
| ? ({ |
| sessionId: resolvedSessionId, |
| cwd, |
| ...(workspaceId ? { workspaceId } : {}), |
| ...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}), |
| ...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}), |
| } as Record<string, unknown>) |
| : null; |
| const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : ""; |
| const stderrLine = firstNonEmptyLine(attempt.proc.stderr); |
| const fallbackErrorMessage = |
| parsedError || |
| stderrLine || |
| `Codex exited with code ${attempt.proc.exitCode ?? -1}`; |
|
|
| return { |
| exitCode: attempt.proc.exitCode, |
| signal: attempt.proc.signal, |
| timedOut: false, |
| errorMessage: |
| (attempt.proc.exitCode ?? 0) === 0 |
| ? null |
| : fallbackErrorMessage, |
| usage: attempt.parsed.usage, |
| sessionId: resolvedSessionId, |
| sessionParams: resolvedSessionParams, |
| sessionDisplayId: resolvedSessionId, |
| provider: "openai", |
| biller: resolveCodexBiller(effectiveEnv, billingType), |
| model, |
| billingType, |
| costUsd: null, |
| resultJson: { |
| stdout: attempt.proc.stdout, |
| stderr: attempt.proc.stderr, |
| }, |
| summary: attempt.parsed.summary, |
| clearSession: Boolean(clearSessionOnMissingSession && !resolvedSessionId), |
| }; |
| }; |
|
|
| const initial = await runAttempt(sessionId); |
| if ( |
| sessionId && |
| !initial.proc.timedOut && |
| (initial.proc.exitCode ?? 0) !== 0 && |
| isCodexUnknownSessionError(initial.proc.stdout, initial.rawStderr) |
| ) { |
| await onLog( |
| "stdout", |
| `[paperclip] Codex resume session "${sessionId}" is unavailable; retrying with a fresh session.\n`, |
| ); |
| const retry = await runAttempt(null); |
| return toResult(retry, true); |
| } |
|
|
| return toResult(initial); |
| } |
|
|