chore: rebuild release package and commit updates
This commit is contained in:
200
server.js
200
server.js
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user