import { logger } from '@/ui/logger'; import { loop, type EnhancedMode, type PermissionMode } from './loop'; import { MessageQueue2 } from '@/utils/MessageQueue2'; import { hashObject } from '@/utils/deterministicJson'; import { registerKillSessionHandler } from '@/claude/registerKillSessionHandler'; import type { AgentState } from '@/api/types'; import type { CodexSession } from './session'; import { parseCodexCliOverrides } from './utils/codexCliOverrides'; import { bootstrapSession } from '@/agent/sessionFactory'; import { createModeChangeHandler, createRunnerLifecycle, setControlledByUser } from '@/agent/runnerLifecycle'; import { isPermissionModeAllowedForFlavor } from '@hapi/protocol'; import { CodexCollaborationModeSchema, PermissionModeSchema } from '@hapi/protocol/schemas'; import { formatMessageWithAttachments } from '@/utils/attachmentFormatter'; export { emitReadyIfIdle } from './utils/emitReadyIfIdle'; export async function runCodex(opts: { startedBy?: 'runner' | 'terminal'; codexArgs?: string[]; permissionMode?: PermissionMode; resumeSessionId?: string; model?: string; }): Promise { const workingDirectory = process.cwd(); const startedBy = opts.startedBy ?? 'terminal'; logger.debug(`[codex] Starting with options: startedBy=${startedBy}`); let state: AgentState = { controlledByUser: false }; const { api, session } = await bootstrapSession({ flavor: 'codex', startedBy, workingDirectory, agentState: state, model: opts.model }); const startingMode: 'local' | 'remote' = startedBy === 'runner' ? 'remote' : 'local'; setControlledByUser(session, startingMode); const messageQueue = new MessageQueue2((mode) => hashObject({ permissionMode: mode.permissionMode, model: mode.model, collaborationMode: mode.collaborationMode })); const codexCliOverrides = parseCodexCliOverrides(opts.codexArgs); const sessionWrapperRef: { current: CodexSession | null } = { current: null }; let currentPermissionMode: PermissionMode = opts.permissionMode ?? 'default'; let currentModel = opts.model; let currentCollaborationMode: EnhancedMode['collaborationMode'] = 'default'; const lifecycle = createRunnerLifecycle({ session, logTag: 'codex', stopKeepAlive: () => sessionWrapperRef.current?.stopKeepAlive() }); lifecycle.registerProcessHandlers(); registerKillSessionHandler(session.rpcHandlerManager, lifecycle.cleanupAndExit); const syncSessionMode = () => { const sessionInstance = sessionWrapperRef.current; if (!sessionInstance) { return; } const sessionModel = sessionInstance.getModel(); if (sessionModel !== undefined) { currentModel = sessionModel ?? undefined; } sessionInstance.setPermissionMode(currentPermissionMode); sessionInstance.setModel(currentModel ?? null); sessionInstance.setCollaborationMode(currentCollaborationMode); logger.debug( `[Codex] Synced session config for keepalive: ` + `permissionMode=${currentPermissionMode}, model=${currentModel ?? 'auto'}, collaborationMode=${currentCollaborationMode}` ); }; session.onUserMessage((message) => { const sessionPermissionMode = sessionWrapperRef.current?.getPermissionMode(); if (sessionPermissionMode && isPermissionModeAllowedForFlavor(sessionPermissionMode, 'codex')) { currentPermissionMode = sessionPermissionMode as PermissionMode; } const sessionModel = sessionWrapperRef.current?.getModel(); if (sessionModel !== undefined) { currentModel = sessionModel ?? undefined; } const sessionCollaborationMode = sessionWrapperRef.current?.getCollaborationMode(); if (sessionCollaborationMode) { currentCollaborationMode = sessionCollaborationMode; } const messagePermissionMode = currentPermissionMode; logger.debug( `[Codex] User message received with permission mode: ${currentPermissionMode}, ` + `model: ${currentModel ?? 'auto'}, collaborationMode: ${currentCollaborationMode}` ); const enhancedMode: EnhancedMode = { permissionMode: messagePermissionMode ?? 'default', model: currentModel, collaborationMode: currentCollaborationMode }; const formattedText = formatMessageWithAttachments(message.content.text, message.content.attachments); messageQueue.push(formattedText, enhancedMode); }); const formatFailureReason = (message: string): string => { const maxLength = 200; if (message.length <= maxLength) { return message; } return `${message.slice(0, maxLength)}...`; }; const resolvePermissionMode = (value: unknown): PermissionMode => { const parsed = PermissionModeSchema.safeParse(value); if (!parsed.success || !isPermissionModeAllowedForFlavor(parsed.data, 'codex')) { throw new Error('Invalid permission mode'); } return parsed.data as PermissionMode; }; const resolveCollaborationMode = (value: unknown): EnhancedMode['collaborationMode'] => { if (value === null) { return 'default'; } const parsed = CodexCollaborationModeSchema.safeParse(value); if (!parsed.success) { throw new Error('Invalid collaboration mode'); } return parsed.data; }; session.rpcHandlerManager.registerHandler('set-session-config', async (payload: unknown) => { if (!payload || typeof payload !== 'object') { throw new Error('Invalid session config payload'); } const config = payload as { permissionMode?: unknown; collaborationMode?: unknown }; if (config.permissionMode !== undefined) { currentPermissionMode = resolvePermissionMode(config.permissionMode); } if (config.collaborationMode !== undefined) { currentCollaborationMode = resolveCollaborationMode(config.collaborationMode); } syncSessionMode(); return { applied: { permissionMode: currentPermissionMode, collaborationMode: currentCollaborationMode } }; }); try { await loop({ path: workingDirectory, startingMode, messageQueue, api, session, codexArgs: opts.codexArgs, codexCliOverrides, startedBy, permissionMode: currentPermissionMode, model: currentModel, collaborationMode: currentCollaborationMode, resumeSessionId: opts.resumeSessionId, onModeChange: createModeChangeHandler(session), onSessionReady: (instance) => { sessionWrapperRef.current = instance; syncSessionMode(); } }); } catch (error) { lifecycle.markCrash(error); logger.debug('[codex] Loop error:', error); } finally { const localFailure = sessionWrapperRef.current?.localLaunchFailure; if (localFailure?.exitReason === 'exit') { lifecycle.setExitCode(1); lifecycle.setArchiveReason(`Local launch failed: ${formatFailureReason(localFailure.message)}`); } await lifecycle.cleanupAndExit(); } }