Files
cc-web/lib/codex-app-runtime.js
2026-06-16 09:09:23 +08:00

687 lines
26 KiB
JavaScript

'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: 内容过长,已截断以保护服务稳定性]';
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 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 `<div class="agent-message-divider" data-divider-time="${time}"><span>${time}</span></div>`;
}
function hasAgentMessageBoundaryAtEnd(text) {
return /\n\s*(?:---|\*\*\*|___)\s*$/.test(text)
|| /(?:^|\n)\s*<div\s+class=["']agent-message-divider["'][\s\S]*?<\/div>\s*$/.test(text);
}
function hasAgentMessageBoundaryAtStart(text) {
return /^\s*(?:---|\*\*\*|___)\s*\n/.test(text)
|| /^\s*<div\s+class=["']agent-message-divider["'][\s\S]*?<\/div>\s*/.test(text);
}
function itemKind(item) {
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) {
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;
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;
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 '';
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) 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 '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 = params.turn?.error?.message || 'Codex App 任务失败';
}
return { done: true };
}
case 'error':
case 'warning':
case 'guardianWarning':
case 'configWarning':
case 'deprecationNotice': {
const message = 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: false };
}
default:
return { done: false };
}
}
return {
processCodexAppNotification,
updateUsage,
};
}
module.exports = { createCodexAppRuntime };