修复完整历史与运行态内存放大并重建发布包
This commit is contained in:
204
server.js
204
server.js
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user