fix: stabilize session list and Codex imports
This commit is contained in:
115
server.js
115
server.js
@@ -698,6 +698,10 @@ let codexAppClient = null;
|
||||
let codexAppClientSignature = '';
|
||||
const CODEX_APP_STATE_FILE = 'codexapp-state.json';
|
||||
const CODEX_APP_STATE_FLUSH_DELAY_MS = 250;
|
||||
const CCWEB_MCP_CHILD_UPDATE_FLUSH_DELAY_MS = CODEX_APP_STATE_FLUSH_DELAY_MS;
|
||||
// 同一父会话的 child 增量共用一个短周期待保存快照,避免每条增量都读写整份会话。
|
||||
const pendingCcwebMcpChildSessionFlushes = new Map();
|
||||
let ccwebMcpChildSessionListBroadcastTimer = null;
|
||||
|
||||
// Track which session each ws is viewing: ws -> sessionId
|
||||
const wsSessionMap = new Map();
|
||||
@@ -8917,10 +8921,12 @@ function mergeCcwebMcpChildIntoTool(tool, child) {
|
||||
};
|
||||
}
|
||||
|
||||
function updateCcwebMcpChildToolState(sessionId, child) {
|
||||
function updateCcwebMcpChildToolState(sessionId, child, persistedTool = null) {
|
||||
const entry = activeCodexAppTurns.get(sessionId) || null;
|
||||
let tool = findCcwebMcpChildTargetToolInToolCalls(entry?.toolCalls, child.spawnToolId);
|
||||
|
||||
// 持久化快照已经完成合并时直接复用,避免没有活动 turn 时再次加载父会话。
|
||||
if (!tool && persistedTool) return persistedTool;
|
||||
if (!tool) {
|
||||
const session = loadSession(sessionId);
|
||||
const messages = Array.isArray(session?.messages) ? session.messages : [];
|
||||
@@ -8929,20 +8935,105 @@ function updateCcwebMcpChildToolState(sessionId, child) {
|
||||
return mergeCcwebMcpChildIntoTool(tool, child);
|
||||
}
|
||||
|
||||
function isFinalCcwebMcpChildStatus(status) {
|
||||
return status === 'returned' || status === 'failed' || status === 'interrupted' || status === 'closed';
|
||||
}
|
||||
|
||||
function snapshotCcwebMcpChildForPersist(child = {}) {
|
||||
return {
|
||||
...child,
|
||||
planProgress: child.planProgress && typeof child.planProgress === 'object'
|
||||
? { ...child.planProgress }
|
||||
: child.planProgress || null,
|
||||
};
|
||||
}
|
||||
|
||||
function flushPendingCcwebMcpChildSession(sessionId) {
|
||||
const pending = pendingCcwebMcpChildSessionFlushes.get(sessionId);
|
||||
if (!pending) return false;
|
||||
if (pending.timer) clearTimeout(pending.timer);
|
||||
pendingCcwebMcpChildSessionFlushes.delete(sessionId);
|
||||
|
||||
// 尾随冲刷前重新读取最新会话,避免覆盖这 250ms 内由父 turn 完成等路径写入的数据。
|
||||
const session = loadSession(sessionId) || pending.session;
|
||||
if (!session || !Array.isArray(session.messages)) return false;
|
||||
let merged = false;
|
||||
for (const child of pending.children.values()) {
|
||||
const targetTool = findCcwebMcpChildTargetToolInMessages(session.messages, child.spawnToolId);
|
||||
if (mergeCcwebMcpChildIntoTool(targetTool, child)) merged = true;
|
||||
}
|
||||
if (!merged) return false;
|
||||
|
||||
if (!session.updated || pending.updated > session.updated) session.updated = pending.updated;
|
||||
if (pending.markUnread) session.hasUnread = true;
|
||||
return saveSession(session);
|
||||
}
|
||||
|
||||
function updatePersistedCcwebMcpChildTool(sessionId, child) {
|
||||
const session = loadSession(sessionId);
|
||||
if (!session || !Array.isArray(session.messages)) return null;
|
||||
const targetTool = findCcwebMcpChildTargetToolInMessages(session.messages, child.spawnToolId);
|
||||
if (!mergeCcwebMcpChildIntoTool(targetTool, child)) return null;
|
||||
session.updated = new Date().toISOString();
|
||||
if (!findViewingSessionWs(sessionId)) session.hasUnread = true;
|
||||
saveSession(session);
|
||||
let pending = pendingCcwebMcpChildSessionFlushes.get(sessionId) || null;
|
||||
const createdPending = !pending;
|
||||
if (!pending) {
|
||||
const session = loadSession(sessionId);
|
||||
if (!session || !Array.isArray(session.messages)) return null;
|
||||
pending = {
|
||||
session,
|
||||
children: new Map(),
|
||||
updated: session.updated || '',
|
||||
markUnread: false,
|
||||
timer: null,
|
||||
};
|
||||
pendingCcwebMcpChildSessionFlushes.set(sessionId, pending);
|
||||
}
|
||||
|
||||
const targetTool = findCcwebMcpChildTargetToolInMessages(pending.session.messages, child.spawnToolId);
|
||||
if (!mergeCcwebMcpChildIntoTool(targetTool, child)) {
|
||||
if (createdPending) pendingCcwebMcpChildSessionFlushes.delete(sessionId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 同一可见协作工具下可能挂多个 sibling/nested child,必须按线程分别保留最新状态。
|
||||
const childKey = String(child.threadId || child.spawnToolId || '').trim();
|
||||
pending.children.set(childKey, snapshotCcwebMcpChildForPersist(child));
|
||||
pending.updated = new Date().toISOString();
|
||||
if (!findViewingSessionWs(sessionId)) pending.markUnread = true;
|
||||
|
||||
if (isFinalCcwebMcpChildStatus(child.status)) {
|
||||
flushPendingCcwebMcpChildSession(sessionId);
|
||||
} else if (!pending.timer) {
|
||||
pending.timer = setTimeout(() => {
|
||||
flushPendingCcwebMcpChildSession(sessionId);
|
||||
}, CCWEB_MCP_CHILD_UPDATE_FLUSH_DELAY_MS);
|
||||
if (typeof pending.timer.unref === 'function') pending.timer.unref();
|
||||
}
|
||||
return targetTool;
|
||||
}
|
||||
|
||||
function flushCcwebMcpChildSessionListBroadcast() {
|
||||
if (ccwebMcpChildSessionListBroadcastTimer) {
|
||||
clearTimeout(ccwebMcpChildSessionListBroadcastTimer);
|
||||
ccwebMcpChildSessionListBroadcastTimer = null;
|
||||
}
|
||||
broadcastSessionList();
|
||||
}
|
||||
|
||||
function scheduleCcwebMcpChildSessionListBroadcast(options = {}) {
|
||||
if (options.immediate) {
|
||||
flushCcwebMcpChildSessionListBroadcast();
|
||||
return;
|
||||
}
|
||||
if (ccwebMcpChildSessionListBroadcastTimer) return;
|
||||
ccwebMcpChildSessionListBroadcastTimer = setTimeout(() => {
|
||||
ccwebMcpChildSessionListBroadcastTimer = null;
|
||||
broadcastSessionList();
|
||||
}, CCWEB_MCP_CHILD_UPDATE_FLUSH_DELAY_MS);
|
||||
if (typeof ccwebMcpChildSessionListBroadcastTimer.unref === 'function') {
|
||||
ccwebMcpChildSessionListBroadcastTimer.unref();
|
||||
}
|
||||
}
|
||||
|
||||
function sendCcwebMcpChildAgentUpdate(sessionId, child) {
|
||||
const activeTool = updateCcwebMcpChildToolState(sessionId, child);
|
||||
const persistedTool = updatePersistedCcwebMcpChildTool(sessionId, child);
|
||||
const activeTool = updateCcwebMcpChildToolState(sessionId, child, persistedTool);
|
||||
const tool = activeTool || (persistedTool ? {
|
||||
id: persistedTool.id,
|
||||
name: persistedTool.name,
|
||||
@@ -8961,7 +9052,7 @@ function sendCcwebMcpChildAgentUpdate(sessionId, child) {
|
||||
};
|
||||
const targetWs = activeCodexAppTurns.get(sessionId)?.ws || findViewingSessionWs(sessionId);
|
||||
if (targetWs) wsSend(targetWs, payload);
|
||||
broadcastSessionList();
|
||||
scheduleCcwebMcpChildSessionListBroadcast({ immediate: isFinalCcwebMcpChildStatus(child.status) });
|
||||
}
|
||||
|
||||
function syncCcwebMcpChildAgentsFromCollabItem(routed, item = {}) {
|
||||
@@ -10910,6 +11001,8 @@ function handleListCodexSessions(ws, msg = {}) {
|
||||
for (const filePath of getCodexRolloutFiles()) {
|
||||
const parsed = parseCodexRolloutFile(filePath);
|
||||
if (!parsed?.meta?.threadId) continue;
|
||||
const source = codexImportSourceLabel(parsed.meta.source);
|
||||
if (source === 'subagent') continue;
|
||||
if (seen.has(parsed.meta.threadId)) continue;
|
||||
seen.add(parsed.meta.threadId);
|
||||
const title = parsed.meta.title || parsed.meta.threadId.slice(0, 20);
|
||||
@@ -10925,7 +11018,7 @@ function handleListCodexSessions(ws, msg = {}) {
|
||||
cwd: parsed.meta.cwd || null,
|
||||
updatedAt: parsed.meta.updatedAt || null,
|
||||
cliVersion: parsed.meta.cliVersion || '',
|
||||
source: codexImportSourceLabel(parsed.meta.source),
|
||||
source,
|
||||
sourceConversationId: sourceConversation?.id || null,
|
||||
sourceConversationTitle: sourceConversation?.title || '',
|
||||
duplicateCount: 1,
|
||||
|
||||
Reference in New Issue
Block a user