'use strict'; const CODEX_APP_ONCE_NOTICE_PATTERNS = [ /^Under-development features enabled:/i, /^Heads up: Long threads and multiple compactions/i, ]; function readPositiveIntEnv(name, fallback, options = {}) { const raw = Number.parseInt(String(process.env[name] || ''), 10); const min = Number.isFinite(options.min) ? options.min : 1; const max = Number.isFinite(options.max) ? options.max : Number.MAX_SAFE_INTEGER; if (!Number.isFinite(raw) || raw <= 0) return fallback; return Math.max(min, Math.min(max, raw)); } const RUNTIME_FULL_TEXT_MAX_CHARS = readPositiveIntEnv('CC_WEB_CODEX_APP_RUNTIME_FULL_TEXT_MAX_CHARS', 256 * 1024, { min: 4096 }); const RUNTIME_AGENT_ITEM_MAX_CHARS = readPositiveIntEnv('CC_WEB_CODEX_APP_RUNTIME_AGENT_ITEM_MAX_CHARS', 128 * 1024, { min: 4096 }); const RUNTIME_TOOL_DELTA_MAX_CHARS = readPositiveIntEnv('CC_WEB_CODEX_APP_RUNTIME_TOOL_DELTA_MAX_CHARS', 64 * 1024, { min: 1024 }); const RUNTIME_TOOL_RESULT_MAX_CHARS = readPositiveIntEnv('CC_WEB_CODEX_APP_RUNTIME_TOOL_RESULT_MAX_CHARS', 32 * 1024, { min: 1024 }); const RUNTIME_TOOL_INPUT_MAX_CHARS = readPositiveIntEnv('CC_WEB_CODEX_APP_RUNTIME_TOOL_INPUT_MAX_CHARS', 16 * 1024, { min: 1024 }); const RUNTIME_STREAM_DELTA_MAX_CHARS = readPositiveIntEnv('CC_WEB_CODEX_APP_STREAM_DELTA_MAX_CHARS', 16 * 1024, { min: 1024 }); const RUNTIME_MAX_TOOL_CALLS = readPositiveIntEnv('CC_WEB_CODEX_APP_RUNTIME_MAX_TOOL_CALLS', 120, { min: 1, max: 1000 }); const RUNTIME_TRUNCATED_HEAD = '[cc-web: 前文过长,已保留尾部以保护服务稳定性]\n'; const RUNTIME_TRUNCATED_TAIL = '\n[cc-web: 内容过长,已截断以保护服务稳定性]'; const CODEX_APP_PLAN_ITEM_TYPES = new Set(['plan', 'plan_list', 'planlist', 'todo', 'todo_list', 'todolist', 'task_list']); const CODEX_APP_PLAN_TOOL_NAMES = new Set(['update_plan', 'plan', 'plan_list', 'todo_list', 'updateplan', 'todolist']); function createCodexAppRuntime(deps = {}) { const { wsSend, loadSession, saveSession, truncateObj, } = deps; function limitPreviewValue(value, options = {}, depth = 0, seen = new WeakSet()) { const maxString = options.maxString || RUNTIME_TOOL_RESULT_MAX_CHARS; const maxDepth = options.maxDepth || 4; const maxArray = options.maxArray || 50; const maxKeys = options.maxKeys || 60; if (value === null || value === undefined) return value; if (typeof value === 'string') return truncateEnd(value, maxString); if (typeof value === 'number' || typeof value === 'boolean') return value; if (typeof value === 'bigint') return String(value); if (typeof value === 'function' || typeof value === 'symbol') return undefined; if (Buffer.isBuffer(value)) return `[Buffer ${value.length} bytes]`; if (depth >= maxDepth) return '[Object truncated]'; if (typeof value !== 'object') return String(value); if (seen.has(value)) return '[Circular]'; seen.add(value); if (Array.isArray(value)) { const output = []; const limit = Math.min(value.length, maxArray); for (let index = 0; index < limit; index += 1) { output.push(limitPreviewValue(value[index], options, depth + 1, seen)); } if (value.length > limit) output.push({ __truncated: `omitted ${value.length - limit} items` }); seen.delete(value); return output; } const output = {}; const keys = Object.keys(value); const limit = Math.min(keys.length, maxKeys); for (let index = 0; index < limit; index += 1) { const key = keys[index]; const next = limitPreviewValue(value[key], options, depth + 1, seen); if (next !== undefined) output[key] = next; } if (keys.length > limit) output.__truncated = `omitted ${keys.length - limit} fields`; seen.delete(value); return output; } function safeStringifyPreview(value, maxLen = RUNTIME_TOOL_RESULT_MAX_CHARS, options = {}) { if (typeof value === 'string') return truncateEnd(value, maxLen); try { const limited = limitPreviewValue(value, { maxString: Math.min(maxLen, options.maxString || maxLen), maxDepth: options.maxDepth || 4, maxArray: options.maxArray || 50, maxKeys: options.maxKeys || 60, }); return truncateEnd(JSON.stringify(limited, null, 2), maxLen); } catch { return truncateEnd(String(value), maxLen); } } function truncateEnd(value, maxLen) { const text = String(value || ''); if (!Number.isFinite(maxLen) || maxLen <= 0 || text.length <= maxLen) return text; const keep = Math.max(0, maxLen - RUNTIME_TRUNCATED_TAIL.length); return `${text.slice(0, keep)}${RUNTIME_TRUNCATED_TAIL}`; } function keepTail(value, maxLen) { const text = String(value || ''); if (!Number.isFinite(maxLen) || maxLen <= 0 || text.length <= maxLen) return text; const keep = Math.max(0, maxLen - RUNTIME_TRUNCATED_HEAD.length); return `${RUNTIME_TRUNCATED_HEAD}${text.slice(-keep)}`; } function appendCappedText(current, addition, maxLen) { return keepTail(`${String(current || '')}${String(addition || '')}`, maxLen); } function capStreamDelta(text) { return truncateEnd(text, RUNTIME_STREAM_DELTA_MAX_CHARS); } function truncate(value, maxLen) { if (typeof value === 'string') return truncateEnd(value, maxLen); return safeStringifyPreview(value, maxLen, { maxString: maxLen }); } const shownOnceNoticeKeys = new Set(); function normalizeNoticeMessage(message) { return String(message || '').trim().replace(/\s+/g, ' '); } function shouldShowRuntimeNotice(method, message) { const normalized = normalizeNoticeMessage(message); const isOnceNotice = CODEX_APP_ONCE_NOTICE_PATTERNS.some((pattern) => pattern.test(normalized)); if (!isOnceNotice) return true; const key = `${method}:${normalized}`; if (shownOnceNoticeKeys.has(key)) return false; shownOnceNoticeKeys.add(key); return true; } function normalizeIdentifier(value) { return String(value || '') .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, '_') .replace(/^_+|_+$/g, ''); } function isPlanToolName(value) { const name = normalizeIdentifier(value); return CODEX_APP_PLAN_TOOL_NAMES.has(name) || name.endsWith('_update_plan'); } function isPlanLikeItem(item) { if (!item || typeof item !== 'object') return false; if (CODEX_APP_PLAN_ITEM_TYPES.has(normalizeIdentifier(item.type))) return true; return isPlanToolName(item.tool || item.name || item.functionName || item.function?.name); } function parseMaybeJsonValue(value) { if (typeof value !== 'string') return value; const trimmed = value.trim(); if (!trimmed || !/^[{[]/.test(trimmed)) return value; try { return JSON.parse(trimmed); } catch { return value; } } function extractPlanEntries(value, depth = 0) { if (value === null || value === undefined || depth > 3) return null; const source = parseMaybeJsonValue(value); if (Array.isArray(source)) return source; if (!source || typeof source !== 'object') return null; const keys = ['plan', 'items', 'todos', 'tasks', 'steps']; for (const key of keys) { if (Array.isArray(source[key])) return source[key]; } const nestedKeys = ['arguments', 'input', 'params', 'payload', 'structuredContent', 'result']; for (const key of nestedKeys) { const nested = extractPlanEntries(source[key], depth + 1); if (nested) return nested; } if (Array.isArray(source.contentItems)) { for (const part of source.contentItems) { const text = typeof part?.text === 'string' ? part.text : ''; const nested = extractPlanEntries(text, depth + 1); if (nested) return nested; } } return null; } function planEntryCompleted(entry) { if (!entry || typeof entry !== 'object') return false; if (entry.completed === true || entry.done === true) return true; const status = normalizeIdentifier(entry.status || entry.state); return ['completed', 'complete', 'done', 'success', 'succeeded'].includes(status); } function planEntryText(entry) { if (typeof entry === 'string') return entry; if (!entry || typeof entry !== 'object') return ''; return entry.step || entry.text || entry.title || entry.name || entry.description || entry.task || entry.item || entry.content || ''; } function normalizeTodoListFromPlanItem(item) { if (!isPlanLikeItem(item)) return null; const candidates = [ item.arguments, item.input, item.params, item.payload, item.structuredContent, item.result?.structuredContent, item.result, item, ]; let entries = null; for (const candidate of candidates) { entries = extractPlanEntries(candidate); if (entries) break; } if (!Array.isArray(entries)) return null; const items = entries .map((entry) => { const text = truncateEnd(planEntryText(entry), RUNTIME_TOOL_INPUT_MAX_CHARS); if (!text) return null; return { text, completed: planEntryCompleted(entry), }; }) .filter(Boolean); return { id: item.id || item.itemId || item.planId || 'codex-app-plan', type: 'todo_list', items, }; } function planUpdateItemFromParams(params = {}) { const item = params.item && typeof params.item === 'object' ? { ...params.item } : {}; return { ...params, ...item, id: item.id || params.itemId || params.id || params.planId || 'codex-app-plan', type: item.type || params.type || 'planList', status: item.status || params.status || 'inProgress', plan: item.plan || params.plan, items: item.items || params.items, todos: item.todos || params.todos, tasks: item.tasks || params.tasks, }; } function codexAppErrorMessage(value) { if (!value) return ''; if (typeof value === 'string') return value; if (typeof value !== 'object') return String(value); const parts = []; const directMessage = value.message || value.title || value.detail || value.reason; if (directMessage) parts.push(String(directMessage)); const error = value.error && typeof value.error === 'object' ? value.error : null; if (error) { if (error.message) parts.push(String(error.message)); if (error.code) parts.push(String(error.code)); if (error.type) parts.push(String(error.type)); } if (value.code) parts.push(String(value.code)); if (value.type) parts.push(String(value.type)); if (parts.length > 0) return [...new Set(parts)].join(' '); return safeStringifyPreview(value, 2000, { maxDepth: 3, maxArray: 10, maxKeys: 20 }); } function sendRuntime(entry, sessionId, payload) { wsSend(entry.ws, { ...payload, sessionId }); } function formatAgentMessageDividerTime(date = new Date()) { const pad = (value) => String(value).padStart(2, '0'); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; } function createAgentMessageDivider() { const time = formatAgentMessageDividerTime(); return `
${time}
`; } function hasAgentMessageBoundaryAtEnd(text) { return /\n\s*(?:---|\*\*\*|___)\s*$/.test(text) || /(?:^|\n)\s*\s*$/.test(text); } function hasAgentMessageBoundaryAtStart(text) { return /^\s*(?:---|\*\*\*|___)\s*\n/.test(text) || /^\s*\s*/.test(text); } function itemKind(item) { if (normalizeTodoListFromPlanItem(item)) return 'todo_list'; switch (item?.type) { case 'commandExecution': return 'command_execution'; case 'mcpToolCall': return 'mcp_tool_call'; case 'fileChange': return 'file_change'; case 'reasoning': return 'reasoning'; case 'dynamicToolCall': return 'dynamic_tool_call'; case 'collabAgentToolCall': return 'collab_agent_tool_call'; case 'webSearch': return 'web_search'; case 'imageView': return 'image_view'; case 'imageGeneration': return 'image_generation'; default: return item?.type || 'codex_app_item'; } } function itemName(item) { if (normalizeTodoListFromPlanItem(item)) return 'PlanList'; switch (item?.type) { case 'commandExecution': return 'CommandExecution'; case 'mcpToolCall': return 'McpToolCall'; case 'fileChange': return 'FileChange'; case 'reasoning': return 'Reasoning'; case 'dynamicToolCall': return item.tool || 'DynamicToolCall'; case 'collabAgentToolCall': return item.tool || 'CollabAgentToolCall'; case 'webSearch': return 'WebSearch'; case 'imageGeneration': return 'ImageGeneration'; default: return item?.type || 'CodexAppItem'; } } function itemInput(item) { if (!item) return null; const todoList = normalizeTodoListFromPlanItem(item); if (todoList) return todoList; switch (item.type) { case 'commandExecution': return { command: truncateEnd(item.command || '', RUNTIME_TOOL_INPUT_MAX_CHARS) }; case 'mcpToolCall': return { server: truncateEnd(item.server || '', 256), tool: truncateEnd(item.tool || '', 256), arguments: limitPreviewValue(item.arguments ?? null, { maxString: RUNTIME_TOOL_INPUT_MAX_CHARS, maxDepth: 5, maxArray: 80, maxKeys: 80, }), }; case 'fileChange': return { changes: limitPreviewValue(item.changes || [], { maxString: RUNTIME_TOOL_INPUT_MAX_CHARS, maxDepth: 5 }) }; case 'reasoning': return { content: limitPreviewValue(item.content || [], { maxString: RUNTIME_TOOL_INPUT_MAX_CHARS, maxDepth: 4 }), summary: limitPreviewValue(item.summary || [], { maxString: RUNTIME_TOOL_INPUT_MAX_CHARS, maxDepth: 4 }), }; case 'dynamicToolCall': return { tool: truncateEnd(item.tool || '', 256), namespace: truncateEnd(item.namespace || '', 256) || null, arguments: limitPreviewValue(item.arguments ?? null, { maxString: RUNTIME_TOOL_INPUT_MAX_CHARS, maxDepth: 5 }), }; case 'collabAgentToolCall': return { tool: truncateEnd(item.tool || '', 256), prompt: truncateEnd(item.prompt || '', RUNTIME_TOOL_INPUT_MAX_CHARS) || null, receiverThreadIds: limitPreviewValue(item.receiverThreadIds || [], { maxString: 512, maxDepth: 3 }), agentsStates: limitPreviewValue(item.agentsStates || {}, { maxString: RUNTIME_TOOL_INPUT_MAX_CHARS, maxDepth: 5 }), }; case 'imageGeneration': return { prompt: truncateEnd(item.prompt || item.query || '', RUNTIME_TOOL_INPUT_MAX_CHARS), size: item.size || null, quality: item.quality || null, }; default: return limitPreviewValue(item, { maxString: Math.min(500, RUNTIME_TOOL_INPUT_MAX_CHARS), maxDepth: 4, maxArray: 30, maxKeys: 40, }); } } function reasoningTextFromItem(item) { const parts = [...(item?.summary || []), ...(item?.content || [])]; return parts.map((part) => { if (typeof part === 'string') return part; if (typeof part?.text === 'string') return part.text; return ''; }).filter(Boolean).join('\n\n'); } function hasReasoningContent(item) { return Boolean(reasoningTextFromItem(item).trim()); } function itemMeta(item) { if (!item) return null; if (normalizeTodoListFromPlanItem(item)) { return { kind: 'todo_list', title: 'Plan List', subtitle: item.explanation || item.title || item.tool || '', status: item.status || null, }; } switch (item.type) { case 'commandExecution': return { kind: 'command_execution', title: 'Shell Command', subtitle: item.command || '', exitCode: typeof item.exitCode === 'number' ? item.exitCode : null, status: item.status || null, }; case 'mcpToolCall': return { kind: 'mcp_tool_call', title: 'MCP Tool', subtitle: [item.server, item.tool].filter(Boolean).join('.'), status: item.status || null, }; case 'fileChange': return { kind: 'file_change', title: 'File Change', subtitle: (item.changes || []).map((change) => change.path).filter(Boolean).join(', '), status: item.status || null, }; case 'reasoning': return { kind: 'reasoning', title: 'Reasoning', subtitle: reasoningTextFromItem(item).replace(/\s+/g, ' ').slice(0, 120), status: item.status || null, }; default: return { kind: itemKind(item), title: itemName(item), subtitle: item.tool || item.query || '', status: item.status || null, }; } } function stringifyMcpResult(result) { if (!result) return ''; if (Array.isArray(result.content)) { const text = result.content.map((part) => { if (typeof part?.text === 'string') return truncateEnd(part.text, RUNTIME_TOOL_RESULT_MAX_CHARS); return safeStringifyPreview(part, RUNTIME_TOOL_RESULT_MAX_CHARS); }).filter(Boolean).join('\n'); if (text) return truncateEnd(text, RUNTIME_TOOL_RESULT_MAX_CHARS); } return safeStringifyPreview(result, RUNTIME_TOOL_RESULT_MAX_CHARS); } function itemResult(item) { if (!item) return ''; const todoList = normalizeTodoListFromPlanItem(item); if (todoList) return JSON.stringify(todoList, null, 2); switch (item.type) { case 'commandExecution': return truncateEnd(item.aggregatedOutput || '', RUNTIME_TOOL_RESULT_MAX_CHARS); case 'mcpToolCall': return item.error?.message || stringifyMcpResult(item.result); case 'fileChange': return safeStringifyPreview(item.changes || [], RUNTIME_TOOL_RESULT_MAX_CHARS); case 'reasoning': return truncateEnd(reasoningTextFromItem(item), RUNTIME_TOOL_RESULT_MAX_CHARS); case 'dynamicToolCall': return safeStringifyPreview({ success: item.success ?? null, contentItems: item.contentItems || null, }, RUNTIME_TOOL_RESULT_MAX_CHARS); case 'collabAgentToolCall': return safeStringifyPreview({ status: item.status || null, receiverThreadIds: item.receiverThreadIds || [], agentsStates: item.agentsStates || {}, }, RUNTIME_TOOL_RESULT_MAX_CHARS); case 'imageGeneration': return safeStringifyPreview({ status: item.status || null, images: Array.isArray(item.images) ? item.images.map((image) => ({ path: image.path || image.filePath || null, mime: image.mime || image.mimeType || null, size: image.size || null, })) : null, outputPath: item.outputPath || item.path || null, }, RUNTIME_TOOL_RESULT_MAX_CHARS); default: if (typeof item.text === 'string') return truncateEnd(item.text, RUNTIME_TOOL_RESULT_MAX_CHARS); return safeStringifyPreview(item, Math.min(1200, RUNTIME_TOOL_RESULT_MAX_CHARS)); } } function ensureToolCall(entry, item, sessionId) { if (!item?.id) return null; const kind = itemKind(item); let toolCall = entry.toolCalls.find((tool) => tool.id === item.id); if (toolCall) { toolCall.name = itemName(item); toolCall.kind = kind; toolCall.meta = itemMeta(item) || toolCall.meta || null; if (toolCall.input == null || kind === 'todo_list') toolCall.input = itemInput(item); return toolCall; } if (entry.toolCalls.length >= RUNTIME_MAX_TOOL_CALLS) { let overflowTool = entry.toolCalls.find((tool) => tool.id === 'ccweb-toolcalls-overflow'); if (!overflowTool) { overflowTool = { name: 'cc-web', id: 'ccweb-toolcalls-overflow', kind: 'system', meta: { kind: 'system', title: 'Tool Calls', subtitle: 'too many tool calls', status: 'inProgress' }, input: null, done: false, result: '工具调用数量过多,后续工具调用已折叠显示。', }; entry.toolCalls.push(overflowTool); sendRuntime(entry, sessionId, { type: 'tool_start', name: overflowTool.name, toolUseId: overflowTool.id, input: overflowTool.input, kind: overflowTool.kind, meta: overflowTool.meta, }); } return overflowTool; } toolCall = { name: itemName(item), id: item.id, kind, meta: itemMeta(item), input: itemInput(item), done: false, }; entry.toolCalls.push(toolCall); sendRuntime(entry, sessionId, { type: 'tool_start', name: toolCall.name, toolUseId: toolCall.id, input: toolCall.input, kind: toolCall.kind, meta: toolCall.meta, }); return toolCall; } function appendAgentText(entry, itemId, text) { const nextText = String(text || ''); if (!nextText) return ''; if (!entry.agentMessageItems) entry.agentMessageItems = new Map(); const currentItemText = entry.agentMessageItems.get(itemId) || ''; const separator = agentMessageSeparator(entry, itemId, nextText); const appended = separator + nextText; entry.agentMessageItems.set(itemId, appendCappedText(currentItemText, nextText, RUNTIME_AGENT_ITEM_MAX_CHARS)); entry.fullText = appendCappedText(entry.fullText || '', appended, RUNTIME_FULL_TEXT_MAX_CHARS); return capStreamDelta(appended); } function appendAgentCompletedText(entry, item) { const text = String(item?.text || ''); if (!text) return ''; if (!entry.agentMessageItems) entry.agentMessageItems = new Map(); const currentItemText = entry.agentMessageItems.get(item.id) || ''; if (currentItemText && text.startsWith(currentItemText)) { const remainder = text.slice(currentItemText.length); entry.agentMessageItems.set(item.id, keepTail(text, RUNTIME_AGENT_ITEM_MAX_CHARS)); entry.fullText = appendCappedText(entry.fullText || '', remainder, RUNTIME_FULL_TEXT_MAX_CHARS); return capStreamDelta(remainder); } if (currentItemText === text) return ''; const separator = agentMessageSeparator(entry, item.id, text); const appended = separator + text; entry.agentMessageItems.set(item.id, keepTail(text, RUNTIME_AGENT_ITEM_MAX_CHARS)); entry.fullText = appendCappedText(entry.fullText || '', appended, RUNTIME_FULL_TEXT_MAX_CHARS); return capStreamDelta(appended); } function agentMessageSeparator(entry, itemId, nextText) { if (entry.agentMessageItems?.get(itemId)) return ''; const currentText = entry.fullText || ''; if (!/\S/.test(currentText)) return ''; const hasVisualBoundary = hasAgentMessageBoundaryAtEnd(currentText) || hasAgentMessageBoundaryAtStart(String(nextText || '')); return hasVisualBoundary ? '' : `\n\n${createAgentMessageDivider()}\n\n`; } function updateToolResult(entry, sessionId, itemId, result, done = false, patch = {}) { if (!itemId) return; let toolCall = entry.toolCalls.find((tool) => tool.id === itemId); if (!toolCall) { const targetItemId = entry.toolCalls.length >= RUNTIME_MAX_TOOL_CALLS ? 'ccweb-toolcalls-overflow' : itemId; toolCall = entry.toolCalls.find((tool) => tool.id === targetItemId); itemId = targetItemId; } if (!toolCall) { toolCall = { name: patch.name || 'CodexAppItem', id: itemId, kind: patch.kind || 'codex_app_item', meta: patch.meta || null, input: patch.input || null, done: false, }; entry.toolCalls.push(toolCall); sendRuntime(entry, sessionId, { type: 'tool_start', name: toolCall.name, toolUseId: toolCall.id, input: toolCall.input, kind: toolCall.kind, meta: toolCall.meta, }); } if (patch.name) toolCall.name = patch.name; if (patch.kind) toolCall.kind = patch.kind; if (patch.meta) toolCall.meta = patch.meta; if (patch.input !== undefined) { toolCall.input = limitPreviewValue(patch.input, { maxString: RUNTIME_TOOL_INPUT_MAX_CHARS, maxDepth: 5, maxArray: 80, maxKeys: 80, }); } const safeResult = truncateEnd(result, RUNTIME_TOOL_RESULT_MAX_CHARS); toolCall.done = done; toolCall.result = safeResult; sendRuntime(entry, sessionId, { type: done ? 'tool_end' : 'tool_update', toolUseId: itemId, name: toolCall.name, input: toolCall.input, result: safeResult, kind: toolCall.kind, meta: toolCall.meta, }); } function updateUsage(sessionId, entry, usage) { const total = usage?.total || usage || null; if (!total) return; const session = loadSession(sessionId); if (!session) return; session.totalUsage = { inputTokens: total.inputTokens || 0, cachedInputTokens: total.cachedInputTokens || 0, outputTokens: total.outputTokens || 0, }; entry.lastUsage = session.totalUsage; saveSession(session); sendRuntime(entry, sessionId, { type: 'usage', totalUsage: session.totalUsage }); } function processCodexAppNotification(entry, notification, sessionId) { if (!entry || !notification?.method) return { done: false }; const method = notification.method; const params = notification.params || {}; if (params.threadId && !entry.threadId) entry.threadId = params.threadId; if (params.turnId && !entry.turnId) entry.turnId = params.turnId; switch (method) { case 'turn/started': if (params.turn?.id) entry.turnId = params.turn.id; return { done: false }; case 'item/started': { const item = params.item; if (!item || !item.id || item.type === 'agentMessage' || item.type === 'userMessage') return { done: false }; if (item.type === 'reasoning' && !hasReasoningContent(item)) return { done: false }; ensureToolCall(entry, item, sessionId); return { done: false }; } case 'item/agentMessage/delta': { const chunk = appendAgentText(entry, params.itemId || 'agent-message', params.delta || ''); if (chunk) sendRuntime(entry, sessionId, { type: 'text_delta', text: chunk }); return { done: false }; } case 'item/commandExecution/outputDelta': { const itemId = params.itemId; const current = entry.toolOutputDeltas?.get(itemId) || ''; const next = appendCappedText(current, params.delta || '', RUNTIME_TOOL_DELTA_MAX_CHARS); if (!entry.toolOutputDeltas) entry.toolOutputDeltas = new Map(); entry.toolOutputDeltas.set(itemId, next); updateToolResult(entry, sessionId, itemId, next, false, { name: 'CommandExecution', kind: 'command_execution', }); return { done: false }; } case 'item/fileChange/patchUpdated': { updateToolResult(entry, sessionId, params.itemId, JSON.stringify(params.changes || [], null, 2), false, { name: 'FileChange', kind: 'file_change', input: { changes: params.changes || [] }, meta: { kind: 'file_change', title: 'File Change', subtitle: (params.changes || []).map((change) => change.path).filter(Boolean).join(', '), status: 'inProgress', }, }); return { done: false }; } case 'item/mcpToolCall/progress': { updateToolResult(entry, sessionId, params.itemId, params.message || '', false, { name: 'McpToolCall', kind: 'mcp_tool_call', }); return { done: false }; } case 'plan/updated': case 'turn/plan/updated': case 'item/plan/updated': case 'item/todoList/updated': { const item = planUpdateItemFromParams(params); const todoList = normalizeTodoListFromPlanItem(item); if (!todoList) return { done: false }; updateToolResult(entry, sessionId, todoList.id, JSON.stringify(todoList, null, 2), false, { name: 'PlanList', kind: 'todo_list', input: todoList, meta: itemMeta(item), }); return { done: false }; } case 'item/reasoning/summaryTextDelta': case 'item/reasoning/textDelta': { const itemId = params.itemId; const delta = String(params.delta || ''); if (!itemId || !delta) return { done: false }; const current = entry.toolOutputDeltas?.get(itemId) || ''; const next = appendCappedText(current, delta, RUNTIME_TOOL_DELTA_MAX_CHARS); if (!entry.toolOutputDeltas) entry.toolOutputDeltas = new Map(); entry.toolOutputDeltas.set(itemId, next); updateToolResult(entry, sessionId, itemId, next, false, { name: 'Reasoning', kind: 'reasoning', }); return { done: false }; } case 'item/completed': { const item = params.item; if (!item || !item.id) return { done: false }; if (item.type === 'agentMessage') { const chunk = appendAgentCompletedText(entry, item); if (chunk) sendRuntime(entry, sessionId, { type: 'text_delta', text: chunk }); return { done: false }; } if (item.type === 'userMessage') return { done: false }; if (item.type === 'reasoning') { const result = truncateEnd(itemResult(item) || entry.toolOutputDeltas?.get(item.id) || '', RUNTIME_TOOL_RESULT_MAX_CHARS); if (!result.trim()) return { done: false }; const toolCall = ensureToolCall(entry, item, sessionId); if (!toolCall) return { done: false }; toolCall.done = true; toolCall.result = result; toolCall.meta = itemMeta(item) || toolCall.meta; sendRuntime(entry, sessionId, { type: 'tool_end', toolUseId: toolCall.id, result, kind: toolCall.kind, meta: toolCall.meta, }); return { done: false }; } const toolCall = ensureToolCall(entry, item, sessionId); if (!toolCall) return { done: false }; const result = truncateEnd(itemResult(item), RUNTIME_TOOL_RESULT_MAX_CHARS); toolCall.done = true; toolCall.result = result; toolCall.meta = itemMeta(item) || toolCall.meta; sendRuntime(entry, sessionId, { type: 'tool_end', toolUseId: toolCall.id, result, kind: toolCall.kind, meta: toolCall.meta, }); return { done: false }; } case 'thread/tokenUsage/updated': updateUsage(sessionId, entry, params.tokenUsage); return { done: false }; case 'turn/completed': { if (params.turn?.id) entry.turnId = params.turn.id; entry.turnStatus = params.turn?.status || 'completed'; if (params.turn?.status === 'failed') { entry.lastError = codexAppErrorMessage(params.turn?.error) || 'Codex App 任务失败'; } return { done: true }; } case 'error': case 'warning': case 'guardianWarning': case 'configWarning': case 'deprecationNotice': { const message = method === 'error' ? codexAppErrorMessage(params) : (params.message || params.title || ''); if (message) { if (method === 'error') entry.lastError = message; if (method === 'error' || shouldShowRuntimeNotice(method, message)) { sendRuntime(entry, sessionId, { type: 'system_message', message }); } } return { done: method === 'error' }; } default: return { done: false }; } } return { processCodexAppNotification, updateUsage, }; } module.exports = { createCodexAppRuntime };