import { logger } from '@/ui/logger'; type ConvertedEvent = { type: string; [key: string]: unknown; }; function asRecord(value: unknown): Record | null { if (!value || typeof value !== 'object') { return null; } return value as Record; } function asString(value: unknown): string | null { return typeof value === 'string' && value.length > 0 ? value : null; } function asBoolean(value: unknown): boolean | null { return typeof value === 'boolean' ? value : null; } function asNumber(value: unknown): number | null { return typeof value === 'number' && Number.isFinite(value) ? value : null; } function extractItemId(params: Record): string | null { const direct = asString(params.itemId ?? params.item_id ?? params.id); if (direct) return direct; const item = asRecord(params.item); if (item) { return asString(item.id ?? item.itemId ?? item.item_id); } return null; } function extractItem(params: Record): Record | null { const item = asRecord(params.item); return item ?? params; } function normalizeItemType(value: unknown): string | null { const raw = asString(value); if (!raw) return null; return raw.toLowerCase().replace(/[\s_-]/g, ''); } function extractCommand(value: unknown): string | null { if (typeof value === 'string') return value; if (Array.isArray(value)) { const parts = value.filter((part): part is string => typeof part === 'string'); return parts.length > 0 ? parts.join(' ') : null; } return null; } function extractGeneratedImagePath(item: Record): string | null { return asString( item.savedPath ?? item.saved_path ?? item.path ?? item.filePath ?? item.file_path ?? item.outputPath ?? item.output_path ); } function extractGeneratedImageMimeType(item: Record): string | null { return asString(item.mimeType ?? item.mime_type ?? item.mediaType ?? item.media_type); } function extractGeneratedImageFileName(item: Record, savedPath: string): string { const direct = asString(item.fileName ?? item.file_name ?? item.filename ?? item.name); if (direct) return direct; return savedPath.split(/[\\/]/).filter(Boolean).pop() ?? 'generated-image.png'; } function extractChanges(value: unknown): Record | null { const record = asRecord(value); if (record) return record; if (Array.isArray(value)) { const changes: Record = {}; for (const entry of value) { const entryRecord = asRecord(entry); if (!entryRecord) continue; const path = asString(entryRecord.path ?? entryRecord.file ?? entryRecord.filePath ?? entryRecord.file_path); if (path) { changes[path] = entryRecord; } } return Object.keys(changes).length > 0 ? changes : null; } return null; } function extractTextFromContent(value: unknown): string | null { if (typeof value === 'string' && value.length > 0) { return value; } if (!Array.isArray(value)) { return null; } const chunks: string[] = []; for (const entry of value) { const record = asRecord(entry); if (!record) continue; const text = asString(record.text ?? record.message ?? record.content); if (text) { chunks.push(text); } } if (chunks.length === 0) { return null; } return chunks.join(''); } function extractItemText(item: Record): string | null { return asString(item.text ?? item.message) ?? extractTextFromContent(item.content); } function extractReasoningText(item: Record): string | null { const direct = extractItemText(item); if (direct) { return direct; } const summary = item.summary_text ?? item.summaryText; if (Array.isArray(summary)) { const chunks = summary.filter((part): part is string => typeof part === 'string' && part.length > 0); if (chunks.length > 0) { return chunks.join('\n'); } } return null; } function normalizePlanStatus(value: unknown): 'pending' | 'in_progress' | 'completed' { const raw = typeof value === 'string' ? value.trim().toLowerCase().replace(/[\s-]/g, '_') : ''; if (raw === 'completed' || raw === 'complete' || raw === 'done') return 'completed'; if (raw === 'in_progress' || raw === 'inprogress' || raw === 'active' || raw === 'running') return 'in_progress'; return 'pending'; } function extractPlanEntries(value: unknown): Array<{ step: string; status: 'pending' | 'in_progress' | 'completed' }> { const record = asRecord(value); const entries = Array.isArray(value) ? value : Array.isArray(record?.plan) ? record.plan : Array.isArray(record?.items) ? record.items : Array.isArray(record?.steps) ? record.steps : []; const plan: Array<{ step: string; status: 'pending' | 'in_progress' | 'completed' }> = []; for (const entry of entries) { if (typeof entry === 'string') { plan.push({ step: entry, status: 'pending' }); continue; } const item = asRecord(entry); if (!item) continue; const step = asString(item.step ?? item.content ?? item.text ?? item.title ?? item.description); if (!step) continue; plan.push({ step, status: normalizePlanStatus(item.status ?? item.state) }); } return plan; } function extractPlanUpdate(params: Record): ConvertedEvent[] { const plan = extractPlanEntries( params.plan ?? params.update ?? params.items ?? params.steps ?? params ); return plan.length > 0 ? [{ type: 'plan_update', plan }] : []; } function extractEventScope(params: Record): Record { const thread = asRecord(params.thread); const turn = asRecord(params.turn); const tokenUsage = asRecord(params.tokenUsage ?? params.token_usage ?? params.info); const tokenUsageThread = asRecord(tokenUsage?.thread); const tokenUsageTurn = asRecord(tokenUsage?.turn); const item = asRecord(params.item); const itemThread = asRecord(item?.thread); const itemTurn = asRecord(item?.turn); const threadId = asString( params.threadId ?? params.thread_id ?? thread?.threadId ?? thread?.thread_id ?? thread?.id ?? tokenUsage?.threadId ?? tokenUsage?.thread_id ?? tokenUsageThread?.threadId ?? tokenUsageThread?.thread_id ?? tokenUsageThread?.id ?? item?.threadId ?? item?.thread_id ?? itemThread?.threadId ?? itemThread?.thread_id ?? itemThread?.id ); const turnId = asString( params.turnId ?? params.turn_id ?? turn?.turnId ?? turn?.turn_id ?? turn?.id ?? tokenUsage?.turnId ?? tokenUsage?.turn_id ?? tokenUsageTurn?.turnId ?? tokenUsageTurn?.turn_id ?? tokenUsageTurn?.id ?? item?.turnId ?? item?.turn_id ?? itemTurn?.turnId ?? itemTurn?.turn_id ?? itemTurn?.id ); return { ...(threadId ? { thread_id: threadId } : {}), ...(turnId ? { turn_id: turnId } : {}) }; } function addEventScope(events: ConvertedEvent[], scope: Record): ConvertedEvent[] { if (Object.keys(scope).length === 0) { return events; } return events.map((event) => ({ ...scope, ...event })); } const MAX_UNHANDLED_LOG_STRING_LENGTH = 512; const MAX_UNHANDLED_LOG_ARRAY_LENGTH = 20; const MAX_UNHANDLED_LOG_DEPTH = 8; function sanitizeUnhandledNotificationLogValue(value: unknown, depth: number = 0): unknown { if (typeof value === 'string') { if (value.length <= MAX_UNHANDLED_LOG_STRING_LENGTH) { return value; } return `${value.slice(0, MAX_UNHANDLED_LOG_STRING_LENGTH)}... [truncated ${value.length - MAX_UNHANDLED_LOG_STRING_LENGTH} chars for logs]`; } if (Array.isArray(value)) { const items = value .slice(0, MAX_UNHANDLED_LOG_ARRAY_LENGTH) .map((item) => sanitizeUnhandledNotificationLogValue(item, depth + 1)); if (value.length > MAX_UNHANDLED_LOG_ARRAY_LENGTH) { items.push(`... [truncated ${value.length - MAX_UNHANDLED_LOG_ARRAY_LENGTH} array items for logs]`); } return items; } if (!value || typeof value !== 'object') { return value; } if (depth >= MAX_UNHANDLED_LOG_DEPTH) { return '[truncated nested object for logs]'; } const result: Record = {}; for (const [key, nestedValue] of Object.entries(value)) { result[key] = sanitizeUnhandledNotificationLogValue(nestedValue, depth + 1); } return result; } function normalizeCodexAgentToolName(value: unknown): string | null { const raw = asString(value); if (!raw) return null; const normalized = raw.trim().toLowerCase().replace(/[\s_-]/g, ''); if (normalized === 'spawnagent' || normalized === 'spawn') return 'spawn_agent'; if (normalized === 'sendinput') return 'send_input'; if (normalized === 'sendmessage') return 'send_message'; if (normalized === 'resumeagent' || normalized === 'resume') return 'resume_agent'; if (normalized === 'followuptask' || normalized === 'assigntask') return 'followup_task'; if (normalized === 'waitagent' || normalized === 'wait') return 'wait_agent'; if (normalized === 'closeagent' || normalized === 'close') return 'close_agent'; if (normalized === 'interruptagent' || normalized === 'interrupt') return 'interrupt_agent'; if (normalized === 'listagents') return 'list_agents'; return null; } function normalizeCollabAgentToolName(value: unknown): string | null { const toolName = normalizeCodexAgentToolName(value); return toolName === 'spawn_agent' || toolName === 'send_input' || toolName === 'resume_agent' || toolName === 'wait_agent' || toolName === 'close_agent' ? toolName : null; } function parseRawToolInput(value: unknown): unknown { if (typeof value !== 'string') return value ?? {}; try { return JSON.parse(value) as unknown; } catch { return value; } } const MULTI_AGENT_V1_NAMESPACE = 'multi_agent_v1'; function isRawMultiAgentV2Call( toolName: string, input: unknown, namespace: unknown ): boolean { // V1 already has richer collabAgentToolCall lifecycle items. V2 uses the // configurable namespace ("collaboration" by default), so only the fixed // V1 namespace can be excluded here. if (asString(namespace) === MULTI_AGENT_V1_NAMESPACE) return false; if ( toolName === 'send_message' || toolName === 'followup_task' || toolName === 'interrupt_agent' || toolName === 'list_agents' ) { return true; } const inputRecord = asRecord(input); if (toolName === 'spawn_agent') { return Boolean(asString(inputRecord?.task_name ?? inputRecord?.taskName)); } if (toolName === 'wait_agent') { return !Array.isArray(inputRecord?.targets); } return false; } function extractStringArray(value: unknown): string[] { return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0) : []; } function extractCodexErrorInfo( record: Record, errorRecord: Record | null ): string | null { return asString( record.codexErrorInfo ?? record.codex_error_info ?? errorRecord?.codexErrorInfo ?? errorRecord?.codex_error_info ); } function buildCollabAgentInput(item: Record, toolName: string): Record { const targets = extractStringArray(item.receiverThreadIds ?? item.receiver_thread_ids ?? item.targets); const input: Record = {}; const prompt = asString(item.prompt ?? item.message); if (prompt) { input.message = prompt; } const agentType = asString(item.agentType ?? item.agent_type); if (agentType) { input.agent_type = agentType; } const forkContext = asBoolean(item.forkContext ?? item.fork_context); if (forkContext !== null) { input.fork_context = forkContext; } const model = asString(item.model); if (model) { input.model = model; } const reasoningEffort = asString(item.reasoningEffort ?? item.reasoning_effort); if (reasoningEffort) { input.reasoning_effort = reasoningEffort; } const senderThreadId = asString(item.senderThreadId ?? item.sender_thread_id); if (senderThreadId) { input.sender_thread_id = senderThreadId; } if (targets.length > 0) { input.targets = targets; if (toolName === 'close_agent' || toolName === 'send_input' || toolName === 'resume_agent') { input.target = targets[0]; } } return input; } function statusObjectFromAgentState(value: unknown): unknown { const record = asRecord(value); if (!record) return value; const message = asString(record.message) ?? asString(record.output) ?? asString(record.result) ?? asString(record.finalMessage) ?? asString(record.final_message); const status = asString(record.status ?? record.state); const normalizedStatus = status?.trim().toLowerCase().replace(/[\s_-]/g, ''); const completed = normalizedStatus === 'completed' || normalizedStatus === 'complete' || normalizedStatus === 'done' || record.completed === true || record.done === true; if (completed && message) return { completed: message }; if (completed) return { ...record, status: 'completed' }; if ((normalizedStatus === 'failed' || normalizedStatus === 'error') && message) return { failed: message }; if ((normalizedStatus === 'canceled' || normalizedStatus === 'cancelled') && message) return { canceled: message }; return value; } function buildCollabAgentOutput(item: Record, toolName: string): Record { const targets = extractStringArray(item.receiverThreadIds ?? item.receiver_thread_ids ?? item.targets); const agentsStates = asRecord(item.agentsStates ?? item.agents_states) ?? {}; const status = asString(item.status); const error = asString(item.error ?? item.message); const errorFields = error ? { error, message: error } : {}; if (toolName === 'spawn_agent') { const agentId = targets[0] ?? null; return { ...(agentId ? { agent_id: agentId, agentId } : {}), ...(status ? { status } : {}), ...errorFields, agentsStates }; } if (toolName === 'wait_agent') { const normalizedStatus: Record = {}; for (const [agentId, agentStatus] of Object.entries(agentsStates)) { normalizedStatus[agentId] = statusObjectFromAgentState(agentStatus); } return { status: normalizedStatus, ...errorFields, timed_out: status === 'timedOut' || status === 'timed_out' }; } if (toolName === 'close_agent') { const firstStatus = targets[0] ? agentsStates[targets[0]] : Object.values(agentsStates)[0]; return { previous_status: statusObjectFromAgentState(firstStatus), ...errorFields, ...(targets[0] ? { agent_id: targets[0] } : {}) }; } return { ...(targets.length > 0 ? { targets } : {}), ...(status ? { status } : {}), ...errorFields, agentsStates }; } export class AppServerEventConverter { private readonly agentMessageBuffers = new Map(); private readonly reasoningBuffers = new Map(); private readonly commandOutputBuffers = new Map(); private readonly commandMeta = new Map>(); private readonly fileChangeMeta = new Map>(); private readonly completedAgentMessageItems = new Set(); private readonly completedReasoningItems = new Set(); private readonly reasoningSectionBreakKeys = new Set(); private readonly lastAgentMessageDeltaByItemId = new Map(); private readonly lastReasoningDeltaByItemId = new Map(); private readonly lastCommandOutputDeltaByItemId = new Map(); private readonly rawAgentToolCallIds = new Set(); private readonly rawAgentToolNames = new Map(); private handleWrappedCodexEvent(paramsRecord: Record): ConvertedEvent[] | null { const msg = asRecord(paramsRecord.msg); if (!msg) { return []; } const msgType = asString(msg.type); if (!msgType) { return []; } const msgScope = extractEventScope(msg); if (msgType === 'item_started' || msgType === 'item_completed') { const itemMethod = msgType === 'item_started' ? 'item/started' : 'item/completed'; const item = asRecord(msg.item) ?? {}; const threadId = asString(msg.thread_id ?? msg.threadId ?? msgScope.thread_id); const turnId = asString(msg.turn_id ?? msg.turnId ?? msgScope.turn_id); const params: Record = { ...msgScope, item, itemId: asString(msg.item_id ?? msg.itemId ?? item.id), ...(threadId ? { threadId } : {}), ...(turnId ? { turnId } : {}) }; return this.handleNotification(itemMethod, params); } if ( msgType === 'task_started' || msgType === 'task_complete' || msgType === 'turn_aborted' || msgType === 'task_failed' ) { const turnId = asString(msg.turn_id ?? msg.turnId ?? msgScope.turn_id); if ((msgType === 'task_complete' || msgType === 'turn_aborted' || msgType === 'task_failed') && !turnId) { logger.debug('[AppServerEventConverter] Ignoring wrapped terminal event without turn_id', { msgType }); return []; } const event: ConvertedEvent = { ...msgScope, type: msgType }; if (turnId) { event.turn_id = turnId; } const threadId = asString(msg.thread_id ?? msg.threadId ?? msgScope.thread_id); if (threadId) { event.thread_id = threadId; } if (msgType === 'task_failed') { const error = asString(msg.error ?? msg.message ?? asRecord(msg.error)?.message); if (error) { event.error = error; } } return [event]; } if (msgType === 'agent_message_delta' || msgType === 'agent_message_content_delta') { const itemId = asString(msg.item_id ?? msg.itemId ?? msg.id) ?? 'agent-message'; const delta = asString(msg.delta ?? msg.text ?? msg.message); if (!delta) return []; return this.handleNotification('item/agentMessage/delta', { itemId, delta, ...msgScope }); } if (msgType === 'reasoning_content_delta') { const itemId = asString(msg.item_id ?? msg.itemId ?? msg.id) ?? 'reasoning'; const delta = asString(msg.delta ?? msg.text ?? msg.message); if (!delta) return []; return this.handleNotification('item/reasoning/summaryTextDelta', { itemId, delta, ...msgScope }); } if (msgType === 'agent_reasoning_section_break') { const itemId = asString(msg.item_id ?? msg.itemId ?? msg.id) ?? 'reasoning'; const summaryIndex = asNumber(msg.summary_index ?? msg.summaryIndex); return this.handleNotification('item/reasoning/summaryPartAdded', { itemId, ...msgScope, ...(summaryIndex !== null ? { summaryIndex } : {}) }); } if (msgType === 'agent_reasoning_delta' || msgType === 'agent_reasoning' || msgType === 'agent_message') { return []; } if (msgType === 'exec_command_output_delta') { const itemId = asString(msg.call_id ?? msg.callId ?? msg.item_id ?? msg.itemId ?? msg.id); const delta = asString(msg.delta ?? msg.output ?? msg.stdout ?? msg.text); if (!itemId || !delta) return []; return this.handleNotification('item/commandExecution/outputDelta', { itemId, delta, ...msgScope }); } if (msgType === 'error') { const errorRecord = asRecord(msg.error); const retryable = asBoolean(msg.will_retry ?? msg.willRetry ?? errorRecord?.will_retry ?? errorRecord?.willRetry); const willRetry = retryable ?? false; if (willRetry) { return []; } const error = asString(msg.message ?? msg.reason ?? errorRecord?.message); const codexErrorInfo = extractCodexErrorInfo(msg, errorRecord); return error ? addEventScope([{ type: 'task_failed', ...(retryable !== null ? { retryable } : {}), ...(codexErrorInfo ? { codex_error_info: codexErrorInfo } : {}), error }], msgScope) : []; } if (msgType === 'plan_update') { return addEventScope(extractPlanUpdate(msg), msgScope); } if (msgType === 'context_compacted') { const threadId = asString(msg.thread_id ?? msg.threadId ?? msgScope.thread_id); if (!threadId) { return []; } const turnId = asString(msg.turn_id ?? msg.turnId ?? msgScope.turn_id); return [ { type: 'thread_compacted', thread_id: threadId, ...(turnId ? { turn_id: turnId } : {}) }, ...addEventScope([{ type: 'context_compacted' }], msgScope) ]; } if ( msgType === 'mcp_startup_update' || msgType === 'mcp_startup_complete' || msgType === 'skills_update_available' || msgType === 'stream_error' || msgType === 'warning' || msgType === 'terminal_interaction' || msgType === 'user_message' ) { return []; } return addEventScope([msg as ConvertedEvent], msgScope); } handleNotification(method: string, params: unknown): ConvertedEvent[] { const events: ConvertedEvent[] = []; const paramsRecord = asRecord(params) ?? {}; const eventScope = extractEventScope(paramsRecord); const scoped = (event: ConvertedEvent): ConvertedEvent => ({ ...eventScope, ...event }); if (method.startsWith('codex/event/')) { return this.handleWrappedCodexEvent(paramsRecord) ?? events; } if (method === 'turn/plan/updated') { return addEventScope(extractPlanUpdate(paramsRecord), eventScope); } if (method === 'account/rateLimits/updated') { return events; } if (method === 'thread/compacted') { const threadId = asString(paramsRecord.threadId ?? paramsRecord.thread_id ?? eventScope.thread_id); if (!threadId) { return events; } const turnId = asString(paramsRecord.turnId ?? paramsRecord.turn_id ?? eventScope.turn_id); events.push({ type: 'thread_compacted', thread_id: threadId, ...(turnId ? { turn_id: turnId } : {}) }); events.push(scoped({ type: 'context_compacted' })); return events; } if (method === 'thread/goal/updated') { const goal = asRecord(paramsRecord.goal); const threadId = asString(paramsRecord.threadId ?? paramsRecord.thread_id ?? goal?.threadId ?? goal?.thread_id); if (!threadId || !goal) { return events; } const turnId = asString(paramsRecord.turnId ?? paramsRecord.turn_id); events.push({ type: 'thread_goal_updated', thread_id: threadId, ...(turnId ? { turn_id: turnId } : {}), goal }); return events; } if (method === 'thread/goal/cleared') { const threadId = asString(paramsRecord.threadId ?? paramsRecord.thread_id ?? eventScope.thread_id); if (!threadId) { return events; } events.push({ type: 'thread_goal_cleared', thread_id: threadId }); return events; } if (method === 'thread/started' || method === 'thread/resumed') { const thread = asRecord(paramsRecord.thread) ?? paramsRecord; const threadId = asString(thread.threadId ?? thread.thread_id ?? thread.id); if (threadId) { events.push({ type: 'thread_started', thread_id: threadId }); } return events; } if (method === 'thread/status/changed') { const thread = asRecord(paramsRecord.thread) ?? paramsRecord; const threadId = asString(thread.threadId ?? thread.thread_id ?? thread.id); const status = asRecord(paramsRecord.status ?? thread.status); const statusType = asString(status?.type ?? paramsRecord.statusType ?? paramsRecord.status_type); if (statusType === 'systemError') { const error = asString(status?.message ?? status?.error ?? paramsRecord.message ?? paramsRecord.error) ?? 'Codex thread entered systemError'; events.push(scoped({ type: 'task_failed', ...(threadId ? { thread_id: threadId } : {}), terminal_source: 'thread_status', error })); } return events; } if (method === 'turn/started') { const turn = asRecord(paramsRecord.turn) ?? paramsRecord; const turnId = asString(turn.turnId ?? turn.turn_id ?? turn.id); events.push(scoped({ type: 'task_started', ...(turnId ? { turn_id: turnId } : {}) })); return events; } if (method === 'turn/completed') { const turn = asRecord(paramsRecord.turn) ?? paramsRecord; const statusRaw = asString(paramsRecord.status ?? turn.status); const status = statusRaw?.toLowerCase(); const turnId = asString(turn.turnId ?? turn.turn_id ?? turn.id); const turnError = asRecord(paramsRecord.error ?? turn.error); const errorMessage = asString(paramsRecord.error ?? paramsRecord.message ?? paramsRecord.reason) ?? asString(turnError?.message); if (status === 'interrupted' || status === 'cancelled' || status === 'canceled') { events.push(scoped({ type: 'turn_aborted', ...(turnId ? { turn_id: turnId } : {}) })); return events; } if (status === 'failed' || status === 'error') { const codexErrorInfo = extractCodexErrorInfo(paramsRecord, turnError); events.push(scoped({ type: 'task_failed', ...(turnId ? { turn_id: turnId } : {}), terminal_source: 'turn_completed', ...(codexErrorInfo ? { codex_error_info: codexErrorInfo } : {}), ...(errorMessage ? { error: errorMessage } : {}) })); return events; } events.push(scoped({ type: 'task_complete', ...(turnId ? { turn_id: turnId } : {}) })); return events; } if (method === 'turn/diff/updated') { const diff = asString(paramsRecord.diff ?? paramsRecord.unified_diff ?? paramsRecord.unifiedDiff); if (diff) { events.push(scoped({ type: 'turn_diff', unified_diff: diff })); } return events; } if (method === 'thread/tokenUsage/updated') { const info = asRecord(paramsRecord.tokenUsage ?? paramsRecord.token_usage ?? paramsRecord) ?? {}; events.push(scoped({ type: 'token_count', info })); return events; } if (method === 'model/safetyBuffering/updated') { const model = asString(paramsRecord.model); const showBufferingUi = asBoolean(paramsRecord.showBufferingUi ?? paramsRecord.show_buffering_ui); if (showBufferingUi === null || (showBufferingUi && !model)) { return events; } events.push(scoped({ type: 'model_safety_buffering', ...(model ? { model } : {}), use_cases: extractStringArray(paramsRecord.useCases ?? paramsRecord.use_cases), reasons: extractStringArray(paramsRecord.reasons), show_buffering_ui: showBufferingUi, faster_model: asString(paramsRecord.fasterModel ?? paramsRecord.faster_model) })); return events; } if (method === 'model/rerouted') { const fromModel = asString(paramsRecord.fromModel ?? paramsRecord.from_model); const toModel = asString(paramsRecord.toModel ?? paramsRecord.to_model); const reason = asString(paramsRecord.reason); if (fromModel && toModel && reason) { events.push(scoped({ type: 'model_rerouted', from_model: fromModel, to_model: toModel, reason })); } return events; } if (method === 'model/verification') { events.push(scoped({ type: 'model_verification', verifications: extractStringArray(paramsRecord.verifications) })); return events; } if (method === 'error') { const errorRecord = asRecord(paramsRecord.error); const retryable = asBoolean( paramsRecord.will_retry ?? paramsRecord.willRetry ?? errorRecord?.will_retry ?? errorRecord?.willRetry ); const willRetry = retryable ?? false; if (willRetry) return events; const message = asString(paramsRecord.message) ?? asString(errorRecord?.message); if (message) { const codexErrorInfo = extractCodexErrorInfo(paramsRecord, errorRecord); events.push(scoped({ type: 'task_failed', terminal_source: 'error', ...(retryable !== null ? { retryable } : {}), ...(codexErrorInfo ? { codex_error_info: codexErrorInfo } : {}), error: message })); } return events; } if (method === 'rawResponseItem/completed') { const item = asRecord(paramsRecord.item); if (!item) return events; const itemType = normalizeItemType(item.type); const callId = asString(item.call_id ?? item.callId); if (!itemType || !callId) return events; if (itemType === 'functioncall') { const toolName = normalizeCodexAgentToolName(item.name); const input = parseRawToolInput(item.arguments); if ( !toolName || !isRawMultiAgentV2Call(toolName, input, item.namespace) || this.rawAgentToolCallIds.has(callId) ) return events; this.rawAgentToolCallIds.add(callId); this.rawAgentToolNames.set(callId, toolName); events.push(scoped({ type: 'codex_tool_call_begin', call_id: callId, name: toolName, input })); return events; } if (itemType === 'functioncalloutput') { const toolName = this.rawAgentToolNames.get(callId); if (!toolName) return events; this.rawAgentToolNames.delete(callId); events.push(scoped({ type: 'codex_tool_call_end', call_id: callId, name: toolName, output: item.output, is_error: false })); } return events; } if (method === 'item/agentMessage/delta') { const itemId = extractItemId(paramsRecord); const delta = asString(paramsRecord.delta ?? paramsRecord.text ?? paramsRecord.message); if (itemId && delta) { const lastDelta = this.lastAgentMessageDeltaByItemId.get(itemId); if (lastDelta === delta) { return events; } this.lastAgentMessageDeltaByItemId.set(itemId, delta); const prev = this.agentMessageBuffers.get(itemId) ?? ''; this.agentMessageBuffers.set(itemId, prev + delta); events.push(scoped({ type: 'agent_message_delta' })); } return events; } if (method === 'item/reasoning/textDelta' || method === 'item/reasoning/summaryTextDelta') { const itemId = extractItemId(paramsRecord) ?? 'reasoning'; const delta = asString(paramsRecord.delta ?? paramsRecord.text ?? paramsRecord.message); if (delta) { const lastDelta = this.lastReasoningDeltaByItemId.get(itemId); if (lastDelta === delta) { return events; } this.lastReasoningDeltaByItemId.set(itemId, delta); const prev = this.reasoningBuffers.get(itemId) ?? ''; this.reasoningBuffers.set(itemId, prev + delta); events.push(scoped({ type: 'agent_reasoning_delta', delta })); } return events; } if (method === 'item/reasoning/summaryPartAdded') { const itemId = extractItemId(paramsRecord) ?? 'reasoning'; const summaryIndex = asNumber(paramsRecord.summaryIndex ?? paramsRecord.summary_index); if (summaryIndex !== null) { const key = `${itemId}:${summaryIndex}`; if (this.reasoningSectionBreakKeys.has(key)) { return events; } this.reasoningSectionBreakKeys.add(key); } events.push(scoped({ type: 'agent_reasoning_section_break' })); return events; } if (method === 'item/commandExecution/outputDelta') { const itemId = extractItemId(paramsRecord); const delta = asString(paramsRecord.delta ?? paramsRecord.text ?? paramsRecord.output ?? paramsRecord.stdout); if (itemId && delta) { const lastDelta = this.lastCommandOutputDeltaByItemId.get(itemId); if (lastDelta === delta) { return events; } this.lastCommandOutputDeltaByItemId.set(itemId, delta); const prev = this.commandOutputBuffers.get(itemId) ?? ''; this.commandOutputBuffers.set(itemId, prev + delta); } return events; } if (method === 'item/started' || method === 'item/completed') { const item = extractItem(paramsRecord); if (!item) return events; const itemType = normalizeItemType(item.type ?? item.itemType ?? item.kind); const itemId = extractItemId(paramsRecord) ?? asString(item.id ?? item.itemId ?? item.item_id); if (!itemType || !itemId) { return events; } if (itemType === 'contextcompaction') { if (method === 'item/completed') { const threadId = asString(eventScope.thread_id); const turnId = asString(eventScope.turn_id); if (threadId) { events.push({ type: 'thread_compacted', thread_id: threadId, ...(turnId ? { turn_id: turnId } : {}), await_turn_completion: true }); events.push(scoped({ type: 'context_compacted' })); } } return events; } if (itemType === 'agentmessage') { if (method === 'item/completed') { if (this.completedAgentMessageItems.has(itemId)) { return events; } const text = extractItemText(item) ?? this.agentMessageBuffers.get(itemId); if (text) { events.push(scoped({ type: 'agent_message', message: text })); this.completedAgentMessageItems.add(itemId); this.agentMessageBuffers.delete(itemId); } this.lastAgentMessageDeltaByItemId.delete(itemId); } return events; } if (itemType === 'reasoning') { if (method === 'item/completed') { if (this.completedReasoningItems.has(itemId)) { return events; } const text = extractReasoningText(item) ?? this.reasoningBuffers.get(itemId); if (text) { events.push(scoped({ type: 'agent_reasoning', text })); this.completedReasoningItems.add(itemId); this.reasoningBuffers.delete(itemId); } this.lastReasoningDeltaByItemId.delete(itemId); } return events; } if (itemType === 'commandexecution') { if (method === 'item/started') { const command = extractCommand(item.command ?? item.cmd ?? item.args); const cwd = asString(item.cwd ?? item.workingDirectory ?? item.working_directory); const autoApproved = asBoolean(item.autoApproved ?? item.auto_approved); const commandActions = Array.isArray(item.commandActions) ? item.commandActions : Array.isArray(item.command_actions) ? item.command_actions : null; const source = asString(item.source); const meta: Record = {}; if (command) meta.command = command; if (cwd) meta.cwd = cwd; if (autoApproved !== null) meta.auto_approved = autoApproved; if (commandActions) meta.command_actions = commandActions; if (source) meta.command_source = source; this.commandMeta.set(itemId, meta); events.push(scoped({ type: 'exec_command_begin', call_id: itemId, ...meta })); } if (method === 'item/completed') { const meta = this.commandMeta.get(itemId) ?? {}; const output = asString(item.aggregatedOutput ?? item.aggregated_output ?? item.output ?? item.result ?? item.stdout) ?? this.commandOutputBuffers.get(itemId); const stderr = asString(item.stderr); const error = asString(item.error); const exitCode = asNumber(item.exitCode ?? item.exit_code ?? item.exitcode); const durationMs = asNumber(item.durationMs ?? item.duration_ms); const status = asString(item.status); events.push(scoped({ type: 'exec_command_end', call_id: itemId, ...meta, ...(output ? { output } : {}), ...(stderr ? { stderr } : {}), ...(error ? { error } : {}), ...(exitCode !== null ? { exit_code: exitCode } : {}), ...(durationMs !== null ? { duration_ms: durationMs } : {}), ...(status ? { status } : {}) })); this.commandMeta.delete(itemId); this.commandOutputBuffers.delete(itemId); this.lastCommandOutputDeltaByItemId.delete(itemId); } return events; } if (itemType === 'mcptoolcall') { const server = asString(item.server ?? item.serverName ?? item.server_name); const tool = asString(item.tool ?? item.toolName ?? item.tool_name ?? item.name); const input = item.arguments ?? item.input ?? {}; if (method === 'item/started') { events.push(scoped({ type: 'mcp_tool_call_begin', call_id: itemId, server, tool, invocation: { server, tool, arguments: input } })); } if (method === 'item/completed') { const error = item.error; events.push(scoped({ type: 'mcp_tool_call_end', call_id: itemId, server, tool, result: error ? { Err: error } : item.result })); } return events; } if (itemType === 'imagegeneration') { if (method === 'item/completed') { const savedPath = extractGeneratedImagePath(item); if (!savedPath) { logger.debug('[AppServerEventConverter] imageGeneration missing savedPath', sanitizeUnhandledNotificationLogValue({ item })); return events; } events.push(scoped({ type: 'generated_image', image_id: itemId, saved_path: savedPath, file_name: extractGeneratedImageFileName(item, savedPath), ...(extractGeneratedImageMimeType(item) ? { mime_type: extractGeneratedImageMimeType(item) } : {}) })); } return events; } if (itemType === 'collabagenttoolcall') { if (this.rawAgentToolCallIds.has(itemId)) return events; const toolName = normalizeCollabAgentToolName(item.tool ?? item.name); if (!toolName) return events; if (method === 'item/started') { events.push(scoped({ type: 'codex_tool_call_begin', call_id: itemId, name: toolName, input: buildCollabAgentInput(item, toolName) })); } if (method === 'item/completed') { const status = asString(item.status); events.push(scoped({ type: 'codex_tool_call_end', call_id: itemId, name: toolName, output: buildCollabAgentOutput(item, toolName), is_error: status === 'failed' || status === 'error' })); } return events; } if (itemType === 'filechange') { if (method === 'item/started') { const changes = extractChanges(item.changes ?? item.change ?? item.diff); const autoApproved = asBoolean(item.autoApproved ?? item.auto_approved); const meta: Record = {}; if (changes) meta.changes = changes; if (autoApproved !== null) meta.auto_approved = autoApproved; this.fileChangeMeta.set(itemId, meta); events.push(scoped({ type: 'patch_apply_begin', call_id: itemId, ...meta })); } if (method === 'item/completed') { const meta = this.fileChangeMeta.get(itemId) ?? {}; const stdout = asString(item.stdout ?? item.output); const stderr = asString(item.stderr); const success = asBoolean(item.success ?? item.ok ?? item.applied ?? item.status === 'completed'); events.push(scoped({ type: 'patch_apply_end', call_id: itemId, ...meta, ...(stdout ? { stdout } : {}), ...(stderr ? { stderr } : {}), success: success ?? false })); this.fileChangeMeta.delete(itemId); } return events; } } logger.debug('[AppServerEventConverter] Unhandled notification', sanitizeUnhandledNotificationLogValue({ method, params })); return events; } reset(): void { this.agentMessageBuffers.clear(); this.reasoningBuffers.clear(); this.commandOutputBuffers.clear(); this.commandMeta.clear(); this.fileChangeMeta.clear(); this.completedAgentMessageItems.clear(); this.completedReasoningItems.clear(); this.reasoningSectionBreakKeys.clear(); this.lastAgentMessageDeltaByItemId.clear(); this.lastReasoningDeltaByItemId.clear(); this.lastCommandOutputDeltaByItemId.clear(); this.rawAgentToolCallIds.clear(); this.rawAgentToolNames.clear(); } }