Fix processed history hash rebuild after recovery

This commit is contained in:
Youzini-afk
2026-04-11 01:45:26 +08:00
parent 298d615126
commit b2d8fcc7a1
5 changed files with 103 additions and 3 deletions

View File

@@ -11609,6 +11609,14 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") {
resultCode: "history.recovery.fallback-full-rebuild", 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; currentGraph.vectorIndexState.lastIntegrityIssue = null;
saveGraphToChat({ reason: "history-recovery-fallback-rebuild" }); saveGraphToChat({ reason: "history-recovery-fallback-rebuild" });
refreshPanelLiveState(); refreshPanelLiveState();
@@ -12390,6 +12398,7 @@ async function onRebuild() {
replayExtractionFromHistory, replayExtractionFromHistory,
restoreRuntimeUiState, restoreRuntimeUiState,
saveGraphToChat, saveGraphToChat,
updateProcessedHistorySnapshot,
setCurrentGraph: (graph) => { setCurrentGraph: (graph) => {
currentGraph = graph; currentGraph = graph;
}, },

View File

@@ -257,6 +257,17 @@ export function normalizeGraphRuntimeState(graph, chatId = "") {
historyState.processedMessageHashVersion = PROCESSED_MESSAGE_HASH_VERSION; historyState.processedMessageHashVersion = PROCESSED_MESSAGE_HASH_VERSION;
historyState.processedMessageHashesNeedRefresh = true; 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 ( if (
!vectorIndexState.hashToNodeId || !vectorIndexState.hashToNodeId ||
@@ -613,10 +624,15 @@ export function clearHistoryDirty(graph, result = null) {
graph.historyState.historyDirtyFrom = null; graph.historyState.historyDirtyFrom = null;
graph.historyState.lastMutationReason = ""; graph.historyState.lastMutationReason = "";
graph.historyState.lastMutationSource = ""; graph.historyState.lastMutationSource = "";
const lastProcessedAssistantFloor = Number(
graph.historyState.lastProcessedAssistantFloor,
);
graph.historyState.processedMessageHashVersion = graph.historyState.processedMessageHashVersion =
PROCESSED_MESSAGE_HASH_VERSION; PROCESSED_MESSAGE_HASH_VERSION;
graph.historyState.processedMessageHashes = {}; graph.historyState.processedMessageHashes = {};
graph.historyState.processedMessageHashesNeedRefresh = false; graph.historyState.processedMessageHashesNeedRefresh =
Number.isFinite(lastProcessedAssistantFloor) &&
lastProcessedAssistantFloor >= 0;
if (result) { if (result) {
graph.historyState.lastRecoveryResult = result; graph.historyState.lastRecoveryResult = result;
} }

View File

@@ -456,10 +456,17 @@ async function testManualExtractIgnoresFailedBatchWithoutPersistenceAttempt() {
async function testManualRebuildSetsTerminalRuntimeStatus() { async function testManualRebuildSetsTerminalRuntimeStatus() {
const chat = [{ is_user: true, mes: "u" }, { is_user: false, mes: "a" }]; const chat = [{ is_user: true, mes: "u" }, { is_user: false, mes: "a" }];
let savedHashes = null;
let savedNeedRefresh = null;
const context = { const context = {
...createBaseStatusContext(), ...createBaseStatusContext(),
__confirmHost: true, __confirmHost: true,
currentGraph: { currentGraph: {
historyState: {
lastProcessedAssistantFloor: -1,
processedMessageHashes: {},
processedMessageHashesNeedRefresh: false,
},
vectorIndexState: { vectorIndexState: {
lastWarning: "", lastWarning: "",
}, },
@@ -489,6 +496,11 @@ async function testManualRebuildSetsTerminalRuntimeStatus() {
}, },
createEmptyGraph() { createEmptyGraph() {
return { return {
historyState: {
lastProcessedAssistantFloor: -1,
processedMessageHashes: {},
processedMessageHashesNeedRefresh: false,
},
vectorIndexState: { vectorIndexState: {
lastWarning: "", lastWarning: "",
}, },
@@ -501,14 +513,31 @@ async function testManualRebuildSetsTerminalRuntimeStatus() {
clearInjectionState() {}, clearInjectionState() {},
async prepareVectorStateForReplay() {}, async prepareVectorStateForReplay() {},
async replayExtractionFromHistory() { async replayExtractionFromHistory() {
context.currentGraph.historyState.lastProcessedAssistantFloor = 1;
context.currentGraph.vectorIndexState.lastWarning = ""; context.currentGraph.vectorIndexState.lastWarning = "";
return 2; return 2;
}, },
clearHistoryDirty() {}, clearHistoryDirty(graph) {
graph.historyState.processedMessageHashes = {};
graph.historyState.processedMessageHashesNeedRefresh = true;
},
buildRecoveryResult(status, extra = {}) { buildRecoveryResult(status, extra = {}) {
return { 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() {}, restoreRuntimeUiState() {},
onRebuildController, onRebuildController,
result: null, result: null,
@@ -524,6 +553,11 @@ async function testManualRebuildSetsTerminalRuntimeStatus() {
assert.equal(context.lastExtractionStatus.text, "图谱重建完成"); assert.equal(context.lastExtractionStatus.text, "图谱重建完成");
assert.equal(context.runtimeStatus.text, "图谱重建完成"); assert.equal(context.runtimeStatus.text, "图谱重建完成");
assert.equal(context.runtimeStatus.level, "success"); assert.equal(context.runtimeStatus.level, "success");
assert.deepEqual(savedHashes, {
0: "u",
1: "a",
});
assert.equal(savedNeedRefresh, false);
} }
testIndexDefinesLastProcessedAssistantFloorHelper(); testIndexDefinesLastProcessedAssistantFloorHelper();

View File

@@ -1,6 +1,7 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { import {
appendBatchJournal, appendBatchJournal,
clearHistoryDirty,
cloneGraphSnapshot, cloneGraphSnapshot,
createBatchJournalEntry, createBatchJournalEntry,
detectHistoryMutation, detectHistoryMutation,
@@ -95,6 +96,17 @@ assert.equal(migratedGraph.historyState.processedMessageHashesNeedRefresh, true)
const migratedDetection = detectHistoryMutation(chat, migratedGraph.historyState); const migratedDetection = detectHistoryMutation(chat, migratedGraph.historyState);
assert.equal(migratedDetection.dirty, false); 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({ const importedGraph = normalizeGraphRuntimeState({
historyState: { historyState: {
chatId: "chat-history-test", chatId: "chat-history-test",
@@ -117,6 +129,19 @@ assert.deepEqual(
snapshotProcessedMessageHashes(chat, 3), 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 truncatedChat = chat.slice(0, 2);
const truncatedDetection = detectHistoryMutation(truncatedChat, { const truncatedDetection = detectHistoryMutation(truncatedChat, {
lastProcessedAssistantFloor: 3, lastProcessedAssistantFloor: 3,

View File

@@ -415,6 +415,22 @@ export async function onRebuildController(runtime) {
reason: "用户手动触发全量重建", 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.saveGraphToChat({ reason: "manual-rebuild-complete" });
runtime.setLastExtractionStatus( runtime.setLastExtractionStatus(
"图谱重建完成", "图谱重建完成",