chore: rebuild release package and commit updates

This commit is contained in:
shiyue
2026-07-13 10:13:04 +08:00
parent dd466a69b5
commit 141a266f34
19 changed files with 1066 additions and 35 deletions

200
server.js
View File

@@ -672,6 +672,14 @@ const activeCodexAppGoalCommands = new Map();
// ccweb MCP child agents tracked from Codex App native collaboration mode:
// childThreadId -> { parentSessionId, parentThreadId, spawnToolId, ...state }
const ccwebMcpChildThreads = new Map();
const codexAppThreadSessionIndex = new Map();
const codexAppSessionThreadIndex = new Map();
const codexAppUnknownThreadMisses = new Map();
const codexAppUnroutedNotificationLogTimes = new Map();
const CODEX_APP_UNKNOWN_THREAD_CACHE_TTL_MS = 30 * 1000;
const CODEX_APP_UNKNOWN_THREAD_CACHE_MAX = 1000;
const CODEX_APP_UNROUTED_NOTIFICATION_LOG_THROTTLE_MS = 30 * 1000;
const CODEX_APP_UNROUTED_NOTIFICATION_LOG_MAX = 1000;
const CODEX_APP_MCP_STARTUP_STATUS_METHOD = 'mcpServer/startupStatus/updated';
const CODEX_APP_MCP_DEFAULT_SERVER = 'ccweb';
const CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS = 1200;
@@ -2954,6 +2962,67 @@ function getRuntimeSessionId(session) {
return session.claudeSessionId || null;
}
function normalizeCodexAppThreadId(value) {
const text = String(value || '').trim();
return text || null;
}
function pruneOldestMapEntries(map, maxSize) {
if (!map || map.size <= maxSize) return;
for (const key of map.keys()) {
map.delete(key);
if (map.size <= maxSize) return;
}
}
function removeSessionRuntimeThreadIndex(sessionId) {
const normalizedId = sanitizeId(sessionId || '');
if (!normalizedId) return;
const previousThreadId = codexAppSessionThreadIndex.get(normalizedId);
if (previousThreadId) {
codexAppThreadSessionIndex.delete(previousThreadId);
codexAppUnknownThreadMisses.delete(previousThreadId);
}
codexAppSessionThreadIndex.delete(normalizedId);
for (const [threadId, indexedSessionId] of codexAppThreadSessionIndex.entries()) {
if (indexedSessionId === normalizedId) codexAppThreadSessionIndex.delete(threadId);
}
}
function updateSessionRuntimeThreadIndex(session) {
const sessionId = sanitizeId(session?.id || '');
if (!sessionId) return;
const previousThreadId = codexAppSessionThreadIndex.get(sessionId);
if (previousThreadId) {
codexAppThreadSessionIndex.delete(previousThreadId);
codexAppSessionThreadIndex.delete(sessionId);
}
const threadId = isCodexAppSession(session) ? normalizeCodexAppThreadId(getRuntimeSessionId(session)) : null;
if (!threadId) return;
codexAppThreadSessionIndex.set(threadId, sessionId);
codexAppSessionThreadIndex.set(sessionId, threadId);
codexAppUnknownThreadMisses.delete(threadId);
}
function hasFreshCodexAppUnknownThreadMiss(threadId) {
const targetThreadId = normalizeCodexAppThreadId(threadId);
if (!targetThreadId) return false;
const expiresAt = codexAppUnknownThreadMisses.get(targetThreadId) || 0;
if (!expiresAt) return false;
if (expiresAt <= Date.now()) {
codexAppUnknownThreadMisses.delete(targetThreadId);
return false;
}
return true;
}
function rememberCodexAppUnknownThreadMiss(threadId) {
const targetThreadId = normalizeCodexAppThreadId(threadId);
if (!targetThreadId) return;
codexAppUnknownThreadMisses.set(targetThreadId, Date.now() + CODEX_APP_UNKNOWN_THREAD_CACHE_TTL_MS);
pruneOldestMapEntries(codexAppUnknownThreadMisses, CODEX_APP_UNKNOWN_THREAD_CACHE_MAX);
}
function mcpStatusObject(value) {
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
}
@@ -3762,7 +3831,9 @@ function loadSession(id) {
try {
const filePath = sessionPath(normalizedId);
if (!fs.existsSync(filePath)) return null;
return normalizeSession(safeReadSessionJson(filePath, SESSION_LOAD_MAX_BYTES, { sessionId: normalizedId }));
const session = normalizeSession(safeReadSessionJson(filePath, SESSION_LOAD_MAX_BYTES, { sessionId: normalizedId }));
updateSessionRuntimeThreadIndex(session);
return session;
} catch (err) {
plog('WARN', 'session_load_failed', {
sessionId: normalizedId.slice(0, 8),
@@ -3788,6 +3859,7 @@ function saveSession(session) {
attempts: result.attempts,
});
}
updateSessionRuntimeThreadIndex(session);
return true;
} catch (err) {
plog('ERROR', 'session_save_failed', {
@@ -4310,6 +4382,7 @@ function recoverCodexAppTurnState(sessionId) {
toolResultMaxChars: SESSION_TOOL_RESULT_MAX_CHARS,
contentMaxChars: CODEX_APP_STATE_FULL_TEXT_MAX_CHARS,
});
recoverCcwebMcpChildThreadsFromPersistedToolCalls(sessionId, state, toolCalls);
const hasRecoverableContent = fullText.trim() || toolCalls.length > 0;
const turnKey = codexAppTurnKey(sessionId, state);
let changed = false;
@@ -7710,6 +7783,7 @@ function handleDeleteSession(ws, sessionId) {
pendingSlashCommands.delete(sessionId);
pendingCompactRetries.delete(sessionId);
cancelCodexCapacityRetry(sessionId);
removeSessionRuntimeThreadIndex(sessionId);
if (activeCodexAppGoalCommands.has(sessionId)) {
const entry = activeCodexAppGoalCommands.get(sessionId);
entry.cancelled = true;
@@ -8344,6 +8418,24 @@ function findCodexAppEntryByRuntime(params = {}) {
function findCodexAppSessionByThreadId(threadId) {
const targetThreadId = String(threadId || '').trim();
if (!targetThreadId) return null;
const cachedSessionId = codexAppThreadSessionIndex.get(targetThreadId);
if (cachedSessionId) {
const session = loadSession(cachedSessionId);
if (session && isCodexAppSession(session) && getRuntimeSessionId(session) === targetThreadId) {
return { sessionId: session.id, session };
}
removeSessionRuntimeThreadIndex(cachedSessionId);
}
// 通过 codexAppUnknownThreadMisses 限制未知线程重复磁盘查找。
if (hasFreshCodexAppUnknownThreadMiss(targetThreadId)) return null;
const matched = scanCodexAppSessionByThreadId(targetThreadId);
if (matched) return matched;
rememberCodexAppUnknownThreadMiss(targetThreadId);
return null;
}
function scanCodexAppSessionByThreadId(targetThreadId) {
try {
for (const file of fs.readdirSync(SESSIONS_DIR)) {
if (!file.endsWith('.json')) continue;
@@ -8511,6 +8603,83 @@ function ccwebMcpChildLabel(state = {}, fallbackThreadId = '') {
return String(label || fallbackThreadId || '子代理').trim();
}
function ccwebMcpRecoveredChildLabel(input = {}, fallbackThreadId = '') {
const agentPath = String(input.agentPath || input.agent_path || '').trim();
const fromPath = agentPath ? path.basename(agentPath) : '';
return String(input.label || input.title || input.nickname || input.name || input.agent || fromPath || fallbackThreadId || '子代理').trim();
}
function ccwebMcpRecoveredChildStatus(input = {}, tool = {}) {
const direct = input.status || input.state || tool.status || '';
if (direct) return ccwebMcpChildStatus(direct, 'running');
const kind = String(input.kind || tool.kind || '').trim().toLowerCase();
if (/fail|error/.test(kind)) return 'failed';
if (/close|closed|cancel|abort|interrupt/.test(kind)) return 'closed';
if (/return|complete|done|finish|success/.test(kind)) return 'returned';
return 'running';
}
function isSubAgentActivityTool(tool = {}, input = {}) {
return tool.name === 'subAgentActivity'
|| tool.kind === 'subAgentActivity'
|| input.type === 'subAgentActivity'
|| input.activityType === 'subAgentActivity';
}
function recoverCcwebMcpChildThreadsFromPersistedToolCalls(sessionId, state = {}, toolCalls = []) {
const parentSessionId = sanitizeId(sessionId || '');
if (!parentSessionId || !Array.isArray(toolCalls) || toolCalls.length === 0) return 0;
const parentThreadId = normalizeCodexAppThreadId(state.threadId) || '';
const now = new Date().toISOString();
let restored = 0;
for (const tool of toolCalls) {
const input = parseMaybeJsonObject(tool?.input) || (tool?.input && typeof tool.input === 'object' ? tool.input : {});
if (!isSubAgentActivityTool(tool, input)) continue;
const threadId = normalizeCodexAppThreadId(
input.agentThreadId || input.agent_thread_id || input.threadId || input.thread_id
);
if (!threadId) continue;
const existing = ccwebMcpChildThreads.get(threadId);
const child = existing || {
threadId,
turnId: null,
parentSessionId,
parentThreadId,
spawnToolId: tool.id || '',
label: ccwebMcpRecoveredChildLabel(input, threadId),
role: String(input.role || input.agentRole || input.agent_role || '').trim(),
lastAssistantMessage: '',
candidateResult: '',
finalMessage: '',
status: 'running',
summaryAttempts: 0,
createdAt: now,
updatedAt: now,
};
child.parentSessionId = child.parentSessionId || parentSessionId;
child.parentThreadId = child.parentThreadId || parentThreadId;
child.spawnToolId = child.spawnToolId || tool.id || '';
child.label = ccwebMcpRecoveredChildLabel(input, child.label || threadId);
child.role = String(input.role || input.agentRole || input.agent_role || child.role || '').trim();
if (child.status !== 'closed') child.status = ccwebMcpRecoveredChildStatus(input, tool);
child.updatedAt = child.updatedAt || now;
child.recoveredFromState = true;
ccwebMcpChildThreads.set(threadId, child);
restored += existing ? 0 : 1;
}
if (restored > 0) {
plog('INFO', 'ccweb_mcp_child_threads_recovered', {
sessionId: parentSessionId.slice(0, 8),
parentThreadId: parentThreadId || null,
restored,
});
}
return restored;
}
function ccwebMcpChildSummary(child = {}) {
const candidate = String(child.candidateResult || child.finalMessage || child.lastAssistantMessage || '').replace(/\s+/g, ' ').trim();
return candidate ? truncateTextValue(candidate, 180, '...') : '';
@@ -8755,8 +8924,6 @@ function processCcwebMcpChildNotification(child, notification) {
function findCodexAppRouteByRuntime(params = {}, method = '') {
const parent = findCodexAppEntryByRuntime(params);
if (parent) return { ...parent, role: 'parent' };
const recoveredParent = adoptCodexAppUnroutedTurn(params, method);
if (recoveredParent) return { ...recoveredParent, role: 'parent' };
const threadId = codexAppRuntimeThreadId(params);
if (threadId && ccwebMcpChildThreads.has(threadId)) {
const child = ccwebMcpChildThreads.get(threadId);
@@ -8767,18 +8934,35 @@ function findCodexAppRouteByRuntime(params = {}, method = '') {
child,
};
}
const recoveredParent = adoptCodexAppUnroutedTurn(params, method);
if (recoveredParent) return { ...recoveredParent, role: 'parent' };
return null;
}
function shouldLogCodexAppUnroutedNotification(notification) {
const method = String(notification?.method || '').trim() || 'unknown';
const params = notification?.params || {};
const threadId = normalizeCodexAppThreadId(params.threadId || params.thread?.id) || 'unknown';
const key = `${threadId}:${method}`;
const now = Date.now();
const lastLoggedAt = codexAppUnroutedNotificationLogTimes.get(key) || 0;
if (lastLoggedAt && now - lastLoggedAt < CODEX_APP_UNROUTED_NOTIFICATION_LOG_THROTTLE_MS) return false;
codexAppUnroutedNotificationLogTimes.set(key, now);
pruneOldestMapEntries(codexAppUnroutedNotificationLogTimes, CODEX_APP_UNROUTED_NOTIFICATION_LOG_MAX);
return true;
}
function handleCodexAppNotification(notification) {
const routed = findCodexAppRouteByRuntime(notification?.params || {}, notification?.method || '');
if (handleCodexAppMcpStartupStatusNotification(notification, routed)) return;
if (!routed) {
plog('INFO', 'codex_app_notification_unrouted', {
method: notification?.method || '',
threadId: notification?.params?.threadId || notification?.params?.thread?.id || null,
turnId: notification?.params?.turnId || notification?.params?.turn?.id || null,
});
if (shouldLogCodexAppUnroutedNotification(notification)) {
plog('INFO', 'codex_app_notification_unrouted', {
method: notification?.method || '',
threadId: notification?.params?.threadId || notification?.params?.thread?.id || null,
turnId: notification?.params?.turnId || notification?.params?.turn?.id || null,
});
}
return;
}