import { getCodexSessionMessages } from '@/projects.js'; import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js'; import { CodexProviderAuth } from '@/modules/providers/list/codex/codex-auth.provider.js'; import { CodexMcpProvider } from '@/modules/providers/list/codex/codex-mcp.provider.js'; import type { IProviderAuth } from '@/shared/interfaces.js'; import type { FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js'; import { createNormalizedMessage, generateMessageId, readObjectRecord } from '@/shared/utils.js'; const PROVIDER = 'codex'; type RawProviderMessage = Record; type CodexHistoryResult = | RawProviderMessage[] | { messages?: RawProviderMessage[]; total?: number; hasMore?: boolean; tokenUsage?: unknown; }; const loadCodexSessionMessages = getCodexSessionMessages as unknown as ( sessionId: string, limit: number | null, offset: number, ) => Promise; function readRawProviderMessage(raw: unknown): RawProviderMessage | null { return readObjectRecord(raw) as RawProviderMessage | null; } export class CodexProvider extends AbstractProvider { readonly mcp = new CodexMcpProvider(); readonly auth: IProviderAuth = new CodexProviderAuth(); constructor() { super('codex'); } /** * Normalizes a persisted Codex JSONL entry. * * Live Codex SDK events are transformed before they reach normalizeMessage(), * while history entries already use a compact message/tool shape from projects.js. */ private normalizeHistoryEntry(raw: RawProviderMessage, sessionId: string | null): NormalizedMessage[] { const ts = raw.timestamp || new Date().toISOString(); const baseId = raw.uuid || generateMessageId('codex'); if (raw.message?.role === 'user') { const content = typeof raw.message.content === 'string' ? raw.message.content : Array.isArray(raw.message.content) ? raw.message.content .map((part: string | RawProviderMessage) => typeof part === 'string' ? part : part?.text || '') .filter(Boolean) .join('\n') : String(raw.message.content || ''); if (!content.trim()) { return []; } return [createNormalizedMessage({ id: baseId, sessionId, timestamp: ts, provider: PROVIDER, kind: 'text', role: 'user', content, })]; } if (raw.message?.role === 'assistant') { const content = typeof raw.message.content === 'string' ? raw.message.content : Array.isArray(raw.message.content) ? raw.message.content .map((part: string | RawProviderMessage) => typeof part === 'string' ? part : part?.text || '') .filter(Boolean) .join('\n') : ''; if (!content.trim()) { return []; } return [createNormalizedMessage({ id: baseId, sessionId, timestamp: ts, provider: PROVIDER, kind: 'text', role: 'assistant', content, })]; } if (raw.type === 'thinking' || raw.isReasoning) { return [createNormalizedMessage({ id: baseId, sessionId, timestamp: ts, provider: PROVIDER, kind: 'thinking', content: raw.message?.content || '', })]; } if (raw.type === 'tool_use' || raw.toolName) { return [createNormalizedMessage({ id: baseId, sessionId, timestamp: ts, provider: PROVIDER, kind: 'tool_use', toolName: raw.toolName || 'Unknown', toolInput: raw.toolInput, toolId: raw.toolCallId || baseId, })]; } if (raw.type === 'tool_result') { return [createNormalizedMessage({ id: baseId, sessionId, timestamp: ts, provider: PROVIDER, kind: 'tool_result', toolId: raw.toolCallId || '', content: raw.output || '', isError: Boolean(raw.isError), })]; } return []; } /** * Normalizes either a Codex history entry or a transformed live SDK event. */ normalizeMessage(rawMessage: unknown, sessionId: string | null): NormalizedMessage[] { const raw = readRawProviderMessage(rawMessage); if (!raw) { return []; } if (raw.message?.role) { return this.normalizeHistoryEntry(raw, sessionId); } const ts = raw.timestamp || new Date().toISOString(); const baseId = raw.uuid || generateMessageId('codex'); if (raw.type === 'item') { switch (raw.itemType) { case 'agent_message': return [createNormalizedMessage({ id: baseId, sessionId, timestamp: ts, provider: PROVIDER, kind: 'text', role: 'assistant', content: raw.message?.content || '', })]; case 'reasoning': return [createNormalizedMessage({ id: baseId, sessionId, timestamp: ts, provider: PROVIDER, kind: 'thinking', content: raw.message?.content || '', })]; case 'command_execution': return [createNormalizedMessage({ id: baseId, sessionId, timestamp: ts, provider: PROVIDER, kind: 'tool_use', toolName: 'Bash', toolInput: { command: raw.command }, toolId: baseId, output: raw.output, exitCode: raw.exitCode, status: raw.status, })]; case 'file_change': return [createNormalizedMessage({ id: baseId, sessionId, timestamp: ts, provider: PROVIDER, kind: 'tool_use', toolName: 'FileChanges', toolInput: raw.changes, toolId: baseId, status: raw.status, })]; case 'mcp_tool_call': return [createNormalizedMessage({ id: baseId, sessionId, timestamp: ts, provider: PROVIDER, kind: 'tool_use', toolName: raw.tool || 'MCP', toolInput: raw.arguments, toolId: baseId, server: raw.server, result: raw.result, error: raw.error, status: raw.status, })]; case 'web_search': return [createNormalizedMessage({ id: baseId, sessionId, timestamp: ts, provider: PROVIDER, kind: 'tool_use', toolName: 'WebSearch', toolInput: { query: raw.query }, toolId: baseId, })]; case 'todo_list': return [createNormalizedMessage({ id: baseId, sessionId, timestamp: ts, provider: PROVIDER, kind: 'tool_use', toolName: 'TodoList', toolInput: { items: raw.items }, toolId: baseId, })]; case 'error': return [createNormalizedMessage({ id: baseId, sessionId, timestamp: ts, provider: PROVIDER, kind: 'error', content: raw.message?.content || 'Unknown error', })]; default: return [createNormalizedMessage({ id: baseId, sessionId, timestamp: ts, provider: PROVIDER, kind: 'tool_use', toolName: raw.itemType || 'Unknown', toolInput: raw.item || raw, toolId: baseId, })]; } } if (raw.type === 'turn_complete') { return [createNormalizedMessage({ id: baseId, sessionId, timestamp: ts, provider: PROVIDER, kind: 'complete', })]; } if (raw.type === 'turn_failed') { return [createNormalizedMessage({ id: baseId, sessionId, timestamp: ts, provider: PROVIDER, kind: 'error', content: raw.error?.message || 'Turn failed', })]; } return []; } /** * Loads Codex JSONL history and keeps token usage metadata when projects.js * provides it. */ async fetchHistory( sessionId: string, options: FetchHistoryOptions = {}, ): Promise { const { limit = null, offset = 0 } = options; let result: CodexHistoryResult; try { result = await loadCodexSessionMessages(sessionId, limit, offset); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.warn(`[CodexProvider] Failed to load session ${sessionId}:`, message); return { messages: [], total: 0, hasMore: false, offset: 0, limit: null }; } const rawMessages = Array.isArray(result) ? result : (result.messages || []); const total = Array.isArray(result) ? rawMessages.length : (result.total || 0); const hasMore = Array.isArray(result) ? false : Boolean(result.hasMore); const tokenUsage = Array.isArray(result) ? undefined : result.tokenUsage; const normalized: NormalizedMessage[] = []; for (const raw of rawMessages) { normalized.push(...this.normalizeHistoryEntry(raw, sessionId)); } const toolResultMap = new Map(); for (const msg of normalized) { if (msg.kind === 'tool_result' && msg.toolId) { toolResultMap.set(msg.toolId, msg); } } for (const msg of normalized) { if (msg.kind === 'tool_use' && msg.toolId && toolResultMap.has(msg.toolId)) { const toolResult = toolResultMap.get(msg.toolId); if (toolResult) { msg.toolResult = { content: toolResult.content, isError: toolResult.isError }; } } } return { messages: normalized, total, hasMore, offset, limit, tokenUsage, }; } }