fix: auto-repair malformed memory scope regions

This commit is contained in:
Youzini-afk
2026-04-23 18:45:45 +08:00
parent ffd1cabb90
commit 0daf723fd1
8 changed files with 452 additions and 21 deletions

View File

@@ -92,6 +92,7 @@ import {
} from "../retrieval/recall-persistence.js";
import { getNodeDisplayName } from "../graph/node-labels.js";
import {
hasGraphPersistDirtyState,
normalizeGraphRuntimeState,
pruneGraphPersistDirtyState,
} from "../runtime/runtime-state.js";
@@ -1041,6 +1042,7 @@ async function createGraphPersistenceHarness({
buildSnapshotFromGraph,
evaluateNativeHydrateGate,
evaluatePersistNativeDeltaGate,
hasGraphPersistDirtyState,
pruneGraphPersistDirtyState,
buildBmeDbName,
BME_GRAPH_LOCAL_STORAGE_MODE_AUTO: "auto",

View File

@@ -15,6 +15,7 @@ import {
} from "../sync/bme-db.js";
import { BmeChatManager } from "../sync/bme-chat-manager.js";
import { createEmptyGraph } from "../graph/graph.js";
import { getGraphPersistDirtyStateSnapshot } from "../runtime/runtime-state.js";
const PREFIX = "[ST-BME][indexeddb-persistence]";
@@ -744,6 +745,29 @@ async function testGraphSnapshotConverters() {
const rebuiltMalformedButFlagged = buildGraphFromSnapshot(malformedButFlaggedSnapshot, {
chatId: "chat-a",
});
const scopeRepairSnapshot = {
...snapshot,
meta: {
...snapshot.meta,
},
nodes: [
{
...snapshot.nodes[0],
scope: {
layer: "objective",
regionPrimary: "王都/钟楼",
regionSecondary: "旧城区 / 集市 / 钟楼",
},
},
],
};
delete scopeRepairSnapshot.meta[BME_RUNTIME_RECORDS_NORMALIZED_META_KEY];
const rebuiltScopeRepair = buildGraphFromSnapshot(scopeRepairSnapshot, {
chatId: "chat-a",
});
const scopeRepairDirtyState = getGraphPersistDirtyStateSnapshot(
rebuiltScopeRepair,
);
assert.equal(rebuilt.historyState.lastProcessedAssistantFloor, 9);
assert.equal(rebuilt.historyState.extractionCount, 4);
assert.equal(rebuilt.nodes.length, 1);
@@ -764,6 +788,23 @@ async function testGraphSnapshotConverters() {
assert.equal(rebuiltMalformedButFlagged.nodes[0].scope?.layer, "objective");
assert.equal(rebuiltMalformedButFlagged.nodes[0].storyTime?.tense, "unknown");
assert.equal(rebuiltMalformedButFlagged.nodes[0].storyTimeSpan?.mixed, false);
assert.equal(rebuiltScopeRepair.nodes[0].scope?.regionPrimary, "钟楼");
assert.deepEqual(rebuiltScopeRepair.nodes[0].scope?.regionPath, ["王都", "钟楼"]);
assert.deepEqual(rebuiltScopeRepair.nodes[0].scope?.regionSecondary, [
"旧城区",
"集市",
]);
assert.equal(
scopeRepairDirtyState?.nodeUpsertIds?.includes("node-converter"),
true,
);
assert.equal(rebuiltScopeRepair.vectorIndexState?.dirty, true);
assert.equal(
rebuiltScopeRepair.vectorIndexState?.replayRequiredNodeIds?.includes(
"node-converter",
),
true,
);
rebuilt.nodes[0].fields.title = "Mutated Converter Node";
rebuilt.nodes[0].embedding[0] = 99;

View File

@@ -8,7 +8,11 @@ import {
findLatestNode,
serializeGraph,
} from "../graph/graph.js";
import { normalizeMemoryScope } from "../graph/memory-scope.js";
import {
buildRegionLine,
getScopeRegionTokens,
normalizeMemoryScope,
} from "../graph/memory-scope.js";
import {
normalizeStoryTime,
normalizeStoryTimeSpan,
@@ -73,6 +77,61 @@ assert.equal(
"已规范的 scope 对象应直接复用",
);
const malformedSecondaryScope = normalizeMemoryScope({
layer: "objective",
regionPrimary: "王都/钟楼",
regionSecondary: "旧城区, 集市 / 下水道 / 钟楼",
});
assert.equal(malformedSecondaryScope.regionPrimary, "钟楼");
assert.deepEqual(malformedSecondaryScope.regionPath, ["王都", "钟楼"]);
assert.deepEqual(malformedSecondaryScope.regionSecondary, [
"旧城区",
"集市",
"下水道",
]);
assert.deepEqual(getScopeRegionTokens(malformedSecondaryScope), [
"钟楼",
"王都",
"旧城区",
"集市",
"下水道",
]);
assert.match(buildRegionLine(malformedSecondaryScope), /次级地区/);
const accessorBackedScope = {};
Object.defineProperty(accessorBackedScope, "layer", {
get() {
return "objective";
},
enumerable: true,
});
Object.defineProperty(accessorBackedScope, "regionPrimary", {
get() {
return "钟楼";
},
enumerable: true,
});
Object.defineProperty(accessorBackedScope, "regionPath", {
get() {
return "王都 > 钟楼";
},
enumerable: true,
});
Object.defineProperty(accessorBackedScope, "regionSecondary", {
get() {
return { label: "旧城区 / 集市" };
},
enumerable: true,
});
const normalizedAccessorScope = normalizeMemoryScope(accessorBackedScope);
assert.notEqual(
normalizedAccessorScope,
accessorBackedScope,
"带 accessor 的 scope 不应复用原对象",
);
assert.deepEqual(normalizedAccessorScope.regionPath, ["王都", "钟楼"]);
assert.deepEqual(normalizedAccessorScope.regionSecondary, ["旧城区", "集市"]);
const normalizedStoryTime = {
segmentId: "tl-1",
label: "第二天清晨",