feat: complete Codex App capabilities and rebuild release

This commit is contained in:
shiyue
2026-08-20 13:37:23 +08:00
parent d7dc32e924
commit 19c1601351
8 changed files with 756 additions and 54 deletions

407
server.js
View File

@@ -765,6 +765,12 @@ const activeCodexAppTurns = new Map();
// Active Codex app-server goal RPCs: sessionId -> { id, ws, action, cancelled }
const activeCodexAppGoalCommands = new Map();
// Latest Codex app-server Goal state: sessionId -> normalized ThreadGoal
const codexAppGoalStates = new Map();
// Active Codex app-server compact RPCs: sessionId -> { id, ws, threadId, cancelled }
const activeCodexAppCompactions = new Map();
// ccweb MCP child agents tracked from Codex App native collaboration mode:
// childThreadId -> { parentSessionId, parentThreadId, spawnToolId, ...state }
const ccwebMcpChildThreads = new Map();
@@ -863,7 +869,7 @@ const DEFAULT_CODEX_CONFIG = {
activeProfile: '',
profiles: [],
enableSearch: false,
supportsSearch: false,
supportsSearch: true,
retry: {
mode: 'limited',
intervalSeconds: Math.max(1, Math.ceil(CODEX_TRANSIENT_RETRY_BASE_DELAY_MS / 1000)),
@@ -1308,9 +1314,8 @@ function loadCodexConfig() {
apiKey: String(profile?.apiKey || ''),
apiBase: String(profile?.apiBase || '').trim(),
})).filter((profile) => profile.name) : [],
enableSearch: false,
supportsSearch: false,
storedEnableSearch: !!raw.enableSearch,
enableSearch: !!raw.enableSearch,
supportsSearch: true,
retry: normalizeCodexRetryConfig(raw.retry),
};
}
@@ -1327,7 +1332,7 @@ function saveCodexConfig(config) {
apiKey: String(profile?.apiKey || ''),
apiBase: String(profile?.apiBase || '').trim(),
})).filter((profile) => profile.name) : [],
enableSearch: false,
enableSearch: !!config.enableSearch,
retry: normalizeCodexRetryConfig(config.retry),
}, null, 2));
}
@@ -1342,9 +1347,8 @@ function getCodexConfigMasked() {
apiKey: maskSecret(profile.apiKey),
apiBase: profile.apiBase || '',
})),
enableSearch: false,
supportsSearch: false,
storedEnableSearch: !!config.storedEnableSearch,
enableSearch: !!config.enableSearch,
supportsSearch: true,
retry: normalizeCodexRetryConfig(config.retry),
};
}
@@ -3249,7 +3253,11 @@ function isCodexLikeSession(session) {
}
function isSessionRunning(sessionId) {
return activeProcesses.has(sessionId) || activeCodexAppTurns.has(sessionId) || activeCodexAppGoalCommands.has(sessionId);
return activeProcesses.has(sessionId)
|| activeCodexAppTurns.has(sessionId)
|| activeCodexAppGoalCommands.has(sessionId)
|| isCodexAppGoalActive(sessionId)
|| activeCodexAppCompactions.has(sessionId);
}
function getRuntimeSessionId(session) {
@@ -3736,11 +3744,17 @@ async function handleReloadMcpApi(req, res, rawSessionId) {
function setRuntimeSessionId(session, runtimeId) {
if (!session) return;
const agent = getSessionAgent(session);
if (agent === 'codex') {
session.codexThreadId = runtimeId || null;
} else if (agent === 'codexapp') {
session.codexAppThreadId = runtimeId || null;
} else {
if (agent === 'codex') {
session.codexThreadId = runtimeId || null;
} else if (agent === 'codexapp') {
const previousThreadId = normalizeCodexAppThreadId(session.codexAppThreadId);
const nextThreadId = normalizeCodexAppThreadId(runtimeId);
session.codexAppThreadId = runtimeId || null;
if (previousThreadId && previousThreadId !== nextThreadId) {
session.codexAppGoal = null;
if (session.id) codexAppGoalStates.delete(session.id);
}
} else {
session.claudeSessionId = runtimeId || null;
}
}
@@ -4250,8 +4264,9 @@ function loadSessionMetaFromFile(filePath) {
hasUnread: !!session.hasUnread,
agent: getSessionAgent(session),
cwd,
projectName: cwd ? path.basename(cwd.replace(/[\\/]+$/, '')) : '',
fileBytes: stat.size,
projectName: cwd ? path.basename(cwd.replace(/[\\/]+$/, '')) : '',
codexAppGoalActive: isCodexThreadGoalActive(session.codexAppGoal),
fileBytes: stat.size,
oversized: stat.size > SESSION_LOAD_MAX_BYTES,
};
}
@@ -4271,8 +4286,9 @@ function loadSessionMetaFromFile(filePath) {
hasUnread: previewBooleanField(previewFields, 'hasUnread'),
agent: normalizeAgent(previewStringField(previewFields, 'agent')),
cwd,
projectName: cwd ? path.basename(cwd.replace(/[\\/]+$/, '')) : '',
fileBytes: stat.size,
projectName: cwd ? path.basename(cwd.replace(/[\\/]+$/, '')) : '',
codexAppGoalActive: false,
fileBytes: stat.size,
oversized: stat.size > SESSION_LOAD_MAX_BYTES,
};
} catch (err) {
@@ -4290,8 +4306,9 @@ function loadSession(id) {
try {
const filePath = sessionPath(normalizedId);
if (!fs.existsSync(filePath)) return null;
const session = normalizeSession(safeReadSessionJson(filePath, SESSION_LOAD_MAX_BYTES, { sessionId: normalizedId }));
updateSessionRuntimeThreadIndex(session);
const session = normalizeSession(safeReadSessionJson(filePath, SESSION_LOAD_MAX_BYTES, { sessionId: normalizedId }));
syncCodexAppGoalStateFromSession(session);
updateSessionRuntimeThreadIndex(session);
return session;
} catch (err) {
plog('WARN', 'session_load_failed', {
@@ -5315,7 +5332,7 @@ function sendSessionList(ws) {
agent: normalizeAgent(meta.agent),
cwd: meta.cwd || '',
projectName: meta.projectName || '',
isRunning: isSessionRunning(meta.id),
isRunning: isSessionRunning(meta.id) || meta.codexAppGoalActive === true,
waitingOnChildren: waitState.waitingOnChildren,
pendingReplyCount: waitState.pendingReplyCount,
readyReplyCount: waitState.readyReplyCount,
@@ -6687,12 +6704,14 @@ function formatRuntimeError(agent, raw, context = {}) {
}
function compactStartMessage(agent) {
if (agent === 'codexapp') return '正在执行 Codex App 原生 /compact 压缩上下文,请稍候…';
return agent === 'codex'
? '正在执行 Codex /compact 压缩上下文,请稍候…'
: '正在执行 Claude 原生 /compact 压缩上下文,请稍候…';
}
function compactDoneMessage(agent) {
if (agent === 'codexapp') return '上下文压缩完成。已执行 Codex App 原生 /compact下次继续在同一会话发送即可。';
return agent === 'codex'
? '上下文压缩完成。已执行 Codex /compact下次继续在同一会话发送即可。'
: '上下文压缩完成。已按 Claude Code 原生策略执行 /compact下次继续在同一会话发送即可。';
@@ -7909,9 +7928,8 @@ function handleSaveCodexConfig(ws, newConfig) {
mode: newConfig.mode === 'custom' ? 'custom' : 'local',
activeProfile: String(newConfig.activeProfile || '').trim(),
profiles: mergedProfiles,
enableSearch: false,
supportsSearch: false,
storedEnableSearch: requestedSearch,
enableSearch: requestedSearch,
supportsSearch: true,
retry,
};
if (merged.mode === 'custom' && merged.profiles.length > 0 && !merged.profiles.some((profile) => profile.name === merged.activeProfile)) {
@@ -7923,7 +7941,7 @@ function handleSaveCodexConfig(ws, newConfig) {
activeProfile: merged.activeProfile || null,
profileCount: merged.profiles.length,
enableSearchRequested: requestedSearch,
enableSearchEffective: false,
enableSearchEffective: merged.enableSearch,
retryMode: retry.mode,
retryIntervalSeconds: retry.intervalSeconds,
retryMaxAttempts: retry.mode === 'limited' ? retry.maxAttempts : null,
@@ -7931,9 +7949,7 @@ function handleSaveCodexConfig(ws, newConfig) {
wsSend(ws, { type: 'codex_config', config: getCodexConfigMasked() });
wsSend(ws, {
type: 'system_message',
message: requestedSearch
? 'Codex 配置已保存。当前 cc-web 的 Codex exec 路径暂未接入 Web Search已自动忽略该开关。'
: 'Codex 配置已保存',
message: 'Codex 配置已保存',
});
}
@@ -8050,6 +8066,63 @@ function normalizeCodexThreadGoal(goal, fallbackThreadId = '') {
};
}
function codexGoalStatusKey(status) {
return goalString(status || 'active').toLowerCase().replace(/[\s_-]/g, '');
}
function isCodexThreadGoalActive(goal) {
return !!goal && codexGoalStatusKey(goal.status) === 'active';
}
function isCodexAppGoalActive(sessionId) {
return isCodexThreadGoalActive(codexAppGoalStates.get(sessionId));
}
function syncCodexAppGoalStateFromSession(session) {
const sessionId = sanitizeId(session?.id || '');
if (!sessionId || !isCodexAppSession(session)) return null;
const threadId = normalizeCodexAppThreadId(getRuntimeSessionId(session));
const goal = normalizeCodexThreadGoal(session.codexAppGoal, threadId || '');
if (!goal || (threadId && goal.threadId && goal.threadId !== threadId)) {
codexAppGoalStates.delete(sessionId);
return null;
}
codexAppGoalStates.set(sessionId, goal);
return goal;
}
function updateCodexAppGoalState(session, goal, options = {}) {
const sessionId = sanitizeId(session?.id || '');
if (!sessionId || !isCodexAppSession(session)) return null;
const threadId = normalizeCodexAppThreadId(getRuntimeSessionId(session));
const normalized = normalizeCodexThreadGoal(goal, threadId || '');
const previous = normalizeCodexThreadGoal(
codexAppGoalStates.get(sessionId) || session.codexAppGoal,
threadId || '',
);
if (
options.force !== true
&& normalized
&& previous
&& normalized.threadId === previous.threadId
&& normalized.updatedAt > 0
&& previous.updatedAt > normalized.updatedAt
) {
return previous;
}
if (normalized) {
session.codexAppGoal = normalized;
codexAppGoalStates.set(sessionId, normalized);
} else {
session.codexAppGoal = null;
codexAppGoalStates.delete(sessionId);
}
if (options.persist !== false) saveSession(session);
if (options.broadcast !== false) broadcastSessionList();
return normalized;
}
function formatCodexGoalStatus(status) {
const normalized = String(status || 'active').trim();
const compact = normalized.toLowerCase().replace(/[\s_-]/g, '');
@@ -8133,6 +8206,122 @@ async function ensureCodexAppGoalThread(session) {
return { client, threadId };
}
function isCurrentCodexAppCompaction(sessionId, entry) {
return !!entry
&& activeCodexAppCompactions.get(sessionId)?.id === entry.id
&& !entry.cancelled;
}
function finishCodexAppCompaction(sessionId, entry) {
if (!entry || activeCodexAppCompactions.get(sessionId)?.id !== entry.id) return false;
activeCodexAppCompactions.delete(sessionId);
broadcastSessionList();
return true;
}
function cancelCodexAppCompaction(sessionId, ws = null) {
const entry = activeCodexAppCompactions.get(sessionId);
if (!entry) return false;
entry.cancelled = true;
activeCodexAppCompactions.delete(sessionId);
const targetWs = ws || entry.ws || null;
if (targetWs) {
wsSend(targetWs, {
type: 'system_message',
sessionId,
message: '已取消 Codex App /compact 状态。底层 app-server 请求可能仍会自然返回,结果将被忽略。',
});
}
broadcastSessionList();
return true;
}
function isCodexCompactUnsupportedError(err) {
const detail = `${err?.code || ''} ${err?.message || err || ''}`;
return err?.code === -32601
|| /compact.*unsupported|unsupported.*compact|method not found|unknown mock method|unsupported remote app-server request/i.test(detail);
}
async function handleCodexAppCompactSlashCommand(ws, session, source = {}) {
if (!session || !isCodexAppSession(session)) return;
const sessionId = session.id;
const sendCompactResponse = (targetWs, payload, options = {}) => {
if (!targetWs) return;
wsSend(targetWs, attachClientRequestId({
...payload,
...(options.preserveComposerDraft ? { preserveComposerDraft: true } : {}),
}, source));
};
const sendCompactSystemMessage = (targetWs, message, extra = {}, options = {}) => {
sendCompactResponse(targetWs, { type: 'system_message', message, ...extra }, options);
};
if (activeCodexAppCompactions.has(sessionId)) {
sendCompactSystemMessage(ws, 'Codex App /compact 正在执行,请稍候。', { sessionId }, { preserveComposerDraft: true });
return;
}
const runtimeThreadId = getRuntimeSessionId(session);
if (!runtimeThreadId) {
sendCompactSystemMessage(ws, '当前会话尚未建立 Codex App 上下文,暂时无需压缩。', { sessionId }, { preserveComposerDraft: true });
return;
}
const compactEntry = {
id: crypto.randomUUID(),
ws,
threadId: runtimeThreadId,
cancelled: false,
startedAt: new Date().toISOString(),
};
activeCodexAppCompactions.set(sessionId, compactEntry);
sendCompactSystemMessage(ws, compactStartMessage('codexapp'), { sessionId });
broadcastSessionList();
try {
const clientResult = getCodexAppClient({ excludeSessionId: sessionId });
if (clientResult.error) throw new Error(clientResult.error);
const client = clientResult.client;
await client.start();
if (!isCurrentCodexAppCompaction(sessionId, compactEntry)) return;
const threadParams = codexAppThreadParams(session);
const resumed = await client.request('thread/resume', {
...threadParams,
threadId: runtimeThreadId,
}, 60000);
const threadId = resumed?.thread?.id || runtimeThreadId;
if (threadId !== runtimeThreadId) {
throw new Error(`Codex App 恢复到不同线程,已停止压缩(期望 ${String(runtimeThreadId).slice(0, 24)},实际 ${String(threadId).slice(0, 24)})。`);
}
const response = await client.request('thread/compact/start', { threadId }, 300000);
if (!isCurrentCodexAppCompaction(sessionId, compactEntry)) return;
session.updated = new Date().toISOString();
saveSession(session);
sendCompactSystemMessage(compactEntry.ws || ws, compactDoneMessage('codexapp'), { sessionId });
if (response?.threadId && response.threadId !== threadId) {
plog('WARN', 'codex_app_compact_thread_mismatch', {
sessionId: sessionId.slice(0, 8),
expectedThreadId: threadId,
actualThreadId: response.threadId,
});
}
} catch (err) {
if (isCurrentCodexAppCompaction(sessionId, compactEntry)) {
const message = isCodexCompactUnsupportedError(err)
? '当前 Codex app-server 不支持 /compact请升级 Codex 后重试。'
: `Codex App /compact 失败:${err?.message || err}`;
sendCompactSystemMessage(compactEntry.ws || ws, message, {
sessionId,
tone: 'danger',
}, { preserveComposerDraft: true });
}
} finally {
finishCodexAppCompaction(sessionId, compactEntry);
}
}
function isCodexGoalUnsupportedError(err) {
const detail = `${err?.code || ''} ${err?.message || err || ''}`;
return err?.code === -32601
@@ -8256,6 +8445,7 @@ async function handleCodexAppGoalSlashCommand(ws, text, session, source = {}) {
const response = await client.request('thread/goal/get', { threadId }, 30000);
if (!isCurrentCodexAppGoalCommand(session.id, activeGoalCommand)) return;
const goal = normalizeCodexThreadGoal(response?.goal, threadId);
updateCodexAppGoalState(session, goal);
const targetWs = activeGoalCommand.ws || ws;
sendGoalSystemMessage(targetWs, goal ? formatCodexGoalUsage(goal) : '用法: /goal <目标描述>', { sessionId: session.id });
sendSessionList(targetWs);
@@ -8265,6 +8455,7 @@ async function handleCodexAppGoalSlashCommand(ws, text, session, source = {}) {
if (command.action === 'clear') {
const response = await client.request('thread/goal/clear', { threadId }, 30000);
if (!isCurrentCodexAppGoalCommand(session.id, activeGoalCommand)) return;
updateCodexAppGoalState(session, null);
const targetWs = activeGoalCommand.ws || ws;
sendGoalSystemMessage(targetWs, response?.cleared ? 'Goal cleared' : 'No goal to clear', {
sessionId: session.id,
@@ -8283,6 +8474,7 @@ async function handleCodexAppGoalSlashCommand(ws, text, session, source = {}) {
}, 30000);
if (!isCurrentCodexAppGoalCommand(session.id, activeGoalCommand)) return;
const goal = normalizeCodexThreadGoal(response?.goal, threadId);
updateCodexAppGoalState(session, goal);
const targetWs = activeGoalCommand.ws || ws;
sendGoalSystemMessage(targetWs, goal ? formatCodexGoalUsage(goal, { includeObjective: false }) : 'Goal updated', {
sessionId: session.id,
@@ -8341,6 +8533,10 @@ function handleSlashCommand(ws, text, sessionId, fallbackAgent, source = {}) {
sendSlashSystemMessage('Codex App Goal 正在同步,请稍候。', { sessionId }, { preserveComposerDraft: true });
return true;
}
if (session && isCodexAppSession(session) && activeCodexAppCompactions.has(sessionId)) {
sendSlashSystemMessage('Codex App /compact 正在执行,请稍候。', { sessionId }, { preserveComposerDraft: true });
return true;
}
switch (cmd) {
case '/clear': {
@@ -8452,7 +8648,9 @@ function handleSlashCommand(ws, text, sessionId, fallbackAgent, source = {}) {
break;
}
if (isCodexAppSession(session)) {
sendSlashSystemMessage('Codex App 模式暂不支持 /compact请切换到旧 Codex 模式或等待后续接入。', {}, { preserveComposerDraft: true });
handleCodexAppCompactSlashCommand(ws, session, source).catch((err) => {
sendSlashSystemMessage(`Codex App /compact 失败:${err?.message || err}`, { sessionId: session.id }, { preserveComposerDraft: true });
});
break;
}
const runtimeId = getRuntimeSessionId(session);
@@ -8531,7 +8729,7 @@ function handleSlashCommand(ws, text, sessionId, fallbackAgent, source = {}) {
sendSlashResponse({
type: 'system_message',
message: codexLikeAgent
? base + `\n/model [名称] — 查看/切换 ${agent === 'codexapp' ? 'Codex App' : 'Codex'} 模型(自由输入)${agent === 'codexapp' ? '\n/goal [目标] — 设置/查看持久目标;支持 pause/resume/clear' : ''}\n/init — 分析项目并生成/更新 AGENTS.md${agent === 'codexapp' ? '\n/compact — Codex App 模式暂不支持' : '\n/compact — 执行 Codex /compact 压缩上下文'}`
? base + `\n/model [名称] — 查看/切换 ${agent === 'codexapp' ? 'Codex App' : 'Codex'} 模型(自由输入)${agent === 'codexapp' ? '\n/goal [目标] — 设置/查看持久目标;支持 pause/resume/clear' : ''}\n/init — 分析项目并生成/更新 AGENTS.md${agent === 'codexapp' ? '\n/compact — 执行 Codex App 原生上下文压缩' : '\n/compact — 执行 Codex /compact 压缩上下文'}`
: base + '\n/model [名称] — 查看/切换模型opus, sonnet, haiku\n/compact — 执行 Claude 原生上下文压缩(保留压缩计划并可自动续跑)\n/init — 分析项目并生成/更新 CLAUDE.md',
});
break;
@@ -9175,6 +9373,8 @@ function handleDeleteSession(ws, sessionId) {
pendingSlashCommands.delete(sessionId);
pendingCompactRetries.delete(sessionId);
cancelCodexCapacityRetry(sessionId);
cancelCodexAppCompaction(sessionId);
codexAppGoalStates.delete(sessionId);
removeSessionRuntimeThreadIndex(sessionId);
if (activeCodexAppGoalCommands.has(sessionId)) {
const entry = activeCodexAppGoalCommands.get(sessionId);
@@ -9290,6 +9490,12 @@ function handleDisconnect(ws, wsId) {
affectedSessions.push({ sessionId: sid.slice(0, 8), threadId: entry.threadId || null, turnId: entry.turnId || null });
}
}
for (const [sid, entry] of activeCodexAppCompactions) {
if (entry.ws === ws) {
entry.ws = null;
affectedSessions.push({ sessionId: sid.slice(0, 8), threadId: entry.threadId || null, compact: true });
}
}
wsSessionMap.delete(ws);
plog('INFO', 'ws_disconnect', { wsId, activeProcessesAffected: affectedSessions });
}
@@ -9305,6 +9511,7 @@ function bindAbortSessionToWs(sessionId, ws) {
activeProcesses.get(sessionId),
activeCodexAppTurns.get(sessionId),
activeCodexAppGoalCommands.get(sessionId),
activeCodexAppCompactions.get(sessionId),
].filter(Boolean);
if (entries.length > 0) detachWsFromActiveRuntimes(ws);
wsSessionMap.set(ws, sessionId);
@@ -9314,13 +9521,81 @@ function bindAbortSessionToWs(sessionId, ws) {
}
}
function pauseCodexAppGoalForAbort(sessionId, ws = null) {
const session = loadSession(sessionId);
const threadId = normalizeCodexAppThreadId(getRuntimeSessionId(session));
const currentGoal = normalizeCodexThreadGoal(
codexAppGoalStates.get(sessionId) || session?.codexAppGoal,
threadId || '',
);
if (!session || !threadId || !isCodexThreadGoalActive(currentGoal)) return false;
const pausedGoal = {
...currentGoal,
status: 'paused',
updatedAt: Math.max(Date.now(), Number(currentGoal.updatedAt || 0) + 1),
};
updateCodexAppGoalState(session, pausedGoal);
plog('INFO', 'codex_app_goal_pause_requested_by_abort', {
sessionId: sessionId.slice(0, 8),
threadId,
});
Promise.resolve().then(async () => {
const clientResult = getCodexAppClient();
if (clientResult.error) throw new Error(clientResult.error);
const client = clientResult.client;
await client.start();
const response = await client.request('thread/goal/set', {
threadId,
status: 'paused',
}, 30000);
const refreshedSession = loadSession(sessionId);
if (!refreshedSession) return;
const confirmedGoal = normalizeCodexThreadGoal(response?.goal, threadId);
if (confirmedGoal) updateCodexAppGoalState(refreshedSession, confirmedGoal);
if (ws) {
wsSend(ws, {
type: 'system_message',
sessionId,
tone: 'info',
transient: true,
autoDismissMs: 5000,
message: 'Goal paused',
});
}
}).catch((err) => {
const refreshedSession = loadSession(sessionId);
const latestGoal = codexAppGoalStates.get(sessionId);
if (
refreshedSession
&& codexGoalStatusKey(latestGoal?.status) === 'paused'
&& Number(latestGoal?.updatedAt || 0) === pausedGoal.updatedAt
) {
updateCodexAppGoalState(refreshedSession, currentGoal, { force: true });
}
if (ws) {
wsSend(ws, {
type: 'error',
sessionId,
code: 'codexapp_goal_pause_failed',
message: `暂停 Goal 失败:${err?.message || err}`,
});
}
});
return true;
}
function handleAbort(ws, msg = {}) {
const requestedSessionId = sanitizeId(msg?.sessionId || '');
const sessionId = requestedSessionId || wsSessionMap.get(ws);
if (!sessionId) return;
bindAbortSessionToWs(sessionId, ws);
if (handleCodexAppAbortSession(sessionId, ws)) return;
const goalPauseStarted = pauseCodexAppGoalForAbort(sessionId, ws);
const turnAbortStarted = handleCodexAppAbortSession(sessionId, ws);
if (turnAbortStarted || goalPauseStarted) return;
if (cancelCodexAppGoalCommand(sessionId, ws)) return;
if (cancelCodexAppCompaction(sessionId, ws)) return;
const entry = activeProcesses.get(sessionId);
if (!entry) {
if (cancelCodexCapacityRetry(sessionId)) {
@@ -9453,6 +9728,15 @@ function handleMessage(ws, msg, options = {}) {
return fail('session_running', 'Codex App Goal 正在同步,请稍候。');
}
if (sessionId && activeCodexAppCompactions.has(sessionId)) {
return fail('session_running', 'Codex App /compact 正在执行,请稍候。');
}
if (sessionId && !codexAppGoalStates.has(sessionId)) loadSession(sessionId);
if (sessionId && isCodexAppGoalActive(sessionId)) {
return fail('session_running', 'Goal 仍在持续运行,请先点击停止按钮暂停 Goal。');
}
if (sessionId && activeProcesses.has(sessionId)) {
return fail('session_running', '正在处理中,请先点击停止按钮。');
}
@@ -9825,6 +10109,12 @@ function detachWsFromActiveRuntimes(ws, options = {}) {
if (disconnectTime) entry.wsDisconnectTime = disconnectTime;
}
}
for (const [, entry] of activeCodexAppCompactions) {
if (entry.ws === ws) {
entry.ws = null;
if (disconnectTime) entry.wsDisconnectTime = disconnectTime;
}
}
}
function codexAppRuntimeThreadId(params = {}) {
@@ -10614,7 +10904,47 @@ function shouldLogCodexAppUnroutedNotification(notification) {
return true;
}
function handleCodexAppGoalNotification(notification) {
const method = String(notification?.method || '').trim();
if (method !== 'thread/goal/updated' && method !== 'thread/goal/cleared') return false;
const threadId = normalizeCodexAppThreadId(
notification?.params?.threadId || notification?.params?.thread?.id,
);
if (!threadId) return false;
const matched = findCodexAppSessionByThreadId(threadId);
if (!matched?.session) return false;
const goal = method === 'thread/goal/updated'
? normalizeCodexThreadGoal(notification?.params?.goal, threadId)
: null;
if (method === 'thread/goal/updated' && !goal) return false;
updateCodexAppGoalState(matched.session, goal);
plog('INFO', method === 'thread/goal/updated' ? 'codex_app_goal_updated' : 'codex_app_goal_cleared', {
sessionId: matched.sessionId.slice(0, 8),
threadId,
status: goal?.status || null,
tokensUsed: goal?.tokensUsed || 0,
});
return true;
}
function handleCodexAppNotification(notification) {
if (notification?.method === 'thread/compacted') {
const threadId = normalizeCodexAppThreadId(
notification?.params?.threadId
|| notification?.params?.thread_id
|| notification?.params?.thread?.id,
);
if (threadId) {
for (const entry of activeCodexAppCompactions.values()) {
if (entry.threadId === threadId) {
entry.compactedNotification = true;
return;
}
}
}
}
if (handleCodexAppGoalNotification(notification)) return;
const routed = findCodexAppRouteByRuntime(notification?.params || {}, notification?.method || '');
if (handleCodexAppMcpStartupStatusNotification(notification, routed)) return;
if (!routed) {
@@ -11187,7 +11517,9 @@ function codexAppCcwebMcpEnv(session, options = {}) {
}
function codexAppThreadConfig(session, options = {}) {
const config = {};
const config = {
web_search: loadCodexConfig().enableSearch ? 'live' : 'disabled',
};
for (const item of listRuntimeMcpServerConfigs({ ...options, session, agent: 'codexapp' })) {
if (!item?.server || !item?.config) continue;
config[`mcp_servers.${item.server}`] = item.config;
@@ -11580,6 +11912,7 @@ function handleCodexAppTurnComplete(sessionId, options = {}) {
activeCodexAppTurns.delete(sessionId);
cleanupCodexAppTurnState(sessionId, entry);
const goalActive = isCodexAppGoalActive(sessionId);
dispatchTaskBoardLifecycle(sessionId, {
type: completionError
? TASK_BOARD_LIFECYCLE_EVENTS.TURN_FAILED
@@ -11590,7 +11923,7 @@ function handleCodexAppTurnComplete(sessionId, options = {}) {
outcome: completionError ? 'failed' : 'completed',
trackingEnabled: entry.taskTrackingEnabled === true,
});
if (session && !completionError && !options.interrupted && !entry.userAborted) {
if (session && !goalActive && !completionError && !options.interrupted && !entry.userAborted) {
const toolEvidence = assistantToolCalls.length > 0
? truncateTextValue(JSON.stringify(assistantToolCalls.map((toolCall) => ({
name: toolCall?.name || '',
@@ -11619,6 +11952,7 @@ function handleCodexAppTurnComplete(sessionId, options = {}) {
responseLen: (entry.fullText || '').length,
toolCallCount: (entry.toolCalls || []).length,
error: rawError || null,
goalActive,
});
if (entry.ws) {
@@ -11626,11 +11960,16 @@ function handleCodexAppTurnComplete(sessionId, options = {}) {
entry.errorSent = true;
wsSend(entry.ws, { type: 'error', sessionId, message: completionError });
}
wsSend(entry.ws, { type: 'done', sessionId, costUsd: null });
wsSend(entry.ws, { type: 'done', sessionId, costUsd: null, goalActive });
sendSessionList(entry.ws);
return;
}
if (goalActive) {
broadcastSessionList();
return;
}
if (wss && session) {
for (const client of wss.clients) {
if (client.readyState === 1) {