修复完整历史与运行态内存放大并重建发布包

This commit is contained in:
shiyue
2026-10-14 18:13:02 +08:00
parent 69b6db0f5b
commit 2b1c91488d
28 changed files with 2344 additions and 159 deletions

204
server.js
View File

@@ -2377,12 +2377,14 @@ function cleanupExpiredAttachments() {
function collectSessionAttachmentIds(session) {
const ids = new Set();
for (const message of Array.isArray(session?.messages) ? session.messages : []) {
const collect = (message) => {
for (const attachment of Array.isArray(message?.attachments) ? message.attachments : []) {
const id = sanitizeId(attachment?.id || '');
if (id) ids.add(id);
}
}
};
if (sessionHistoryStore.state(session)?.partial) sessionHistoryStore.forEachMessage(session.id, collect);
else for (const message of Array.isArray(session?.messages) ? session.messages : []) collect(message);
return Array.from(ids);
}
@@ -4808,11 +4810,21 @@ function loadNativeSessionHistory(session) {
}
}
function resolveSessionHistory(session) {
function resolveSessionHistory(session, options = {}) {
const persisted = Array.isArray(session?.messages) ? session.messages : [];
const persistedMessages = persisted.filter((message) => !isPersistenceNoticeMessage(message));
const notice = persisted.some(isPersistenceNoticeMessage);
const historyState = sessionHistoryStore.state(session);
if (historyState?.source === 'archive' && (Number.isFinite(options.count) || historyState.partial)) {
if (Number.isFinite(options.count)) {
const page = sessionHistoryStore.loadHistoryPage(session.id, options.before, options.count);
if (!page) throw new Error('完整会话归档已移除');
return { ...page, messages: page.messages.map(normalizeHistoryMessage), source: 'archive',
available: page.complete, recoverable: false };
}
return { messages: normalizeArchivedHistory(persistedMessages), total: historyState.total,
start: historyState.start, source: 'archive', available: historyState.complete, recoverable: false };
}
if (historyState?.source === 'archive' && historyState.complete) {
return { messages: normalizeArchivedHistory(persistedMessages), source: 'archive', available: true, recoverable: false };
}
@@ -6153,7 +6165,7 @@ function refreshSessionSnapshotCache(id) {
if (fd !== undefined) fs.closeSync(fd);
}
if (cachedRevision === revision) return null;
const session = loadSession(id);
const session = loadSession(id, { count: SESSION_PERSIST_MAX_MESSAGES });
if (!session) return null;
const result = buildSessionJsonForPersist({ ...session, historyArchiveRevision: revision });
try {
@@ -6223,12 +6235,24 @@ function loadSessionMetaFromFile(filePath) {
}
}
function loadSession(id) {
function loadSession(id, options = {}) {
const normalizedId = sanitizeId(id || '');
if (!normalizedId) return null;
try {
const filePath = sessionPath(normalizedId);
let session = sessionHistoryStore.load(normalizedId);
// 运行对象仅持有可修改的尾部;完整历史的可信基线是磁盘块引用。
let window = options.window;
if (options.messageId) {
const index = sessionHistoryStore.messageIndex(normalizedId, options.messageId);
if (index >= 0) window = { start: index, end: index + 1 };
} else if (options.promptId) {
const found = sessionHistoryStore.findMessage(normalizedId, message => message?.ccwebPrompt?.id === options.promptId);
if (found) window = { start: found.index, end: found.index + 1 };
}
let session = window ? sessionHistoryStore.loadWindow(normalizedId, window.start, window.end)
: options.full === true ? sessionHistoryStore.load(normalizedId)
: options.metadata === true ? sessionHistoryStore.loadMetadata(normalizedId)
: sessionHistoryStore.loadTail(normalizedId, options.count ?? SESSION_PERSIST_MAX_MESSAGES, { maxBytes: 2 * 1024 * 1024 });
if (!session) {
if (!fs.existsSync(filePath)) return null;
session = safeReadSessionJson(filePath, SESSION_LOAD_MAX_BYTES, { sessionId: normalizedId });
@@ -6248,11 +6272,25 @@ function loadSession(id) {
}
}
function findSessionMessage(session, predicate) {
const messages = Array.isArray(session?.messages) ? session.messages : [];
for (let index = messages.length - 1; index >= 0; index -= 1) {
if (predicate(messages[index])) return messages[index];
}
const historyState = sessionHistoryStore.state(session);
if (!historyState?.partial) return null;
const found = sessionHistoryStore.findMessage(session.id, (message, index) => (
(index < historyState.start || index >= historyState.end) && predicate(message)
));
return found?.message || null;
}
function preserveNewerTaskTrackingForSessionSave(session, targetPath) {
// 可信归档的版本保护由同一次存储提交处理,避免保存前重复读取清单。
if (sessionHistoryStore.state(session)?.source === 'archive') return;
let persisted;
try {
persisted = sessionHistoryStore.load(session.id);
if (!persisted && fs.existsSync(targetPath)) {
if (fs.existsSync(targetPath)) {
persisted = safeReadSessionJson(targetPath, SESSION_LOAD_MAX_BYTES, { sessionId: session.id });
}
} catch {
@@ -6280,9 +6318,13 @@ function saveSession(session, options = {}) {
const targetPath = sessionPath(session.id);
try {
if (!options.taskBoardMetadata) preserveNewerTaskTrackingForSessionSave(session, targetPath);
const committed = sessionHistoryStore.save(session);
const committed = sessionHistoryStore.save(session, { taskBoardMetadata: options.taskBoardMetadata === true });
if (committed.taskTrackingPreserved) plog('INFO', 'task_tracking_preserved_on_session_save', {
sessionId: String(session.id || '').slice(0, 8), ...committed.taskTrackingPreserved,
});
try {
const result = buildSessionJsonForPersist({ ...committed.session, historyArchiveRevision: committed.revision });
const preview = sessionHistoryStore.loadTail(session.id, SESSION_PERSIST_MAX_MESSAGES, { maxBytes: 2 * 1024 * 1024 });
const result = buildSessionJsonForPersist({ ...preview, historyArchiveRevision: committed.revision });
writeFileAtomicSync(targetPath, result.json);
if (result.guarded) {
const lastAttempt = result.attempts[result.attempts.length - 1] || {};
@@ -6344,7 +6386,7 @@ function listTaskBoardSessionIds() {
function saveTaskBoardSessionMetadata(session) {
// 任务服务会克隆会话;重新读取可信档案,只提交它负责的元数据。
const completeSession = loadSession(session?.id);
const completeSession = loadSession(session?.id, { metadata: true });
if (!completeSession) return false;
if (Object.prototype.hasOwnProperty.call(session, 'taskTracking')) {
completeSession.taskTracking = session.taskTracking;
@@ -6357,7 +6399,7 @@ function saveTaskBoardSessionMetadata(session) {
function createProductionTaskBoardService() {
const dependencies = {
loadSession,
loadSession: id => loadSession(id, { metadata: true }),
saveSession: saveTaskBoardSessionMetadata,
listSessions: listTaskBoardSessionIds,
loadStatusDefinitions: loadTaskBoardStatusConfig,
@@ -6990,8 +7032,7 @@ function appendCodexAppRecoveryNotice(sessionId, content) {
const session = loadSession(sessionId);
if (!session || !isCodexAppSession(session)) return false;
// 状态文件按设计会保留证据,重启会再次进入恢复分支;提示只保留一条。
if (Array.isArray(session.messages)
&& session.messages.some(message => message?.codexAppRecoveredPartial === true && message?.content === content)) {
if (findSessionMessage(session, message => message?.codexAppRecoveredPartial === true && message?.content === content)) {
return true;
}
session.messages.push({
@@ -7006,8 +7047,7 @@ function appendCodexAppRecoveryNotice(sessionId, content) {
}
function hasCodexAppTurnMessage(session, turnKey) {
return Array.isArray(session?.messages)
&& session.messages.some((message) => message?.codexAppTurnKey === turnKey);
return !!findSessionMessage(session, message => message?.codexAppTurnKey === turnKey);
}
function recoverCodexAppTurnState(sessionId) {
@@ -8125,7 +8165,7 @@ function startCrossConversationReplyAutoRun(sourceSession, targetSession, replyT
function hasProcessedCrossConversationReply(session, requestId) {
const normalizedRequestId = String(requestId || '').trim();
if (!normalizedRequestId || !Array.isArray(session?.messages)) return false;
return session.messages.some((message) => (
return !!findSessionMessage(session, (message) => (
message?.crossConversation?.replyToRequestId === normalizedRequestId &&
message?.crossConversation?.processed === true
));
@@ -8342,7 +8382,8 @@ function getPendingCrossConversationReply(args = {}, sourceSessionId = '') {
if (!pending || pending.sourceConversationId !== sourceId) {
const sourceSession = loadSession(sourceId);
if (hasProcessedCrossConversationReply(sourceSession, requestId)) {
const message = sourceSession.messages.find((item) => item?.crossConversation?.replyToRequestId === requestId);
const message = findSessionMessage(sourceSession, item => item?.crossConversation?.replyToRequestId === requestId
&& item?.crossConversation?.processed === true);
return {
ok: true,
requestId,
@@ -8491,17 +8532,15 @@ function completeCrossConversationReply(requestId, entry = {}, targetSession = n
}
function latestAssistantMessageText(session) {
const messages = Array.isArray(session?.messages) ? session.messages : [];
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];
if (message?.role !== 'assistant') continue;
const text = extractCrossConversationReplyText(message.content || '');
if (text.trim()) return text.trim();
}
return '';
const message = findSessionMessage(session, item => item?.role === 'assistant'
&& extractCrossConversationReplyText(item.content || '').trim());
return message ? extractCrossConversationReplyText(message.content || '').trim() : '';
}
function assistantMessageCount(session) {
if (sessionHistoryStore.state(session)?.source === 'archive') {
return sessionHistoryStore.countMessages(session.id, 'assistant');
}
return Array.isArray(session?.messages)
? session.messages.filter((message) => message?.role === 'assistant').length
: 0;
@@ -8682,7 +8721,7 @@ function scriptSelectSemanticBranchApi(args = {}, sourceConversationId = '') {
function insertJavascriptRunFailureNotice(sourceConversationId, entry = {}) {
const session = loadSession(sourceConversationId);
if (!session) return false;
if (Array.isArray(session.messages) && session.messages.some((message) => message?.scriptNotification?.runId === entry.runId)) return false;
if (findSessionMessage(session, message => message?.scriptNotification?.runId === entry.runId)) return false;
const reason = entry.terminationReason || entry.signal || `exitCode=${entry.exitCode ?? 'unknown'}`;
const stderr = String(entry.stderrPreview || '').trim();
const lines = [
@@ -9218,10 +9257,10 @@ function compactStartMessage(agent) {
}
function compactDoneMessage(agent) {
if (agent === 'codexapp') return '上下文压缩完成。已执行 Codex App 原生 /compact,下次继续在同一会话发送即可。';
if (agent === 'codexapp') return '上下文压缩完成。已执行 Codex App 原生 /compact。';
return agent === 'codex'
? '上下文压缩完成。已执行 Codex /compact,下次继续在同一会话发送即可。'
: '上下文压缩完成。已按 Claude Code 原生策略执行 /compact,下次继续在同一会话发送即可。';
? '上下文压缩完成。已执行 Codex /compact。'
: '上下文压缩完成。已按 Claude Code 原生策略执行 /compact。';
}
function initStartMessage(agent) {
@@ -9339,8 +9378,7 @@ function cliRunKey(sessionId, entry = {}) {
}
function hasCliRunMessage(session, runKey) {
return Array.isArray(session?.messages)
&& session.messages.some(message => message?.role === 'assistant' && message.runKey === runKey);
return !!findSessionMessage(session, message => message?.role === 'assistant' && message.runKey === runKey);
}
function retryTailText(value, maxChars) {
@@ -11534,8 +11572,10 @@ function setCurrentConversationTitle(args = {}, sourceSessionId = '') {
const changed = previousTitle !== normalizedTitle;
let titleEvent = null;
if (changed) {
const messageIndex = Array.isArray(session.messages) ? session.messages.length : 0;
const anchorMessageIndex = findLatestUserMessageIndex(session.messages);
const historyBaseIndex = Number(session.historySnapshotBaseIndex) || 0;
const messageIndex = historyBaseIndex + (Array.isArray(session.messages) ? session.messages.length : 0);
const localAnchor = findLatestUserMessageIndex(session.messages);
const anchorMessageIndex = localAnchor === null ? null : historyBaseIndex + localAnchor;
titleEvent = {
title: normalizedTitle,
changedAt: new Date().toISOString(),
@@ -11611,7 +11651,7 @@ function resolveBranchSource(args = {}) {
const sourceSessionId = sanitizeId(args.branchSourceSessionId || args.sourceSessionId || '');
if (!sourceSessionId) return { ok: true, sourceSession: null };
const sourceSession = loadSession(sourceSessionId);
const sourceSession = loadSession(sourceSessionId, { full: true });
if (!sourceSession) {
return mcpToolError('branch_source_not_found', '来源会话不存在,无法创建分支。', { sourceSessionId });
}
@@ -11721,7 +11761,7 @@ function createPersistentConversationSession(args = {}, options = {}) {
function buildSessionInfoPayload(session) {
const waitState = crossConversationWaitState(session.id);
const history = typeof resolveSessionHistory === 'function'
? resolveSessionHistory(session)
? resolveSessionHistory(session, { count: INITIAL_HISTORY_COUNT })
: { messages: session.messages || [], source: 'snapshot', available: true };
const messages = sanitizeMessagesForTransport(history.messages || []);
const taskTracking = taskTrackingSnapshotForSession(session);
@@ -11740,8 +11780,10 @@ function buildSessionInfoPayload(session) {
totalCost: session.totalCost || 0,
totalUsage: session.totalUsage || { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
updated: session.updated,
historyTotal: messages.length,
historyBaseIndex: 0,
historyTotal: history.total ?? messages.length,
historyBaseIndex: history.start || 0,
historyCursor: history.start || 0,
historyTruncated: (history.start || 0) > 0,
hasUnread: false,
historyPending: false,
isRunning: false,
@@ -11822,7 +11864,7 @@ function handleNewSession(ws, msg) {
function handleLoadHistoryPage(ws, msg = {}) {
const sessionId = sanitizeId(msg.sessionId || '');
const session = loadSession(sessionId);
const session = loadSession(sessionId, { metadata: true });
if (!session) {
return wsSend(ws, attachClientRequestId({
type: 'error',
@@ -11831,11 +11873,22 @@ function handleLoadHistoryPage(ws, msg = {}) {
message: 'Session not found',
}, msg));
}
const requestedBefore = Number.parseInt(String(msg.before || ''), 10);
const archived = sessionHistoryStore.state(session);
if (archived?.source === 'archive') {
const before = Number.isFinite(requestedBefore) ? requestedBefore : Math.max(0, archived.total - INITIAL_HISTORY_COUNT);
const page = sessionHistoryStore.loadHistoryPage(session.id, before, HISTORY_CHUNK_SIZE);
return wsSend(ws, attachClientRequestId({
type: 'session_history_chunk', sessionId: session.id,
messages: sanitizeMessagesForTransport(page.messages.map(normalizeHistoryMessage)), remaining: 0,
historyCursor: page.start, historyBaseIndex: page.start, historyTruncated: page.start > 0,
historyTotal: page.total, historyAvailable: page.complete, historySource: 'archive',
}, msg));
}
const history = typeof resolveSessionHistory === 'function'
? resolveSessionHistory(session)
: { messages: session.messages || [], source: 'snapshot', available: true };
const list = sanitizeMessagesForTransport(history.messages);
const requestedBefore = Number.parseInt(String(msg.before || ''), 10);
const list = history.messages;
const before = Number.isFinite(requestedBefore)
? Math.max(0, Math.min(list.length, requestedBefore))
: Math.max(0, list.length - INITIAL_HISTORY_COUNT);
@@ -11844,7 +11897,7 @@ function handleLoadHistoryPage(ws, msg = {}) {
wsSend(ws, attachClientRequestId({
type: 'session_history_chunk',
sessionId: session.id,
messages: list.slice(start, end),
messages: sanitizeMessagesForTransport(list.slice(start, end)),
remaining: 0,
historyCursor: start,
historyBaseIndex: start,
@@ -11866,9 +11919,9 @@ function handleMigrateSessionHistory(ws, msg = {}) {
if (!sessionId || !fs.existsSync(filePath)) return respond({ ok: false, code: 'session_not_found' });
try {
const original = fs.readFileSync(filePath, 'utf8');
const session = loadSession(sessionId);
const session = loadSession(sessionId, { full: true });
if (!session) return respond({ ok: false, code: 'session_not_found' });
if (sessionHistoryStore.state(session)?.complete === true || !session.messages.some(isPersistenceNoticeMessage)) {
if (sessionHistoryStore.state(session)?.complete === true) {
return respond({ ok: true, changed: false, messageCount: session.messages.length });
}
const snapshot = session.messages.filter((message) => !isPersistenceNoticeMessage(message));
@@ -11887,7 +11940,7 @@ function handleMigrateSessionHistory(ws, msg = {}) {
session.messages = merged.messages;
sessionHistoryStore.markComplete(session);
if (!saveSession(session)) return respond({ ok: false, code: 'history_archive_save_failed', backupPath });
const reloaded = loadSession(sessionId);
const reloaded = loadSession(sessionId, { full: true });
if (!reloaded || reloaded.messages.length !== merged.messages.length) {
return respond({ ok: false, code: 'history_reload_failed', backupPath });
}
@@ -11986,7 +12039,7 @@ function attachActiveRuntimeToWs(ws, sessionId, source = {}) {
function handleResumeSession(ws, msg = {}) {
const sessionId = sanitizeId(msg.sessionId || '');
const session = loadSession(sessionId);
const session = loadSession(sessionId, { metadata: true });
if (!session) {
return wsSend(ws, attachClientRequestId({
type: 'error',
@@ -12001,7 +12054,7 @@ function handleResumeSession(ws, msg = {}) {
// 重连只发送 resume_generating 会让前端无法补回断线期间丢掉的历史气泡;
// 同时带上最近历史,前端可按消息索引幂等补齐而不会重绘整个会话。
const history = typeof resolveSessionHistory === 'function'
? resolveSessionHistory(session)
? resolveSessionHistory(session, { count: INITIAL_HISTORY_COUNT })
: { messages: session.messages || [], source: 'snapshot', available: true };
const historyMessages = sanitizeMessagesForTransport(history.messages || []);
const recentMessages = historyMessages.slice(-INITIAL_HISTORY_COUNT);
@@ -12016,8 +12069,8 @@ function handleResumeSession(ws, msg = {}) {
isRunning: isSessionRunning(sessionId),
attached: hasActiveRuntime,
messages: recentMessages,
historyTotal: historyMessages.length,
historyBaseIndex: Math.max(0, historyMessages.length - recentMessages.length),
historyTotal: history.total ?? historyMessages.length,
historyBaseIndex: (history.start || 0) + Math.max(0, historyMessages.length - recentMessages.length),
historyAvailable: history.available !== false,
historySource: history.source || 'snapshot',
}, msg));
@@ -12028,7 +12081,7 @@ function handleResumeSession(ws, msg = {}) {
function handleLoadSession(ws, msg) {
const sessionId = sanitizeId(typeof msg === 'string' ? msg : msg?.sessionId);
reconcilePendingCrossConversationReplies();
const session = loadSession(sessionId);
const session = loadSession(sessionId, { metadata: true });
if (!session) {
return wsSend(ws, attachClientRequestId({
type: 'error',
@@ -12038,7 +12091,7 @@ function handleLoadSession(ws, msg) {
}, msg));
}
flushPendingCrossConversationReplies(sessionId);
const refreshedSession = loadSession(sessionId) || session;
const refreshedSession = loadSession(sessionId, { metadata: true }) || session;
if (getSessionAgent(refreshedSession) === 'claude' && !refreshedSession.cwd && refreshedSession.claudeSessionId) {
const localMeta = resolveClaudeSessionLocalMeta(refreshedSession.claudeSessionId);
if (localMeta?.cwd) {
@@ -12050,20 +12103,27 @@ function handleLoadSession(ws, msg) {
const requestedTargetMessageIndex = Number.parseInt(String(
typeof msg === 'object' ? msg?.targetMessageIndex ?? '' : '',
), 10);
const history = typeof resolveSessionHistory === 'function'
? resolveSessionHistory(refreshedSession)
let history = typeof resolveSessionHistory === 'function'
? resolveSessionHistory(refreshedSession, { count: INITIAL_HISTORY_COUNT })
: { messages: refreshedSession.messages || [], source: 'snapshot', available: true };
const historyTotal = history.total ?? history.messages.length;
const targetMessageIndex = Number.isFinite(requestedTargetMessageIndex)
? Math.max(0, Math.min(history.messages.length - 1, requestedTargetMessageIndex))
? Math.max(0, Math.min(historyTotal - 1, requestedTargetMessageIndex))
: null;
const recentHistoryBaseIndex = Math.max(0, history.messages.length - INITIAL_HISTORY_COUNT);
const recentHistoryBaseIndex = Math.max(0, historyTotal - INITIAL_HISTORY_COUNT);
const targetPrefetchChunks = targetMessageIndex !== null && targetMessageIndex < recentHistoryBaseIndex
? Math.ceil((recentHistoryBaseIndex - targetMessageIndex) / HISTORY_CHUNK_SIZE)
: 0;
const { recentMessages, olderChunks, historyRemaining, historyBuffered } = splitHistoryMessages(
const prefetchChunks = targetMessageIndex === null ? 0 : Math.min(HISTORY_MAX_CHUNKS_PER_LOAD, Math.max(HISTORY_PREFETCH_CHUNKS, targetPrefetchChunks));
if (prefetchChunks > 0 && sessionHistoryStore.state(refreshedSession)?.source === 'archive') {
history = resolveSessionHistory(refreshedSession, { count: INITIAL_HISTORY_COUNT + prefetchChunks * HISTORY_CHUNK_SIZE });
}
const split = splitHistoryMessages(
history.messages,
{ prefetchChunks: targetMessageIndex === null ? 0 : Math.max(HISTORY_PREFETCH_CHUNKS, targetPrefetchChunks) },
{ prefetchChunks },
);
const { recentMessages, olderChunks, historyBuffered } = split;
const historyRemaining = (history.start || 0) + split.historyRemaining;
const effectiveCwd = refreshedSession.cwd || activeProcesses.get(sessionId)?.cwd || activeCodexAppTurns.get(sessionId)?.cwd || null;
const waitState = crossConversationWaitState(sessionId);
const taskTracking = taskTrackingSnapshotForSession(refreshedSession);
@@ -12095,10 +12155,10 @@ function handleLoadSession(ws, msg) {
cwd: effectiveCwd,
totalCost: session.totalCost || 0,
totalUsage: session.totalUsage || null,
historyTotal: history.messages.length,
historyTotal,
historyBuffered,
historyCursor: historyRemaining,
historyBaseIndex: Math.max(0, history.messages.length - recentMessages.length),
historyBaseIndex: Math.max(0, historyTotal - recentMessages.length),
historyTruncated: historyRemaining > 0,
historyPending: olderChunks.length > 0,
historyAvailable: history.available,
@@ -12116,7 +12176,7 @@ function handleLoadSession(ws, msg) {
}, msg));
if (olderChunks.length > 0) {
let chunkEnd = Math.max(0, history.messages.length - recentMessages.length);
let chunkEnd = Math.max(0, historyTotal - recentMessages.length);
olderChunks.forEach((chunk, index) => {
const chunkStart = Math.max(0, chunkEnd - chunk.length);
wsSend(ws, attachClientRequestId({
@@ -12127,7 +12187,7 @@ function handleLoadSession(ws, msg) {
historyCursor: index === olderChunks.length - 1 ? historyRemaining : null,
historyBaseIndex: chunkStart,
historyTruncated: historyRemaining > 0,
historyTotal: history.messages.length,
historyTotal,
historyAvailable: history.available,
historySource: history.source,
}, msg));
@@ -12740,7 +12800,7 @@ function handleMessage(ws, msg, options = {}) {
if (ws && (options.emitUserMessage || msg.codexAppSteer === true) && persistedUserMessage) {
wsSend(ws, {
type: 'session_message', sessionId: currentSessionId, message: persistedUserMessage,
messageIndex: session.messages.findIndex((message) => message.id === persistedUserMessage.id),
messageIndex: (Number(session.historySnapshotBaseIndex) || 0) + session.messages.findIndex((message) => message.id === persistedUserMessage.id),
});
}
sendSessionList(ws);
@@ -12758,7 +12818,7 @@ function handleMessage(ws, msg, options = {}) {
wsSend(ws, {
type: 'codex_app_steer_status', sessionId: currentSessionId,
clientMessageId: msg.clientMessageId, status: 'sent', message: '已转为新对话',
messageIndex: session.messages.findIndex((message) => message.id === persistedUserMessage?.id),
messageIndex: (Number(session.historySnapshotBaseIndex) || 0) + session.messages.findIndex((message) => message.id === persistedUserMessage?.id),
});
wsSend(ws, buildCodexAppResumePayload(currentSessionId, activeCodexAppTurns.get(currentSessionId)));
}
@@ -14165,7 +14225,7 @@ function handleCcwebPromptUserResponse(ws, msg = {}) {
return fail('session_running', '当前会话正在运行,暂不能提交 ccweb 表单答案。');
}
const session = loadSession(sessionId);
const session = loadSession(sessionId, { promptId });
if (!session) return fail('session_not_found', '会话不存在。');
const found = findCcwebPromptMessage(session, promptId);
@@ -14227,7 +14287,7 @@ function handleCcwebPromptUserDismiss(ws, msg = {}) {
if (!sessionId) return fail('missing_session_id', '缺少会话 ID。');
if (!promptId) return fail('missing_prompt_id', '缺少 promptId。');
const session = loadSession(sessionId);
const session = loadSession(sessionId, { promptId });
if (!session) return fail('session_not_found', '会话不存在。');
const found = removeCcwebPromptMessage(session, promptId);
if (!found) return fail('prompt_not_found', 'ccweb 表单不存在或已被清理。', { promptId });
@@ -15217,7 +15277,7 @@ function handleCodexAppTurnComplete(sessionId, options = {}) {
}
const assistantMessage = session?.messages?.find((message) => message.codexAppTurnKey === turnKey);
const assistantMessageIndex = assistantMessage
? resolveSessionHistory(session).messages.findIndex((message) => message.id === assistantMessage.id)
? sessionHistoryStore.messageIndex(sessionId, assistantMessage.id)
: -1;
wsSend(entry.ws, {
type: 'done', sessionId, costUsd: null, goalActive,
@@ -15266,7 +15326,7 @@ function isCodexAppNoActiveTurnError(err) {
function rollbackPersistedCodexAppSteerMessage(sessionId, userMessageId) {
const id = String(userMessageId || '').trim();
if (!sessionId || !id) return false;
const session = loadSession(sessionId);
const session = loadSession(sessionId, { messageId: id });
if (!session || !Array.isArray(session.messages)) return false;
const nextMessages = session.messages.filter((message) => !(message?.role === 'user' && message.id === id));
if (nextMessages.length === session.messages.length) return false;
@@ -15294,7 +15354,8 @@ function reorderStaleCodexAppTurnMessages(sessionId, entry, userMessageId) {
session.updated = new Date().toISOString();
if (!saveSession(session)) return false;
wsSend(entry.ws, {
type: 'session_message', sessionId, message: sanitizeMessageForTransport(assistantMessage), messageIndex: userIndex,
type: 'session_message', sessionId, message: sanitizeMessageForTransport(assistantMessage),
messageIndex: (Number(session.historySnapshotBaseIndex) || 0) + userIndex,
});
return true;
}
@@ -15318,7 +15379,8 @@ function handleCodexAppSteerMessage(ws, msg, options = {}) {
if (clientMessageId) payload.clientMessageId = clientMessageId;
if (status === 'sent') {
const savedSession = loadSession(sessionId);
payload.messageIndex = savedSession?.messages?.findIndex((message) => message.id === userMessageId);
const index = savedSession?.messages?.findIndex((message) => message.id === userMessageId);
payload.messageIndex = index >= 0 ? (Number(savedSession.historySnapshotBaseIndex) || 0) + index : index;
}
wsSend(targetWs, payload);
};
@@ -15886,6 +15948,10 @@ function handleListNativeSessions(ws) {
function prepareNativeHistoryImport(existingSession, importedSession) {
if (!existingSession) return normalizeSession(importedSession);
if (sessionHistoryStore.state(existingSession)?.partial) {
existingSession = loadSession(existingSession.id, { full: true });
if (!existingSession) return null;
}
const snapshot = existingSession.messages.filter(message => !isPersistenceNoticeMessage(message));
const merged = mergeNativeHistoryWithSnapshot(existingSession, snapshot, importedSession.messages);
if (!merged.confirmed) return null;