From b2d8fcc7a128ae1e3f6a4c5ac78963d63a04b8e8 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sat, 11 Apr 2026 01:45:26 +0800 Subject: [PATCH] Fix processed history hash rebuild after recovery --- index.js | 9 +++++++ runtime/runtime-state.js | 18 +++++++++++++- tests/mobile-status-regressions.mjs | 38 +++++++++++++++++++++++++++-- tests/runtime-history.mjs | 25 +++++++++++++++++++ ui/ui-actions-controller.js | 16 ++++++++++++ 5 files changed, 103 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 0991fc9..a0de6ea 100644 --- a/index.js +++ b/index.js @@ -11609,6 +11609,14 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") { resultCode: "history.recovery.fallback-full-rebuild", }), ); + const recoveredLastProcessedFloor = Number.isFinite( + currentGraph?.historyState?.lastProcessedAssistantFloor, + ) + ? currentGraph.historyState.lastProcessedAssistantFloor + : -1; + if (recoveredLastProcessedFloor >= 0) { + updateProcessedHistorySnapshot(chat, recoveredLastProcessedFloor); + } currentGraph.vectorIndexState.lastIntegrityIssue = null; saveGraphToChat({ reason: "history-recovery-fallback-rebuild" }); refreshPanelLiveState(); @@ -12390,6 +12398,7 @@ async function onRebuild() { replayExtractionFromHistory, restoreRuntimeUiState, saveGraphToChat, + updateProcessedHistorySnapshot, setCurrentGraph: (graph) => { currentGraph = graph; }, diff --git a/runtime/runtime-state.js b/runtime/runtime-state.js index 20f4999..ba72c99 100644 --- a/runtime/runtime-state.js +++ b/runtime/runtime-state.js @@ -257,6 +257,17 @@ export function normalizeGraphRuntimeState(graph, chatId = "") { historyState.processedMessageHashVersion = PROCESSED_MESSAGE_HASH_VERSION; historyState.processedMessageHashesNeedRefresh = true; } + const lastProcessedAssistantFloor = Number( + historyState.lastProcessedAssistantFloor, + ); + if ( + historyState.processedMessageHashesNeedRefresh !== true && + Number.isFinite(lastProcessedAssistantFloor) && + lastProcessedAssistantFloor >= 0 && + Object.keys(historyState.processedMessageHashes).length === 0 + ) { + historyState.processedMessageHashesNeedRefresh = true; + } if ( !vectorIndexState.hashToNodeId || @@ -613,10 +624,15 @@ export function clearHistoryDirty(graph, result = null) { graph.historyState.historyDirtyFrom = null; graph.historyState.lastMutationReason = ""; graph.historyState.lastMutationSource = ""; + const lastProcessedAssistantFloor = Number( + graph.historyState.lastProcessedAssistantFloor, + ); graph.historyState.processedMessageHashVersion = PROCESSED_MESSAGE_HASH_VERSION; graph.historyState.processedMessageHashes = {}; - graph.historyState.processedMessageHashesNeedRefresh = false; + graph.historyState.processedMessageHashesNeedRefresh = + Number.isFinite(lastProcessedAssistantFloor) && + lastProcessedAssistantFloor >= 0; if (result) { graph.historyState.lastRecoveryResult = result; } diff --git a/tests/mobile-status-regressions.mjs b/tests/mobile-status-regressions.mjs index 6a7e49f..8fb6a9e 100644 --- a/tests/mobile-status-regressions.mjs +++ b/tests/mobile-status-regressions.mjs @@ -456,10 +456,17 @@ async function testManualExtractIgnoresFailedBatchWithoutPersistenceAttempt() { async function testManualRebuildSetsTerminalRuntimeStatus() { const chat = [{ is_user: true, mes: "u" }, { is_user: false, mes: "a" }]; + let savedHashes = null; + let savedNeedRefresh = null; const context = { ...createBaseStatusContext(), __confirmHost: true, currentGraph: { + historyState: { + lastProcessedAssistantFloor: -1, + processedMessageHashes: {}, + processedMessageHashesNeedRefresh: false, + }, vectorIndexState: { lastWarning: "", }, @@ -489,6 +496,11 @@ async function testManualRebuildSetsTerminalRuntimeStatus() { }, createEmptyGraph() { return { + historyState: { + lastProcessedAssistantFloor: -1, + processedMessageHashes: {}, + processedMessageHashesNeedRefresh: false, + }, vectorIndexState: { lastWarning: "", }, @@ -501,14 +513,31 @@ async function testManualRebuildSetsTerminalRuntimeStatus() { clearInjectionState() {}, async prepareVectorStateForReplay() {}, async replayExtractionFromHistory() { + context.currentGraph.historyState.lastProcessedAssistantFloor = 1; context.currentGraph.vectorIndexState.lastWarning = ""; return 2; }, - clearHistoryDirty() {}, + clearHistoryDirty(graph) { + graph.historyState.processedMessageHashes = {}; + graph.historyState.processedMessageHashesNeedRefresh = true; + }, buildRecoveryResult(status, extra = {}) { return { status, ...extra }; }, - saveGraphToChat() {}, + updateProcessedHistorySnapshot(chatInput, floor) { + context.currentGraph.historyState.lastProcessedAssistantFloor = floor; + context.currentGraph.historyState.processedMessageHashes = {}; + for (let index = 0; index <= floor; index += 1) { + context.currentGraph.historyState.processedMessageHashes[index] = + String(chatInput[index]?.mes || ""); + } + context.currentGraph.historyState.processedMessageHashesNeedRefresh = false; + }, + saveGraphToChat() { + savedHashes = { ...context.currentGraph.historyState.processedMessageHashes }; + savedNeedRefresh = + context.currentGraph.historyState.processedMessageHashesNeedRefresh; + }, restoreRuntimeUiState() {}, onRebuildController, result: null, @@ -524,6 +553,11 @@ async function testManualRebuildSetsTerminalRuntimeStatus() { assert.equal(context.lastExtractionStatus.text, "图谱重建完成"); assert.equal(context.runtimeStatus.text, "图谱重建完成"); assert.equal(context.runtimeStatus.level, "success"); + assert.deepEqual(savedHashes, { + 0: "u", + 1: "a", + }); + assert.equal(savedNeedRefresh, false); } testIndexDefinesLastProcessedAssistantFloorHelper(); diff --git a/tests/runtime-history.mjs b/tests/runtime-history.mjs index e361047..5d74444 100644 --- a/tests/runtime-history.mjs +++ b/tests/runtime-history.mjs @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import { appendBatchJournal, + clearHistoryDirty, cloneGraphSnapshot, createBatchJournalEntry, detectHistoryMutation, @@ -95,6 +96,17 @@ assert.equal(migratedGraph.historyState.processedMessageHashesNeedRefresh, true) const migratedDetection = detectHistoryMutation(chat, migratedGraph.historyState); assert.equal(migratedDetection.dirty, false); +const emptyHashGraph = normalizeGraphRuntimeState({ + historyState: { + chatId: "chat-history-test", + lastProcessedAssistantFloor: 3, + processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION, + processedMessageHashes: {}, + processedMessageHashesNeedRefresh: false, + }, +}); +assert.equal(emptyHashGraph.historyState.processedMessageHashesNeedRefresh, true); + const importedGraph = normalizeGraphRuntimeState({ historyState: { chatId: "chat-history-test", @@ -117,6 +129,19 @@ assert.deepEqual( snapshotProcessedMessageHashes(chat, 3), ); +const clearedGraph = normalizeGraphRuntimeState({ + historyState: { + chatId: "chat-history-test", + lastProcessedAssistantFloor: 3, + processedMessageHashVersion: PROCESSED_MESSAGE_HASH_VERSION, + processedMessageHashes: hashes, + processedMessageHashesNeedRefresh: false, + }, +}); +clearHistoryDirty(clearedGraph, { status: "replayed" }); +assert.deepEqual(clearedGraph.historyState.processedMessageHashes, {}); +assert.equal(clearedGraph.historyState.processedMessageHashesNeedRefresh, true); + const truncatedChat = chat.slice(0, 2); const truncatedDetection = detectHistoryMutation(truncatedChat, { lastProcessedAssistantFloor: 3, diff --git a/ui/ui-actions-controller.js b/ui/ui-actions-controller.js index c1e29e7..356d469 100644 --- a/ui/ui-actions-controller.js +++ b/ui/ui-actions-controller.js @@ -415,6 +415,22 @@ export async function onRebuildController(runtime) { reason: "用户手动触发全量重建", }), ); + const recoveredLastProcessedFloor = Number.isFinite( + runtime.getCurrentGraph()?.historyState?.lastProcessedAssistantFloor, + ) + ? runtime.getCurrentGraph().historyState.lastProcessedAssistantFloor + : -1; + if (recoveredLastProcessedFloor >= 0) { + if (typeof runtime.updateProcessedHistorySnapshot === "function") { + runtime.updateProcessedHistorySnapshot(chat, recoveredLastProcessedFloor); + } else if (typeof runtime.applyProcessedHistorySnapshotToGraph === "function") { + runtime.applyProcessedHistorySnapshotToGraph( + runtime.getCurrentGraph(), + chat, + recoveredLastProcessedFloor, + ); + } + } runtime.saveGraphToChat({ reason: "manual-rebuild-complete" }); runtime.setLastExtractionStatus( "图谱重建完成",