chore: rebuild CentOS7 release package

This commit is contained in:
shiyue
2026-06-25 21:52:09 +08:00
parent 04dd48deb2
commit c387c92e4b
11 changed files with 924 additions and 34 deletions

368
server.js
View File

@@ -99,6 +99,8 @@ const RUN_OUTPUT_RECOVERY_MAX_BYTES = readPositiveIntEnv('CC_WEB_RUN_OUTPUT_RECO
const RUN_OUTPUT_TAILER_MAX_READ_BYTES = readPositiveIntEnv('CC_WEB_RUN_OUTPUT_TAILER_MAX_READ_BYTES', 2 * 1024 * 1024, { min: 64 * 1024 });
const CROSS_CONVERSATION_MAX_CONTENT_CHARS = readPositiveIntEnv('CC_WEB_CROSS_CONVERSATION_MAX_CONTENT_CHARS', 64 * 1024, { min: 1024 });
const MCP_CREATE_CONVERSATION_MAX_HOP_COUNT = readPositiveIntEnv('CC_WEB_MCP_CREATE_CONVERSATION_MAX_HOP_COUNT', 3, { min: 1, max: 20 });
const CODEX_TRANSIENT_RETRY_MAX_ATTEMPTS = readPositiveIntEnv('CC_WEB_CODEX_TRANSIENT_RETRY_MAX_ATTEMPTS', 3, { min: 1, max: 10 });
const CODEX_TRANSIENT_RETRY_BASE_DELAY_MS = readPositiveIntEnv('CC_WEB_CODEX_TRANSIENT_RETRY_BASE_DELAY_MS', 2000, { min: 100, max: 60000 });
const MAX_CODEX_GOAL_OBJECTIVE_CHARS = 4000;
const CODEX_APP_WORKER_DISABLED = /^(0|false|no|off)$/i.test(String(process.env.CC_WEB_CODEX_APP_WORKER || ''));
const CODEX_APP_WORKER_ENABLED = !CODEX_APP_WORKER_DISABLED;
@@ -632,6 +634,9 @@ const pendingSlashCommands = new Map();
// Pending compact retry metadata: sessionId -> { text: string, mode: string, reason: string }
const pendingCompactRetries = new Map();
// Pending Codex transient retry metadata: sessionId -> { text, runtimeText, mode, attempts, timer }
const pendingCodexCapacityRetries = new Map();
// Active processes: sessionId -> { pid, ws, fullText, toolCalls, lastCost, tailer }
const activeProcesses = new Map();
@@ -699,6 +704,11 @@ const DEFAULT_CODEX_CONFIG = {
profiles: [],
enableSearch: false,
supportsSearch: false,
retry: {
mode: 'limited',
intervalSeconds: Math.max(1, Math.ceil(CODEX_TRANSIENT_RETRY_BASE_DELAY_MS / 1000)),
maxAttempts: CODEX_TRANSIENT_RETRY_MAX_ATTEMPTS,
},
};
function stripTomlInlineComment(value) {
@@ -1009,6 +1019,20 @@ function saveModelConfig(config) {
fs.writeFileSync(MODEL_CONFIG_PATH, JSON.stringify(config, null, 2));
}
function normalizeCodexRetryConfig(raw = {}) {
const defaults = DEFAULT_CODEX_CONFIG.retry;
const mode = ['off', 'limited', 'forever'].includes(raw?.mode) ? raw.mode : defaults.mode;
const rawInterval = Number.parseInt(String(raw?.intervalSeconds ?? raw?.interval ?? ''), 10);
const intervalSeconds = Number.isFinite(rawInterval)
? Math.max(1, Math.min(3600, rawInterval))
: defaults.intervalSeconds;
const rawMaxAttempts = Number.parseInt(String(raw?.maxAttempts ?? raw?.attempts ?? ''), 10);
const maxAttempts = Number.isFinite(rawMaxAttempts)
? Math.max(1, Math.min(1000, rawMaxAttempts))
: defaults.maxAttempts;
return { mode, intervalSeconds, maxAttempts };
}
function loadCodexConfig() {
try {
if (fs.existsSync(CODEX_CONFIG_PATH)) {
@@ -1024,6 +1048,7 @@ function loadCodexConfig() {
enableSearch: false,
supportsSearch: false,
storedEnableSearch: !!raw.enableSearch,
retry: normalizeCodexRetryConfig(raw.retry),
};
}
} catch {}
@@ -1040,6 +1065,7 @@ function saveCodexConfig(config) {
apiBase: String(profile?.apiBase || '').trim(),
})).filter((profile) => profile.name) : [],
enableSearch: false,
retry: normalizeCodexRetryConfig(config.retry),
}, null, 2));
}
@@ -1056,6 +1082,7 @@ function getCodexConfigMasked() {
enableSearch: false,
supportsSearch: false,
storedEnableSearch: !!config.storedEnableSearch,
retry: normalizeCodexRetryConfig(config.retry),
};
}
@@ -4561,6 +4588,12 @@ function formatRuntimeError(agent, raw, context = {}) {
if (/rate limit|quota|billing|credits/i.test(condensed)) {
return 'Codex 请求被额度或速率限制拦截。请检查账号配额、计费状态或稍后重试。';
}
if (isCodexTransientCapacityError(condensed)) {
return 'Codex 服务暂时繁忙或所选模型容量不足。cc-web 已自动重试,仍未成功;请稍后再试或临时切换模型。';
}
if (isCodexTransientConnectionError(condensed)) {
return 'Codex App 连接暂时中断。cc-web 已自动重试,仍未成功;请稍后再试或检查网络代理。';
}
if (/network|timed out|timeout|ECONNRESET|ENOTFOUND|TLS|certificate|fetch failed/i.test(condensed)) {
return 'Codex 运行时网络请求失败。请检查当前网络、代理或证书环境后重试。';
}
@@ -4634,6 +4667,144 @@ function isContextLimitError(agent, raw) {
return /context\s+(window|length)|maximum context length|context limit|token limit|too many tokens|input.*too long|prompt.*too long|request too large|please use\s*\/compact|use\s*\/compact|reduce (the )?(input|prompt|message)|exceed(?:ed|s).*(token|context)/i.test(text);
}
function isCodexTransientCapacityError(raw) {
const text = String(raw || '');
if (!text) return false;
return /server_is_overloaded|service_unavailable_error|ServiceUnavailableError|servers?\s+(?:are\s+)?(?:currently\s+)?overloaded|server\s+is\s+overloaded|model\s+is\s+at\s+capacity|selected model is at capacity|model.*overloaded|503\b.*(?:overloaded|unavailable)|temporarily unavailable|please try again later/i.test(text);
}
function isCodexTransientConnectionError(raw) {
const text = String(raw || '');
if (!text) return false;
return /reconnecting(?:\.\.\.)?\s*\d+\/\d+|connection\s+(?:lost|closed|reset|refused|interrupted)|disconnect(?:ed|ion)|ECONNRESET|ECONNREFUSED|EPIPE|ETIMEDOUT|ENOTFOUND|fetch failed|network.*(?:error|failed)|TLS connection.*terminated/i.test(text);
}
function isCodexTransientRetryableError(raw) {
return isCodexTransientCapacityError(raw) || isCodexTransientConnectionError(raw);
}
function hasRuntimeOutput(entry) {
if ((entry.fullText || '').trim()) return true;
if (Array.isArray(entry.contentBlocks) && entry.contentBlocks.length > 0) return true;
return Array.isArray(entry.toolCalls) && entry.toolCalls.length > 0;
}
function getCodexRetryConfig() {
return normalizeCodexRetryConfig(loadCodexConfig().retry);
}
function codexTransientRetryDelayMs(config) {
return Math.max(0, (config?.intervalSeconds || DEFAULT_CODEX_CONFIG.retry.intervalSeconds) * 1000);
}
function cancelCodexCapacityRetry(sessionId) {
const retry = pendingCodexCapacityRetries.get(sessionId);
if (!retry) return false;
if (retry.timer) clearTimeout(retry.timer);
pendingCodexCapacityRetries.delete(sessionId);
return true;
}
function shouldRetryCodexTransientFailure(entry, rawError, context = {}) {
if (!['codex', 'codexapp'].includes(entry.agent || 'claude')) return false;
if (!rawError || !isCodexTransientRetryableError(rawError)) return false;
if (getCodexRetryConfig().mode === 'off') return false;
if (context.contextLimitExceeded || context.pendingSlash) return false;
if (entry.crossConversationReplyRequestId) return false;
if ((entry.agent || 'claude') !== 'codexapp' && hasRuntimeOutput(entry)) return false;
return !!(entry.retryRequest?.text || entry.retryRequest?.runtimeText);
}
function scheduleCodexCapacityRetry(sessionId, entry, rawError) {
const retryRequest = entry.retryRequest || {};
const previous = pendingCodexCapacityRetries.get(sessionId) || null;
const retryConfig = getCodexRetryConfig();
if (retryConfig.mode === 'off') {
cancelCodexCapacityRetry(sessionId);
return false;
}
const attempts = (previous?.attempts || 0) + 1;
if (retryConfig.mode === 'limited' && attempts > retryConfig.maxAttempts) {
cancelCodexCapacityRetry(sessionId);
return false;
}
const delayMs = codexTransientRetryDelayMs(retryConfig);
if (previous?.timer) clearTimeout(previous.timer);
const retry = {
text: retryRequest.text || retryRequest.runtimeText || '',
runtimeText: retryRequest.runtimeText || retryRequest.text || '',
mode: retryRequest.mode || 'yolo',
agent: retryRequest.agent || entry.agent || 'codex',
attachments: Array.isArray(retryRequest.attachments) ? retryRequest.attachments : [],
mcpContext: retryRequest.mcpContext || {},
attempts,
retryMode: retryConfig.mode,
timer: null,
ws: entry.ws || null,
};
retry.timer = setTimeout(() => {
const latest = pendingCodexCapacityRetries.get(sessionId);
if (!latest || latest.timer !== retry.timer) return;
latest.timer = null;
const session = loadSession(sessionId);
if (!session) {
pendingCodexCapacityRetries.delete(sessionId);
return;
}
if (activeProcesses.has(sessionId) || activeCodexAppTurns.has(sessionId)) {
plog('WARN', 'codex_capacity_retry_skipped_busy', {
sessionId: sessionId.slice(0, 8),
attempt: latest.attempts,
});
return;
}
const ws = latest.ws && latest.ws.readyState === 1 ? latest.ws : null;
plog('INFO', 'codex_capacity_retry_start', {
sessionId: sessionId.slice(0, 8),
attempt: latest.attempts,
});
handleMessage(ws, {
type: 'message',
text: latest.text,
sessionId,
mode: latest.mode,
agent: latest.agent || 'codex',
attachments: latest.attachments,
}, {
hideInHistory: true,
runtimeText: latest.runtimeText,
mcpContext: latest.mcpContext,
skipPendingCrossConversationFlush: true,
});
}, delayMs);
pendingCodexCapacityRetries.set(sessionId, retry);
plog('WARN', 'codex_capacity_retry_scheduled', {
sessionId: sessionId.slice(0, 8),
attempt: attempts,
maxAttempts: retryConfig.mode === 'limited' ? retryConfig.maxAttempts : null,
retryMode: retryConfig.mode,
delayMs,
error: String(rawError || '').slice(0, 300),
});
if (entry.ws) {
const attemptText = retryConfig.mode === 'forever'
? `${attempts}`
: `${attempts}/${retryConfig.maxAttempts}`;
wsSend(entry.ws, {
type: 'system_message',
sessionId,
message: `Codex 服务暂时繁忙,${retryConfig.intervalSeconds} 秒后自动重试(${attemptText})。`,
});
}
return true;
}
function handleProcessComplete(sessionId, exitCode, signal) {
const entry = activeProcesses.get(sessionId);
if (!entry) return;
@@ -4699,6 +4870,19 @@ function handleProcessComplete(sessionId, exitCode, signal) {
// Save result to session
const session = loadSession(sessionId);
if (session && shouldRetryCodexTransientFailure(entry, rawCompletionError, { contextLimitExceeded, pendingSlash })) {
activeProcesses.delete(sessionId);
cleanRunDir(sessionId);
pendingSlashCommands.delete(sessionId);
if (scheduleCodexCapacityRetry(sessionId, entry, rawCompletionError)) {
return;
}
}
if (!shouldRetryCodexTransientFailure(entry, rawCompletionError, { contextLimitExceeded, pendingSlash })) {
cancelCodexCapacityRetry(sessionId);
}
if (session && (entry.fullText || entry.contentBlocks)) {
session.messages.push({
role: 'assistant',
@@ -5468,6 +5652,7 @@ function handleSaveCodexConfig(ws, newConfig) {
});
}
const requestedSearch = !!newConfig.enableSearch;
const retry = normalizeCodexRetryConfig(newConfig.retry);
const merged = {
mode: newConfig.mode === 'custom' ? 'custom' : 'local',
activeProfile: String(newConfig.activeProfile || '').trim(),
@@ -5475,6 +5660,7 @@ function handleSaveCodexConfig(ws, newConfig) {
enableSearch: false,
supportsSearch: false,
storedEnableSearch: requestedSearch,
retry,
};
if (merged.mode === 'custom' && merged.profiles.length > 0 && !merged.profiles.some((profile) => profile.name === merged.activeProfile)) {
merged.activeProfile = merged.profiles[0].name;
@@ -5486,6 +5672,9 @@ function handleSaveCodexConfig(ws, newConfig) {
profileCount: merged.profiles.length,
enableSearchRequested: requestedSearch,
enableSearchEffective: false,
retryMode: retry.mode,
retryIntervalSeconds: retry.intervalSeconds,
retryMaxAttempts: retry.mode === 'limited' ? retry.maxAttempts : null,
});
wsSend(ws, { type: 'codex_config', config: getCodexConfigMasked() });
wsSend(ws, {
@@ -5740,6 +5929,7 @@ function handleSlashCommand(ws, text, sessionId, fallbackAgent) {
switch (cmd) {
case '/clear': {
if (session) {
cancelCodexCapacityRetry(sessionId);
if (activeProcesses.has(sessionId)) {
const entry = activeProcesses.get(sessionId);
killProcess(entry.pid);
@@ -5951,6 +6141,56 @@ function resolveConversationMode(rawMode, fallbackMode = 'yolo', options = {}) {
return { ok: true, mode: VALID_PERMISSION_MODES.has(fallbackMode) ? fallbackMode : 'yolo' };
}
function resolveConversationModel(rawModel, agent, sourceSession = null) {
const value = String(rawModel || '').trim();
if (value) {
if (agent === 'codex' || agent === 'codexapp') return value;
return MODEL_MAP[value.toLowerCase()] || value;
}
if (sourceSession?.model) return sourceSession.model;
return agent === 'codex' || agent === 'codexapp' ? getDefaultCodexModel() : MODEL_MAP.opus;
}
function buildBranchSessionTitle(sourceSession) {
const sourceTitle = normalizeConversationTitle(sourceSession?.title, '新会话');
return normalizeConversationTitle(`${sourceTitle} 的分支`);
}
function resolveBranchSource(args = {}) {
const sourceSessionId = sanitizeId(args.branchSourceSessionId || args.sourceSessionId || '');
if (!sourceSessionId) return { ok: true, sourceSession: null };
const sourceSession = loadSession(sourceSessionId);
if (!sourceSession) {
return mcpToolError('branch_source_not_found', '来源会话不存在,无法创建分支。', { sourceSessionId });
}
const sourceMessages = Array.isArray(sourceSession.messages) ? sourceSession.messages : [];
if (sourceMessages.length === 0) {
return mcpToolError('branch_source_empty', '来源会话没有可复制的上下文。', { sourceSessionId });
}
const parsedIndex = Number.parseInt(String(args.branchMessageIndex ?? ''), 10);
const sourceMessageIndex = Number.isFinite(parsedIndex)
? Math.max(0, Math.min(sourceMessages.length - 1, parsedIndex))
: sourceMessages.length - 1;
const createdAt = new Date().toISOString();
return {
ok: true,
sourceSession,
initialMessages: sourceMessages.slice(0, sourceMessageIndex + 1),
createdFrom: {
kind: 'branch',
sourceSessionId: sourceSession.id,
sourceTitle: sourceSession.title || 'Untitled',
sourceMessageIndex,
createdAt,
},
defaultTitle: buildBranchSessionTitle(sourceSession),
};
}
function createPersistentConversationSession(args = {}, options = {}) {
const sourceSession = options.sourceSession || null;
const strict = !!options.strict;
@@ -5981,6 +6221,9 @@ function createPersistentConversationSession(args = {}, options = {}) {
const now = new Date().toISOString();
const agent = agentResult.agent;
const initialMessages = Array.isArray(options.initialMessages)
? sanitizeMessagesForPersist(options.initialMessages)
: [];
const session = {
id: crypto.randomUUID(),
title: normalizeConversationTitle(args.title),
@@ -5991,12 +6234,16 @@ function createPersistentConversationSession(args = {}, options = {}) {
claudeSessionId: null,
codexThreadId: null,
codexAppThreadId: null,
// Codex/Codex App 读取 ~/.codex/config.toml 默认模型Claude 继续默认 opus 1M
model: agent === 'codex' || agent === 'codexapp' ? getDefaultCodexModel() : MODEL_MAP.opus,
// Codex/Codex App 默认读取 ~/.codex/config.toml;分支会话优先继承来源模型
model: resolveConversationModel(
args.model,
agent,
options.inheritSourceModel === true ? sourceSession : null,
),
permissionMode: modeResult.mode,
totalCost: 0,
totalUsage: { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
messages: [],
messages: initialMessages,
cwd: cwdResult.path || getDefaultSessionCwd(),
};
if (options.createdFrom) session.createdFrom = options.createdFrom;
@@ -6009,10 +6256,11 @@ function createPersistentConversationSession(args = {}, options = {}) {
function buildSessionInfoPayload(session) {
const waitState = crossConversationWaitState(session.id);
const messages = session.messages || [];
return {
type: 'session_info',
sessionId: session.id,
messages: session.messages || [],
messages,
title: session.title,
pinnedAt: session.pinnedAt || null,
mode: session.permissionMode || 'yolo',
@@ -6022,6 +6270,8 @@ function buildSessionInfoPayload(session) {
totalCost: session.totalCost || 0,
totalUsage: session.totalUsage || { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
updated: session.updated,
historyTotal: messages.length,
historyBaseIndex: 0,
hasUnread: false,
historyPending: false,
isRunning: false,
@@ -6040,10 +6290,29 @@ function attachClientRequestId(payload, source = {}) {
}
function handleNewSession(ws, msg) {
const result = createPersistentConversationSession(msg || {}, {
defaultAgent: normalizeAgent(msg?.agent),
const request = msg || {};
const branch = resolveBranchSource(request);
if (!branch.ok) {
return wsSend(ws, {
type: 'error',
code: branch.code,
message: branch.message,
});
}
const createArgs = { ...request };
if (branch.defaultTitle && !String(createArgs.title || '').trim()) {
createArgs.title = branch.defaultTitle;
}
const result = createPersistentConversationSession(createArgs, {
defaultAgent: normalizeAgent(request.agent),
defaultMode: 'yolo',
allowCreateCwd: true,
sourceSession: branch.sourceSession || null,
initialMessages: branch.initialMessages || null,
createdFrom: branch.createdFrom || null,
inheritSourceModel: !!branch.sourceSession,
});
if (!result.ok) {
return wsSend(ws, {
@@ -6079,6 +6348,7 @@ function handleLoadHistoryPage(ws, msg = {}) {
messages: list.slice(start, end),
remaining: 0,
historyCursor: start,
historyBaseIndex: start,
historyTruncated: start > 0,
});
}
@@ -6132,6 +6402,7 @@ function handleLoadSession(ws, msg) {
historyTotal: refreshedSession.messages.length,
historyBuffered,
historyCursor: historyRemaining,
historyBaseIndex: Math.max(0, refreshedSession.messages.length - recentMessages.length),
historyTruncated: historyRemaining > 0,
historyPending: olderChunks.length > 0,
updated: refreshedSession.updated,
@@ -6145,15 +6416,19 @@ function handleLoadSession(ws, msg) {
}, msg));
if (olderChunks.length > 0) {
let chunkEnd = Math.max(0, refreshedSession.messages.length - recentMessages.length);
olderChunks.forEach((chunk, index) => {
const chunkStart = Math.max(0, chunkEnd - chunk.length);
wsSend(ws, {
type: 'session_history_chunk',
sessionId: refreshedSession.id,
messages: chunk,
remaining: Math.max(0, olderChunks.length - index - 1),
historyCursor: index === olderChunks.length - 1 ? historyRemaining : null,
historyBaseIndex: chunkStart,
historyTruncated: historyRemaining > 0,
});
chunkEnd = chunkStart;
});
}
@@ -6256,6 +6531,7 @@ function deleteCodexLocalSession(session) {
function handleDeleteSession(ws, sessionId) {
pendingSlashCommands.delete(sessionId);
pendingCompactRetries.delete(sessionId);
cancelCodexCapacityRetry(sessionId);
deleteCrossConversationRepliesForSession(sessionId);
for (const [threadId, child] of ccwebMcpChildThreads.entries()) {
if (child.parentSessionId === sessionId) ccwebMcpChildThreads.delete(threadId);
@@ -6376,9 +6652,17 @@ function handleAbort(ws) {
if (!sessionId) return;
if (handleCodexAppAbortSession(sessionId, ws)) return;
const entry = activeProcesses.get(sessionId);
if (!entry) return;
if (!entry) {
if (cancelCodexCapacityRetry(sessionId)) {
wsSend(ws, { type: 'system_message', sessionId, message: '已取消 Codex 自动重试。' });
wsSend(ws, { type: 'done', sessionId });
sendSessionList(ws);
}
return;
}
plog('INFO', 'user_abort', { sessionId: sessionId.slice(0, 8), pid: entry.pid });
cancelCodexCapacityRetry(sessionId);
killProcess(entry.pid);
setTimeout(() => {
killProcess(entry.pid, true);
@@ -6465,6 +6749,9 @@ function handleMessage(ws, msg, options = {}) {
if (!normalizedText && resolvedAttachments.length === 0) {
return fail('empty_message', '消息内容不能为空。');
}
if (sessionId && !hideInHistory) {
cancelCodexCapacityRetry(sessionId);
}
const savedAttachments = resolvedAttachments.map((attachment) => ({
id: attachment.id,
@@ -6695,6 +6982,13 @@ function handleMessage(ws, msg, options = {}) {
lastError: null,
errorSent: false,
crossConversationReplyRequestId: options.crossConversation?.replyRequestId || null,
retryRequest: {
text: textValue,
runtimeText: runtimeTextValue,
mode: session.permissionMode || 'yolo',
attachments: savedAttachments,
mcpContext: options.mcpContext || {},
},
tailer: null,
};
activeProcesses.set(currentSessionId, entry);
@@ -7666,6 +7960,14 @@ function handleCodexAppServerRequest(request) {
}
function handleCodexAppServerExit(signature, info = {}) {
if (signature && signature !== codexAppClientSignature) {
plog('INFO', 'codex_app_server_exit_stale', {
code: info.code ?? null,
signal: info.signal || null,
activeTurns: activeCodexAppTurns.size,
});
return;
}
if (signature && signature === codexAppClientSignature) {
codexAppClient = null;
codexAppClientSignature = '';
@@ -7821,13 +8123,27 @@ function buildCodexAppClientSpec() {
};
}
function getCodexAppClient() {
function getCodexAppClient(options = {}) {
const spec = buildCodexAppClientSpec();
if (spec?.error) return { error: spec.error };
if (codexAppClient && codexAppClientSignature !== spec.signature) {
if (activeCodexAppTurns.size > 0) {
return { error: 'Codex App 配置已变更,但仍有运行中的任务。请等待任务结束后再发送新消息。' };
const excludeSessionId = sanitizeId(options.excludeSessionId || '');
const blockingSessionIds = Array.from(activeCodexAppTurns.keys())
.filter((sessionId) => !excludeSessionId || sessionId !== excludeSessionId);
if (blockingSessionIds.length > 0) {
if (codexAppClient.isRunning()) {
plog('WARN', 'codex_app_config_changed_reusing_active_client', {
activeTurns: activeCodexAppTurns.size,
blockingTurns: blockingSessionIds.length,
excludeSessionId: excludeSessionId ? excludeSessionId.slice(0, 8) : null,
});
return { client: codexAppClient, staleConfig: true };
}
const message = 'Codex App 配置已变更,旧 app-server 已不可用,已结束残留运行任务。请重试。';
for (const sessionId of blockingSessionIds) {
handleCodexAppTurnFailure(sessionId, new Error(message));
}
}
codexAppClient.stop();
codexAppClient = null;
@@ -7920,6 +8236,16 @@ function handleCodexAppMessage(ws, session, runtimeTextValue, resolvedAttachment
return { ok: false, code: 'empty_message', message: '消息内容不能为空。' };
}
const retryAttachments = resolvedAttachments.map((attachment) => ({
id: attachment.id,
kind: 'image',
filename: attachment.filename,
mime: attachment.mime,
size: attachment.size,
createdAt: attachment.createdAt,
expiresAt: attachment.expiresAt,
storageState: attachment.storageState,
}));
const entry = {
ws,
agent: 'codexapp',
@@ -7935,6 +8261,14 @@ function handleCodexAppMessage(ws, session, runtimeTextValue, resolvedAttachment
lastError: null,
errorSent: false,
crossConversationReplyRequestId: options.crossConversation?.replyRequestId || null,
retryRequest: {
text: runtimeTextValue,
runtimeText: runtimeTextValue,
mode: session.permissionMode || 'yolo',
agent: 'codexapp',
attachments: retryAttachments,
mcpContext: options.mcpContext || {},
},
clientUserMessageId: crypto.randomUUID(),
startedAt: new Date().toISOString(),
};
@@ -7954,7 +8288,7 @@ async function startCodexAppTurn(sessionId, input) {
const entry = activeCodexAppTurns.get(sessionId);
if (!session || !entry) return;
const clientResult = getCodexAppClient();
const clientResult = getCodexAppClient({ excludeSessionId: sessionId });
if (clientResult.error) throw new Error(clientResult.error);
const client = clientResult.client;
await client.start();
@@ -8002,6 +8336,18 @@ function handleCodexAppTurnComplete(sessionId, options = {}) {
toolResultMaxChars: SESSION_TOOL_RESULT_MAX_CHARS,
contentMaxChars: SESSION_MESSAGE_CONTENT_MAX_CHARS,
});
if (session && shouldRetryCodexTransientFailure(entry, rawError)) {
activeCodexAppTurns.delete(sessionId);
cleanupCodexAppTurnState(sessionId, entry);
if (scheduleCodexCapacityRetry(sessionId, entry, rawError)) {
sendSessionList(entry.ws);
return;
}
} else {
cancelCodexCapacityRetry(sessionId);
}
if (session && (assistantContent.trim() || assistantToolCalls.length > 0) && !hasCodexAppTurnMessage(session, turnKey)) {
session.messages.push({
role: 'assistant',