2026-02-19 14:39:37 -06:00
import fs from "node:fs/promises" ;
2026-02-19 14:01:58 -06:00
import path from "node:path" ;
2026-02-19 14:39:37 -06:00
import { fileURLToPath } from "node:url" ;
2026-03-14 22:00:12 -05:00
import { inferOpenAiCompatibleBiller , type AdapterExecutionContext , type AdapterExecutionResult } from "@paperclipai/adapter-utils" ;
2026-02-18 13:53:03 -06:00
import {
asString ,
asNumber ,
asBoolean ,
asStringArray ,
parseObject ,
buildPaperclipEnv ,
redactEnvForLogs ,
ensureAbsoluteDirectory ,
ensureCommandResolvable ,
2026-03-12 15:44:44 -05:00
ensurePaperclipSkillSymlink ,
2026-02-18 13:53:03 -06:00
ensurePathInEnv ,
2026-03-15 07:05:01 -05:00
readPaperclipRuntimeSkillEntries ,
resolvePaperclipDesiredSkillNames ,
2026-02-18 13:53:03 -06:00
renderTemplate ,
2026-03-13 08:49:11 -05:00
joinPromptSections ,
2026-02-18 13:53:03 -06:00
runChildProcess ,
2026-03-03 08:45:26 -06:00
} from "@paperclipai/adapter-utils/server-utils" ;
2026-02-19 14:01:58 -06:00
import { parseCodexJsonl , isCodexUnknownSessionError } from "./parse.js" ;
2026-03-25 15:55:51 -07:00
import { pathExists , prepareManagedCodexHome , resolveManagedCodexHomeDir , resolveSharedCodexHomeDir } from "./codex-home.js" ;
2026-03-13 22:49:42 -05:00
import { resolveCodexDesiredSkillNames } from "./skills.js" ;
2026-02-18 13:53:03 -06:00
2026-03-03 16:06:12 -06:00
const __moduleDir = path . dirname ( fileURLToPath ( import . meta . url ) ) ;
2026-02-19 14:39:37 -06:00
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 ) ? ? ""
) ;
}
2026-02-25 21:35:44 -06:00
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" {
// Codex uses API-key auth when OPENAI_API_KEY is present; otherwise rely on local login/session auth.
return hasNonEmptyEnvValue ( env , "OPENAI_API_KEY" ) ? "api" : "subscription" ;
}
2026-03-14 22:00:12 -05:00
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" ;
}
2026-03-13 11:53:56 -05:00
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 ;
}
2026-03-18 14:07:24 -05:00
async function isLikelyPaperclipRuntimeSkillPath (
candidate : string ,
skillName : string ,
options : { requireSkillMarkdown? : boolean } = { } ,
) : Promise < boolean > {
2026-03-13 11:53:56 -05:00
if ( path . basename ( candidate ) !== skillName ) return false ;
const skillsRoot = path . dirname ( candidate ) ;
if ( path . basename ( skillsRoot ) !== "skills" ) return false ;
2026-03-18 14:07:24 -05:00
if ( options . requireSkillMarkdown !== false && ! ( await pathExists ( path . join ( candidate , "SKILL.md" ) ) ) ) {
return false ;
}
2026-03-13 11:53:56 -05:00
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 ;
2026-02-19 14:39:37 -06:00
}
2026-03-18 14:07:24 -05:00
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 ` ,
) ;
}
}
2026-03-25 15:55:51 -07:00
function resolveCodexSkillsHome ( ) : string {
return path . join ( resolveSharedCodexHomeDir ( ) , "skills" ) ;
2026-03-19 07:15:36 -05:00
}
2026-03-12 15:44:44 -05:00
type EnsureCodexSkillsInjectedOptions = {
skillsHome? : string ;
2026-03-16 18:27:20 -05:00
skillsEntries? : Array < { key : string ; runtimeName : string ; source : string } > ;
2026-03-13 22:49:42 -05:00
desiredSkillNames? : string [ ] ;
2026-03-12 15:44:44 -05:00
linkSkill ? : ( source : string , target : string ) = > Promise < void > ;
} ;
2026-03-03 16:06:12 -06:00
2026-03-12 15:44:44 -05:00
export async function ensureCodexSkillsInjected (
onLog : AdapterExecutionContext [ "onLog" ] ,
options : EnsureCodexSkillsInjectedOptions = { } ,
) {
2026-03-15 07:05:01 -05:00
const allSkillsEntries = options . skillsEntries ? ? await readPaperclipRuntimeSkillEntries ( { } , __moduleDir ) ;
2026-03-13 22:49:42 -05:00
const desiredSkillNames =
2026-03-16 18:27:20 -05:00
options . desiredSkillNames ? ? allSkillsEntries . map ( ( entry ) = > entry . key ) ;
2026-03-13 22:49:42 -05:00
const desiredSet = new Set ( desiredSkillNames ) ;
2026-03-16 18:27:20 -05:00
const skillsEntries = allSkillsEntries . filter ( ( entry ) = > desiredSet . has ( entry . key ) ) ;
2026-03-12 15:44:44 -05:00
if ( skillsEntries . length === 0 ) return ;
2026-02-19 14:39:37 -06:00
2026-03-25 15:55:51 -07:00
const skillsHome = options . skillsHome ? ? resolveCodexSkillsHome ( ) ;
2026-02-19 14:39:37 -06:00
await fs . mkdir ( skillsHome , { recursive : true } ) ;
2026-03-12 15:44:44 -05:00
const linkSkill = options . linkSkill ;
for ( const entry of skillsEntries ) {
2026-03-16 18:27:20 -05:00
const target = path . join ( skillsHome , entry . runtimeName ) ;
2026-02-19 14:39:37 -06:00
try {
2026-03-13 11:53:56 -05:00
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 &&
2026-03-18 14:07:24 -05:00
( await isLikelyPaperclipRuntimeSkillPath ( resolvedLinkedPath , entry . runtimeName ) )
2026-03-13 11:53:56 -05:00
) {
await fs . unlink ( target ) ;
if ( linkSkill ) {
await linkSkill ( entry . source , target ) ;
} else {
await fs . symlink ( entry . source , target ) ;
}
await onLog (
2026-03-16 18:09:43 -05:00
"stdout" ,
2026-03-17 10:45:14 -05:00
` [paperclip] Repaired Codex skill " ${ entry . runtimeName } " into ${ skillsHome } \ n ` ,
2026-03-13 11:53:56 -05:00
) ;
continue ;
}
}
2026-03-12 15:44:44 -05:00
const result = await ensurePaperclipSkillSymlink ( entry . source , target , linkSkill ) ;
if ( result === "skipped" ) continue ;
2026-02-19 14:39:37 -06:00
await onLog (
2026-03-16 18:09:43 -05:00
"stdout" ,
2026-03-17 10:45:14 -05:00
` [paperclip] ${ result === "repaired" ? "Repaired" : "Injected" } Codex skill " ${ entry . runtimeName } " into ${ skillsHome } \ n ` ,
2026-02-19 14:39:37 -06:00
) ;
} catch ( err ) {
await onLog (
"stderr" ,
2026-03-16 18:27:20 -05:00
` [paperclip] Failed to inject Codex skill " ${ entry . key } " into ${ skillsHome } : ${ err instanceof Error ? err.message : String ( err ) } \ n ` ,
2026-02-19 14:39:37 -06:00
) ;
}
}
2026-03-18 14:07:24 -05:00
await pruneBrokenUnavailablePaperclipSkillSymlinks (
skillsHome ,
skillsEntries . map ( ( entry ) = > entry . runtimeName ) ,
onLog ,
) ;
2026-02-19 14:39:37 -06:00
}
2026-02-18 13:53:03 -06:00
export async function execute ( ctx : AdapterExecutionContext ) : Promise < AdapterExecutionResult > {
2026-03-19 11:20:36 -05:00
const { runId , agent , runtime , config , context , onLog , onMeta , onSpawn , authToken } = ctx ;
2026-02-18 13:53:03 -06:00
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 , "" ) ;
2026-02-20 10:32:07 -06:00
const modelReasoningEffort = asString (
config . modelReasoningEffort ,
asString ( config . reasoningEffort , "" ) ,
) ;
2026-02-18 13:53:03 -06:00
const search = asBoolean ( config . search , false ) ;
2026-02-23 19:44:10 -06:00
const bypass = asBoolean (
config . dangerouslyBypassApprovalsAndSandbox ,
asBoolean ( config . dangerouslyBypassSandbox , false ) ,
) ;
2026-02-18 13:53:03 -06:00
2026-02-25 08:38:58 -06:00
const workspaceContext = parseObject ( context . paperclipWorkspace ) ;
const workspaceCwd = asString ( workspaceContext . cwd , "" ) ;
const workspaceSource = asString ( workspaceContext . source , "" ) ;
2026-03-10 10:58:38 -05:00
const workspaceStrategy = asString ( workspaceContext . strategy , "" ) ;
2026-02-25 08:38:58 -06:00
const workspaceId = asString ( workspaceContext . workspaceId , "" ) ;
const workspaceRepoUrl = asString ( workspaceContext . repoUrl , "" ) ;
const workspaceRepoRef = asString ( workspaceContext . repoRef , "" ) ;
2026-03-10 10:58:38 -05:00
const workspaceBranch = asString ( workspaceContext . branchName , "" ) ;
const workspaceWorktreePath = asString ( workspaceContext . worktreePath , "" ) ;
2026-03-14 00:36:53 -07:00
const agentHome = asString ( workspaceContext . agentHome , "" ) ;
2026-02-25 21:35:44 -06:00
const workspaceHints = Array . isArray ( context . paperclipWorkspaces )
? context . paperclipWorkspaces . filter (
( value ) : value is Record < string , unknown > = > typeof value === "object" && value !== null ,
)
: [ ] ;
2026-03-10 10:58:38 -05:00
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 , "" ) ;
2026-02-25 21:35:44 -06:00
const configuredCwd = asString ( config . cwd , "" ) ;
const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd . length > 0 ;
const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd ;
const cwd = effectiveWorkspaceCwd || configuredCwd || process . cwd ( ) ;
2026-02-18 13:53:03 -06:00
const envConfig = parseObject ( config . env ) ;
2026-03-13 11:53:56 -05:00
const configuredCodexHome =
typeof envConfig . CODEX_HOME === "string" && envConfig . CODEX_HOME . trim ( ) . length > 0
? path . resolve ( envConfig . CODEX_HOME . trim ( ) )
: null ;
2026-03-15 07:05:01 -05:00
const codexSkillEntries = await readPaperclipRuntimeSkillEntries ( config , __moduleDir ) ;
const desiredSkillNames = resolveCodexDesiredSkillNames ( config , codexSkillEntries ) ;
2026-03-13 11:53:56 -05:00
await ensureAbsoluteDirectory ( cwd , { createIfMissing : true } ) ;
2026-03-20 14:44:27 -05:00
const preparedManagedCodexHome =
configuredCodexHome ? null : await prepareManagedCodexHome ( process . env , onLog , agent . companyId ) ;
const defaultCodexHome = resolveManagedCodexHomeDir ( process . env , agent . companyId ) ;
const effectiveCodexHome = configuredCodexHome ? ? preparedManagedCodexHome ? ? defaultCodexHome ;
2026-03-20 14:25:18 -05:00
await fs . mkdir ( effectiveCodexHome , { recursive : true } ) ;
2026-03-25 15:55:51 -07:00
const codexSkillsDir = resolveCodexSkillsHome ( ) ;
2026-03-13 11:53:56 -05:00
await ensureCodexSkillsInjected (
onLog ,
2026-03-18 14:38:39 -05:00
{
2026-03-25 15:55:51 -07:00
skillsHome : codexSkillsDir ,
2026-03-18 14:38:39 -05:00
skillsEntries : codexSkillEntries ,
desiredSkillNames ,
} ,
2026-03-13 11:53:56 -05:00
) ;
2026-02-18 16:46:45 -06:00
const hasExplicitApiKey =
typeof envConfig . PAPERCLIP_API_KEY === "string" && envConfig . PAPERCLIP_API_KEY . trim ( ) . length > 0 ;
2026-02-18 13:53:03 -06:00
const env : Record < string , string > = { . . . buildPaperclipEnv ( agent ) } ;
2026-03-18 14:38:39 -05:00
env . CODEX_HOME = effectiveCodexHome ;
2026-02-19 09:09:50 -06:00
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 ;
2026-02-20 10:32:07 -06:00
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 ;
2026-02-19 13:02:53 -06:00
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 )
: [ ] ;
2026-02-19 09:09:50 -06:00
if ( wakeTaskId ) {
env . PAPERCLIP_TASK_ID = wakeTaskId ;
}
if ( wakeReason ) {
env . PAPERCLIP_WAKE_REASON = wakeReason ;
}
2026-02-20 10:32:07 -06:00
if ( wakeCommentId ) {
env . PAPERCLIP_WAKE_COMMENT_ID = wakeCommentId ;
}
2026-02-19 13:02:53 -06:00
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 ( "," ) ;
}
2026-02-25 21:35:44 -06:00
if ( effectiveWorkspaceCwd ) {
env . PAPERCLIP_WORKSPACE_CWD = effectiveWorkspaceCwd ;
2026-02-25 08:38:58 -06:00
}
if ( workspaceSource ) {
env . PAPERCLIP_WORKSPACE_SOURCE = workspaceSource ;
}
2026-03-10 10:58:38 -05:00
if ( workspaceStrategy ) {
env . PAPERCLIP_WORKSPACE_STRATEGY = workspaceStrategy ;
}
2026-02-25 08:38:58 -06:00
if ( workspaceId ) {
env . PAPERCLIP_WORKSPACE_ID = workspaceId ;
}
if ( workspaceRepoUrl ) {
env . PAPERCLIP_WORKSPACE_REPO_URL = workspaceRepoUrl ;
}
if ( workspaceRepoRef ) {
env . PAPERCLIP_WORKSPACE_REPO_REF = workspaceRepoRef ;
}
2026-03-10 10:58:38 -05:00
if ( workspaceBranch ) {
env . PAPERCLIP_WORKSPACE_BRANCH = workspaceBranch ;
}
if ( workspaceWorktreePath ) {
env . PAPERCLIP_WORKSPACE_WORKTREE_PATH = workspaceWorktreePath ;
}
2026-03-14 00:36:53 -07:00
if ( agentHome ) {
env . AGENT_HOME = agentHome ;
}
2026-02-25 21:35:44 -06:00
if ( workspaceHints . length > 0 ) {
env . PAPERCLIP_WORKSPACES_JSON = JSON . stringify ( workspaceHints ) ;
}
2026-03-10 10:58:38 -05:00
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 ;
}
2026-02-18 13:53:03 -06:00
for ( const [ k , v ] of Object . entries ( envConfig ) ) {
if ( typeof v === "string" ) env [ k ] = v ;
}
2026-02-18 16:46:45 -06:00
if ( ! hasExplicitApiKey && authToken ) {
env . PAPERCLIP_API_KEY = authToken ;
}
2026-03-14 22:00:12 -05:00
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 ) ;
2026-02-18 13:53:03 -06:00
await ensureCommandResolvable ( command , cwd , runtimeEnv ) ;
2026-02-20 10:32:07 -06:00
const timeoutSec = asNumber ( config . timeoutSec , 0 ) ;
2026-02-18 13:53:03 -06:00
const graceSec = asNumber ( config . graceSec , 20 ) ;
const extraArgs = ( ( ) = > {
const fromExtraArgs = asStringArray ( config . extraArgs ) ;
if ( fromExtraArgs . length > 0 ) return fromExtraArgs ;
return asStringArray ( config . args ) ;
} ) ( ) ;
2026-02-19 14:01:58 -06:00
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 (
2026-03-18 21:16:37 -05:00
"stdout" ,
2026-02-19 14:01:58 -06:00
` [paperclip] Codex session " ${ runtimeSessionId } " was saved for cwd " ${ runtimeSessionCwd } " and will not be resumed in " ${ cwd } ". \ n ` ,
) ;
}
2026-02-26 16:34:15 -06:00
const instructionsFilePath = asString ( config . instructionsFilePath , "" ) . trim ( ) ;
2026-03-02 16:43:59 -06:00
const instructionsDir = instructionsFilePath ? ` ${ path . dirname ( instructionsFilePath ) } / ` : "" ;
2026-02-26 16:34:15 -06:00
let instructionsPrefix = "" ;
2026-03-13 08:49:11 -05:00
let instructionsChars = 0 ;
2026-02-26 16:34:15 -06:00
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 ` ;
2026-03-13 08:49:11 -05:00
instructionsChars = instructionsPrefix . length ;
2026-02-26 16:34:15 -06:00
} catch ( err ) {
const reason = err instanceof Error ? err.message : String ( err ) ;
await onLog (
2026-03-18 21:16:37 -05:00
"stdout" ,
2026-02-26 16:34:15 -06:00
` [paperclip] Warning: could not read agent instructions file " ${ instructionsFilePath } ": ${ reason } \ n ` ,
) ;
}
}
2026-03-23 16:55:10 -05:00
const repoAgentsNote =
"Codex exec automatically applies repo-scoped AGENTS.md instructions from the current workspace; Paperclip does not currently suppress that discovery." ;
2026-03-02 16:43:59 -06:00
const commandNotes = ( ( ) = > {
2026-03-23 16:55:10 -05:00
if ( ! instructionsFilePath ) {
return [ repoAgentsNote ] ;
}
2026-03-02 16:43:59 -06:00
if ( instructionsPrefix . length > 0 ) {
return [
` Loaded agent instructions from ${ instructionsFilePath } ` ,
` Prepended instructions + path directive to stdin prompt (relative references from ${ instructionsDir } ). ` ,
2026-03-23 16:55:10 -05:00
repoAgentsNote ,
2026-03-02 16:43:59 -06:00
] ;
}
return [
` Configured instructionsFilePath ${ instructionsFilePath } , but file could not be read; continuing without injected instructions. ` ,
2026-03-23 16:55:10 -05:00
repoAgentsNote ,
2026-03-02 16:43:59 -06:00
] ;
} ) ( ) ;
2026-03-13 08:49:11 -05:00
const bootstrapPromptTemplate = asString ( config . bootstrapPromptTemplate , "" ) ;
const templateData = {
2026-02-19 14:39:37 -06:00
agentId : agent.id ,
companyId : agent.companyId ,
runId ,
2026-02-18 13:53:03 -06:00
company : { id : agent.companyId } ,
agent ,
run : { id : runId , source : "on_demand" } ,
context ,
2026-03-13 08:49:11 -05:00
} ;
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 ,
} ;
2026-02-18 13:53:03 -06:00
2026-02-19 14:01:58 -06:00
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 ) ;
2026-02-20 10:32:07 -06:00
if ( modelReasoningEffort ) args . push ( "-c" , ` model_reasoning_effort= ${ JSON . stringify ( modelReasoningEffort ) } ` ) ;
2026-02-19 14:01:58 -06:00
if ( extraArgs . length > 0 ) args . push ( . . . extraArgs ) ;
2026-02-19 14:39:37 -06:00
if ( resumeSessionId ) args . push ( "resume" , resumeSessionId , "-" ) ;
else args . push ( "-" ) ;
2026-02-19 14:01:58 -06:00
return args ;
} ;
const runAttempt = async ( resumeSessionId : string | null ) = > {
const args = buildArgs ( resumeSessionId ) ;
if ( onMeta ) {
await onMeta ( {
adapterType : "codex_local" ,
command ,
cwd ,
2026-03-02 16:43:59 -06:00
commandNotes ,
2026-02-19 14:01:58 -06:00
commandArgs : args.map ( ( value , idx ) = > {
2026-02-19 14:39:37 -06:00
if ( idx === args . length - 1 && value !== "-" ) return ` <prompt ${ prompt . length } chars> ` ;
2026-02-19 14:01:58 -06:00
return value ;
} ) ,
env : redactEnvForLogs ( env ) ,
prompt ,
2026-03-13 08:49:11 -05:00
promptMetrics ,
2026-02-19 14:01:58 -06:00
context ,
} ) ;
}
2026-02-18 13:53:03 -06:00
2026-02-19 14:01:58 -06:00
const proc = await runChildProcess ( runId , command , args , {
2026-02-18 13:53:03 -06:00
cwd ,
2026-02-19 14:01:58 -06:00
env ,
2026-02-19 14:39:37 -06:00
stdin : prompt ,
2026-02-19 14:01:58 -06:00
timeoutSec ,
graceSec ,
2026-03-19 11:20:36 -05:00
onSpawn ,
2026-02-19 14:39:37 -06:00
onLog : async ( stream , chunk ) = > {
if ( stream !== "stderr" ) {
await onLog ( stream , chunk ) ;
return ;
}
const cleaned = stripCodexRolloutNoise ( chunk ) ;
if ( ! cleaned . trim ( ) ) return ;
await onLog ( stream , cleaned ) ;
} ,
2026-02-18 13:53:03 -06:00
} ) ;
2026-02-19 14:39:37 -06:00
const cleanedStderr = stripCodexRolloutNoise ( proc . stderr ) ;
2026-02-19 14:01:58 -06:00
return {
2026-02-19 14:39:37 -06:00
proc : {
. . . proc ,
stderr : cleanedStderr ,
} ,
rawStderr : proc.stderr ,
2026-02-19 14:01:58 -06:00
parsed : parseCodexJsonl ( proc . stdout ) ,
} ;
} ;
2026-02-18 13:53:03 -06:00
2026-02-19 14:01:58 -06:00
const toResult = (
2026-02-19 14:39:37 -06:00
attempt : { proc : { exitCode : number | null ; signal : string | null ; timedOut : boolean ; stdout : string ; stderr : string } ; rawStderr : string ; parsed : ReturnType < typeof parseCodexJsonl > } ,
2026-02-19 14:01:58 -06:00
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
2026-02-25 08:38:58 -06:00
? ( {
sessionId : resolvedSessionId ,
cwd ,
. . . ( workspaceId ? { workspaceId } : { } ) ,
. . . ( workspaceRepoUrl ? { repoUrl : workspaceRepoUrl } : { } ) ,
. . . ( workspaceRepoRef ? { repoRef : workspaceRepoRef } : { } ) ,
} as Record < string , unknown > )
2026-02-19 14:01:58 -06:00
: null ;
2026-02-19 14:39:37 -06:00
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 } ` ;
2026-02-18 13:53:03 -06:00
return {
2026-02-19 14:01:58 -06:00
exitCode : attempt.proc.exitCode ,
signal : attempt.proc.signal ,
timedOut : false ,
errorMessage :
( attempt . proc . exitCode ? ? 0 ) === 0
? null
2026-02-19 14:39:37 -06:00
: fallbackErrorMessage ,
2026-02-19 14:01:58 -06:00
usage : attempt.parsed.usage ,
sessionId : resolvedSessionId ,
sessionParams : resolvedSessionParams ,
sessionDisplayId : resolvedSessionId ,
provider : "openai" ,
2026-03-14 22:00:12 -05:00
biller : resolveCodexBiller ( effectiveEnv , billingType ) ,
2026-02-19 14:01:58 -06:00
model ,
2026-02-25 21:35:44 -06:00
billingType ,
2026-02-19 14:01:58 -06:00
costUsd : null ,
resultJson : {
stdout : attempt.proc.stdout ,
stderr : attempt.proc.stderr ,
} ,
summary : attempt.parsed.summary ,
clearSession : Boolean ( clearSessionOnMissingSession && ! resolvedSessionId ) ,
2026-02-18 13:53:03 -06:00
} ;
2026-02-19 14:01:58 -06:00
} ;
2026-02-18 13:53:03 -06:00
2026-02-19 14:01:58 -06:00
const initial = await runAttempt ( sessionId ) ;
if (
sessionId &&
! initial . proc . timedOut &&
( initial . proc . exitCode ? ? 0 ) !== 0 &&
2026-02-19 14:39:37 -06:00
isCodexUnknownSessionError ( initial . proc . stdout , initial . rawStderr )
2026-02-19 14:01:58 -06:00
) {
await onLog (
2026-03-18 21:16:37 -05:00
"stdout" ,
2026-02-19 14:01:58 -06:00
` [paperclip] Codex resume session " ${ sessionId } " is unavailable; retrying with a fresh session. \ n ` ,
) ;
const retry = await runAttempt ( null ) ;
return toResult ( retry , true ) ;
}
2026-02-18 13:53:03 -06:00
2026-02-19 14:01:58 -06:00
return toResult ( initial ) ;
2026-02-18 13:53:03 -06:00
}