Stabilize ccweb codex app runtime

This commit is contained in:
shiyue
2026-06-16 09:09:23 +08:00
parent 0f4a1c27fe
commit 2e119fd7e3
6 changed files with 1361 additions and 124 deletions

View File

@@ -5,6 +5,24 @@ const CODEX_APP_ONCE_NOTICE_PATTERNS = [
/^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,
@@ -13,10 +31,87 @@ function createCodexAppRuntime(deps = {}) {
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 truncateObj === 'function') return truncateObj(value, maxLen);
const text = typeof value === 'string' ? value : JSON.stringify(value);
return text.length > maxLen ? `${text.slice(0, maxLen)}...` : value;
if (typeof value === 'string') return truncateEnd(value, maxLen);
return safeStringifyPreview(value, maxLen, { maxString: maxLen });
}
const shownOnceNoticeKeys = new Set();
@@ -112,28 +207,51 @@ function createCodexAppRuntime(deps = {}) {
if (!item) return null;
switch (item.type) {
case 'commandExecution':
return { command: item.command || '' };
return { command: truncateEnd(item.command || '', RUNTIME_TOOL_INPUT_MAX_CHARS) };
case 'mcpToolCall':
return {
server: item.server || '',
tool: item.tool || '',
arguments: item.arguments ?? null,
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: item.changes || [] };
return { changes: limitPreviewValue(item.changes || [], { maxString: RUNTIME_TOOL_INPUT_MAX_CHARS, maxDepth: 5 }) };
case 'reasoning':
return { content: item.content || [], summary: item.summary || [] };
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: item.tool || '', namespace: item.namespace || null, arguments: item.arguments ?? null };
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: item.tool || '',
prompt: item.prompt || null,
receiverThreadIds: item.receiverThreadIds || [],
agentsStates: item.agentsStates || {},
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 truncate(item, 500);
return limitPreviewValue(item, {
maxString: Math.min(500, RUNTIME_TOOL_INPUT_MAX_CHARS),
maxDepth: 4,
maxArray: 30,
maxKeys: 40,
});
}
}
@@ -196,47 +314,49 @@ function createCodexAppRuntime(deps = {}) {
if (!result) return '';
if (Array.isArray(result.content)) {
const text = result.content.map((part) => {
if (typeof part?.text === 'string') return part.text;
try {
return JSON.stringify(part);
} catch {
return String(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 text;
}
try {
return JSON.stringify(result, null, 2);
} catch {
return String(result);
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 item.aggregatedOutput || '';
return truncateEnd(item.aggregatedOutput || '', RUNTIME_TOOL_RESULT_MAX_CHARS);
case 'mcpToolCall':
return item.error?.message || stringifyMcpResult(item.result);
case 'fileChange':
return JSON.stringify(item.changes || [], null, 2);
return safeStringifyPreview(item.changes || [], RUNTIME_TOOL_RESULT_MAX_CHARS);
case 'reasoning':
return reasoningTextFromItem(item);
return truncateEnd(reasoningTextFromItem(item), RUNTIME_TOOL_RESULT_MAX_CHARS);
case 'dynamicToolCall':
return JSON.stringify({
return safeStringifyPreview({
success: item.success ?? null,
contentItems: item.contentItems || null,
}, null, 2);
}, RUNTIME_TOOL_RESULT_MAX_CHARS);
case 'collabAgentToolCall':
return JSON.stringify({
return safeStringifyPreview({
status: item.status || null,
receiverThreadIds: item.receiverThreadIds || [],
agentsStates: item.agentsStates || {},
}, null, 2);
}, 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 item.text;
return JSON.stringify(truncate(item, 1200));
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));
}
}
@@ -252,6 +372,31 @@ function createCodexAppRuntime(deps = {}) {
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,
@@ -278,9 +423,10 @@ function createCodexAppRuntime(deps = {}) {
if (!entry.agentMessageItems) entry.agentMessageItems = new Map();
const currentItemText = entry.agentMessageItems.get(itemId) || '';
const separator = agentMessageSeparator(entry, itemId, nextText);
entry.agentMessageItems.set(itemId, currentItemText + nextText);
entry.fullText = (entry.fullText || '') + separator + nextText;
return separator + 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) {
@@ -290,15 +436,16 @@ function createCodexAppRuntime(deps = {}) {
const currentItemText = entry.agentMessageItems.get(item.id) || '';
if (currentItemText && text.startsWith(currentItemText)) {
const remainder = text.slice(currentItemText.length);
entry.agentMessageItems.set(item.id, text);
entry.fullText = (entry.fullText || '') + remainder;
return remainder;
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);
entry.agentMessageItems.set(item.id, text);
entry.fullText = (entry.fullText || '') + separator + text;
return separator + 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) {
@@ -312,6 +459,11 @@ function createCodexAppRuntime(deps = {}) {
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',
@@ -334,15 +486,23 @@ function createCodexAppRuntime(deps = {}) {
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 = patch.input;
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 = result;
toolCall.result = safeResult;
sendRuntime(entry, sessionId, {
type: done ? 'tool_end' : 'tool_update',
toolUseId: itemId,
name: toolCall.name,
input: toolCall.input,
result,
result: safeResult,
kind: toolCall.kind,
meta: toolCall.meta,
});
@@ -393,7 +553,7 @@ function createCodexAppRuntime(deps = {}) {
case 'item/commandExecution/outputDelta': {
const itemId = params.itemId;
const current = entry.toolOutputDeltas?.get(itemId) || '';
const next = current + String(params.delta || '');
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, {
@@ -432,7 +592,7 @@ function createCodexAppRuntime(deps = {}) {
const delta = String(params.delta || '');
if (!itemId || !delta) return { done: false };
const current = entry.toolOutputDeltas?.get(itemId) || '';
const next = current + delta;
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, {
@@ -452,7 +612,7 @@ function createCodexAppRuntime(deps = {}) {
}
if (item.type === 'userMessage') return { done: false };
if (item.type === 'reasoning') {
const result = (itemResult(item) || entry.toolOutputDeltas?.get(item.id) || '').slice(0, 4000);
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 };
@@ -470,7 +630,7 @@ function createCodexAppRuntime(deps = {}) {
}
const toolCall = ensureToolCall(entry, item, sessionId);
if (!toolCall) return { done: false };
const result = itemResult(item).slice(0, 4000);
const result = truncateEnd(itemResult(item), RUNTIME_TOOL_RESULT_MAX_CHARS);
toolCall.done = true;
toolCall.result = result;
toolCall.meta = itemMeta(item) || toolCall.meta;

View File

@@ -0,0 +1,206 @@
'use strict';
const path = require('path');
const { fork } = require('child_process');
function createCodexAppWorkerClient(options = {}) {
const workerPath = options.workerPath || path.join(__dirname, 'codex-app-worker.js');
const onNotification = typeof options.onNotification === 'function' ? options.onNotification : () => {};
const onServerRequest = typeof options.onServerRequest === 'function' ? options.onServerRequest : null;
const onExit = typeof options.onExit === 'function' ? options.onExit : () => {};
const onLog = typeof options.onLog === 'function' ? options.onLog : () => {};
let worker = null;
let nextId = 1;
let configured = false;
let workerExited = false;
let appServerRunning = false;
const pending = new Map();
function rejectAllPending(err) {
for (const [, item] of pending) {
clearTimeout(item.timer);
item.reject(err);
}
pending.clear();
}
function specPayload() {
return {
command: options.command,
args: Array.isArray(options.args) ? options.args : [],
env: options.env || process.env,
cwd: options.cwd || process.cwd(),
clientInfo: options.clientInfo || null,
};
}
function ensureWorker() {
if (worker && !workerExited) return worker;
workerExited = false;
configured = false;
appServerRunning = false;
worker = fork(workerPath, [], {
cwd: options.cwd || process.cwd(),
env: process.env,
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
});
worker.on('message', (message = {}) => {
if (Object.prototype.hasOwnProperty.call(message, 'id')) {
const item = pending.get(message.id);
if (!item) return;
pending.delete(message.id);
clearTimeout(item.timer);
if (message.error) {
const err = new Error(message.error.message || 'Codex App worker 请求失败。');
err.code = message.error.code;
err.data = message.error.data;
item.reject(err);
} else {
item.resolve(message.result || {});
}
return;
}
if (message.type === 'notification') {
onNotification(message.notification);
return;
}
if (message.type === 'serverRequest') {
handleServerRequest(message);
return;
}
if (message.type === 'exit') {
appServerRunning = false;
onExit(message.info || {});
return;
}
if (message.type === 'log') {
onLog(message.level || 'INFO', message.event || 'codex_app_worker_log', message.data || {});
}
});
worker.on('exit', (code, signal) => {
workerExited = true;
configured = false;
appServerRunning = false;
rejectAllPending(new Error(`Codex App worker 已退出: code=${code ?? 'null'} signal=${signal || 'null'}`));
onExit({ code, signal, stderr: 'Codex App worker process exited' });
});
worker.on('error', (err) => {
workerExited = true;
configured = false;
appServerRunning = false;
rejectAllPending(err);
onExit({ code: null, signal: null, stderr: err.message });
});
return worker;
}
function sendWorker(type, payload = {}, timeoutMs = 300000) {
const proc = ensureWorker();
const id = nextId++;
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
pending.delete(id);
reject(new Error(`Codex App worker 请求超时: ${type}`));
}, timeoutMs);
pending.set(id, { resolve, reject, timer, type });
try {
proc.send({ id, type, ...payload });
} catch (err) {
clearTimeout(timer);
pending.delete(id);
reject(err);
}
});
}
function sendWorkerNotification(type, payload = {}) {
const proc = ensureWorker();
proc.send({ type, ...payload });
}
function handleServerRequest(message) {
const requestId = message.requestId;
if (!requestId) return;
if (!onServerRequest) {
sendWorkerNotification('serverRequestResult', {
requestId,
error: { code: -32601, message: 'cc-web 暂不支持 Codex app-server 请求。' },
});
return;
}
Promise.resolve()
.then(() => onServerRequest(message.request || {}))
.then((result) => {
sendWorkerNotification('serverRequestResult', { requestId, result: result || {} });
})
.catch((err) => {
sendWorkerNotification('serverRequestResult', {
requestId,
error: { code: -32603, message: err?.message || 'cc-web 处理 Codex App worker 请求失败。' },
});
});
}
async function configureIfNeeded() {
if (configured) return;
await sendWorker('configure', { spec: specPayload() }, 30000);
configured = true;
}
async function start() {
await configureIfNeeded();
const result = await sendWorker('start', {}, 30000);
appServerRunning = true;
return result;
}
async function request(method, params = {}, timeoutMs = 300000) {
await configureIfNeeded();
return sendWorker('request', { method, params, timeoutMs }, timeoutMs + 1000);
}
function notification(method, params = {}) {
sendWorkerNotification('notification', { method, params });
}
async function reloadMcpServers() {
await configureIfNeeded();
return sendWorker('reloadMcpServers', {}, 30000);
}
function stop() {
appServerRunning = false;
configured = false;
if (worker && !workerExited) {
try { worker.send({ type: 'stop' }); } catch {}
setTimeout(() => {
try {
if (worker && !worker.killed) worker.kill('SIGKILL');
} catch {}
}, 3000);
}
rejectAllPending(new Error('Codex App worker 已停止。'));
}
function isRunning() {
return !!worker && !workerExited && appServerRunning;
}
return {
start,
stop,
request,
notification,
reloadMcpServers,
isRunning,
pid: () => worker?.pid || null,
};
}
module.exports = { createCodexAppWorkerClient };

165
lib/codex-app-worker.js Normal file
View File

@@ -0,0 +1,165 @@
'use strict';
const { createCodexAppServerClient } = require('./codex-app-server-client');
let client = null;
let currentSpec = null;
let nextParentRequestId = 1;
const pendingParentRequests = new Map();
function send(message) {
if (typeof process.send === 'function') {
process.send(message);
}
}
function serializeError(err) {
return {
code: err?.code || -32603,
message: err?.message || String(err || 'Codex App worker error'),
data: err?.data || null,
};
}
function reply(id, result, error) {
if (!id) return;
if (error) {
send({ id, error: serializeError(error) });
} else {
send({ id, result: result || {} });
}
}
function requestParent(request, timeoutMs = 300000) {
const requestId = String(nextParentRequestId++);
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
pendingParentRequests.delete(requestId);
reject(new Error(`Codex App worker 等待主进程处理请求超时: ${request.method || ''}`));
}, timeoutMs);
pendingParentRequests.set(requestId, { resolve, reject, timer });
send({ type: 'serverRequest', requestId, request });
});
}
async function postInitialize({ request, onLog } = {}) {
if (typeof request !== 'function') return;
try {
await request('experimentalFeature/enablement/set', { enablement: { goals: true } }, 30000);
if (typeof onLog === 'function') onLog('INFO', 'codex_app_worker_goals_feature_enabled', {});
} catch (err) {
if (typeof onLog === 'function') {
onLog('INFO', 'codex_app_worker_goals_feature_enable_failed', { error: err?.message || String(err || '') });
}
}
try {
const result = await request('collaborationMode/list', {}, 30000);
if (typeof onLog === 'function') onLog('INFO', 'codex_app_worker_collaboration_modes', { result });
} catch (err) {
if (typeof onLog === 'function') {
onLog('INFO', 'codex_app_worker_collaboration_mode_list_failed', { error: err?.message || String(err || '') });
}
}
}
function configure(spec = {}) {
const nextSpec = {
command: spec.command || 'codex',
args: Array.isArray(spec.args) && spec.args.length > 0 ? spec.args : ['app-server', '--stdio'],
env: spec.env || process.env,
cwd: spec.cwd || process.cwd(),
clientInfo: spec.clientInfo || undefined,
};
const nextSignature = JSON.stringify({
command: nextSpec.command,
args: nextSpec.args,
cwd: nextSpec.cwd,
envCodeHome: nextSpec.env.CODEX_HOME || '',
});
const currentSignature = currentSpec ? JSON.stringify({
command: currentSpec.command,
args: currentSpec.args,
cwd: currentSpec.cwd,
envCodeHome: currentSpec.env.CODEX_HOME || '',
}) : '';
if (client && nextSignature === currentSignature) {
currentSpec = nextSpec;
return;
}
if (client) {
try { client.stop(); } catch {}
client = null;
}
currentSpec = nextSpec;
client = createCodexAppServerClient({
command: nextSpec.command,
args: nextSpec.args,
env: nextSpec.env,
cwd: nextSpec.cwd,
clientInfo: nextSpec.clientInfo,
onNotification: (notification) => send({ type: 'notification', notification }),
onServerRequest: (request) => requestParent(request),
onExit: (info) => send({ type: 'exit', info }),
onLog: (level, event, data) => send({ type: 'log', level, event, data }),
postInitialize,
});
}
process.on('message', (message = {}) => {
if (message.type === 'serverRequestResult') {
const item = pendingParentRequests.get(String(message.requestId || ''));
if (!item) return;
pendingParentRequests.delete(String(message.requestId || ''));
clearTimeout(item.timer);
if (message.error) {
const err = new Error(message.error.message || '主进程处理 Codex App 请求失败。');
err.code = message.error.code;
err.data = message.error.data;
item.reject(err);
} else {
item.resolve(message.result || {});
}
return;
}
Promise.resolve()
.then(async () => {
switch (message.type) {
case 'configure':
configure(message.spec || {});
return {};
case 'start':
if (!client) configure(currentSpec || {});
return client.start();
case 'request':
if (!client) configure(currentSpec || {});
return client.request(message.method, message.params || {}, message.timeoutMs || 300000);
case 'notification':
if (!client) configure(currentSpec || {});
client.notification(message.method, message.params || {});
return {};
case 'reloadMcpServers':
if (!client) configure(currentSpec || {});
return client.reloadMcpServers();
case 'stop':
if (client) client.stop();
process.exit(0);
return {};
default:
throw new Error(`未知 Codex App worker 消息: ${message.type}`);
}
})
.then((result) => reply(message.id, result))
.catch((err) => reply(message.id, null, err));
});
process.on('disconnect', () => {
if (client) {
try { client.stop(); } catch {}
}
process.exit(0);
});