mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-14 02:40:45 +08:00
fix(vector): recover after embedding model changes
This commit is contained in:
@@ -1691,6 +1691,9 @@ result = {
|
||||
getGraphPersistenceState() {
|
||||
return graphPersistenceState;
|
||||
},
|
||||
getPanelRuntimeStatus,
|
||||
getGraphMutationBlockReason,
|
||||
ensureGraphMutationReady,
|
||||
setLocalStoreCapabilitySnapshot(patch = {}) {
|
||||
bmeLocalStoreCapabilitySnapshot = {
|
||||
...bmeLocalStoreCapabilitySnapshot,
|
||||
@@ -4199,6 +4202,70 @@ result = {
|
||||
assert.equal(unrepairedStatus.persistence.blocked, true);
|
||||
}
|
||||
|
||||
{
|
||||
const graph = createMeaningfulGraph(
|
||||
"chat-runtime-fallback-vector-maintenance",
|
||||
"runtime-fallback-vector-maintenance",
|
||||
);
|
||||
graph.historyState.chatId = "chat-runtime-fallback-vector-maintenance";
|
||||
const harness = await createGraphPersistenceHarness({
|
||||
chatId: "",
|
||||
globalChatId: "",
|
||||
chat: [
|
||||
{ is_user: true, mes: "已有聊天" },
|
||||
{ is_user: false, mes: "已有回复" },
|
||||
],
|
||||
});
|
||||
harness.api.setCurrentGraph(graph);
|
||||
harness.api.setGraphPersistenceState({
|
||||
loadState: GRAPH_LOAD_STATES.NO_CHAT,
|
||||
chatId: "chat-runtime-fallback-vector-maintenance",
|
||||
dbReady: false,
|
||||
writesBlocked: true,
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
harness.api.ensureGraphMutationReady("重建向量", {
|
||||
notify: false,
|
||||
allowRuntimeGraphFallback: true,
|
||||
}),
|
||||
true,
|
||||
"live chat id 暂空但 runtime graph 已明确绑定聊天时,向量维护不应被误判为未进入聊天",
|
||||
);
|
||||
const status = harness.api.getPanelRuntimeStatus();
|
||||
assert.equal(status.text, "图谱已加载");
|
||||
assert.match(status.meta, /维护操作会使用图谱身份继续/);
|
||||
}
|
||||
|
||||
{
|
||||
const graph = createMeaningfulGraph(
|
||||
"chat-runtime-fallback-denied",
|
||||
"runtime-fallback-denied",
|
||||
);
|
||||
graph.historyState.chatId = "chat-runtime-fallback-denied";
|
||||
const harness = await createGraphPersistenceHarness({
|
||||
chatId: "",
|
||||
globalChatId: "",
|
||||
chat: [{ is_user: true, mes: "其它上下文" }],
|
||||
});
|
||||
harness.api.setCurrentGraph(graph);
|
||||
harness.api.setGraphPersistenceState({
|
||||
loadState: GRAPH_LOAD_STATES.NO_CHAT,
|
||||
chatId: "",
|
||||
dbReady: false,
|
||||
writesBlocked: true,
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
harness.api.ensureGraphMutationReady("重建向量", {
|
||||
notify: false,
|
||||
allowRuntimeGraphFallback: true,
|
||||
}),
|
||||
false,
|
||||
"没有 graphPersistenceState.chatId 强绑定时,不应仅凭 runtimeGraph/chat 内容放开写入",
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const harness = await createGraphPersistenceHarness({
|
||||
chatId: "chat-pending-persist-already-accepted",
|
||||
|
||||
@@ -69,10 +69,12 @@ import {
|
||||
} from "../ui/ui-status.js";
|
||||
import {
|
||||
onClearGraphController,
|
||||
onClearVectorCacheController,
|
||||
onDeleteCurrentIdbController,
|
||||
onManualCompressController,
|
||||
onManualEvolveController,
|
||||
onManualSleepController,
|
||||
onRebuildVectorIndexController,
|
||||
} from "../ui/ui-actions-controller.js";
|
||||
import { createGenerationRecallHarness } from "./helpers/generation-recall-harness.mjs";
|
||||
|
||||
@@ -7980,6 +7982,97 @@ async function testManualCompressSkipsWithoutCandidatesAndDoesNotPretendItRan()
|
||||
assert.match(String(toastMessages[0]?.[1] || ""), /未发起 LLM 压缩/);
|
||||
}
|
||||
|
||||
async function testClearVectorCacheLeavesActionableRebuildState() {
|
||||
const statuses = [];
|
||||
const toasts = [];
|
||||
let savedReason = "";
|
||||
const graph = {
|
||||
nodes: [
|
||||
{ id: "n1", embedding: [1, 2, 3] },
|
||||
{ id: "n2", embedding: [4, 5, 6] },
|
||||
],
|
||||
vectorIndexState: {
|
||||
hashToNodeId: { h1: "n1", h2: "n2" },
|
||||
nodeToHash: { n1: "h1", n2: "h2" },
|
||||
currentVectorSpace: { vectorSpaceId: "old", observedDim: 3 },
|
||||
manifest: { status: "clean", observedDim: 3, lastError: "" },
|
||||
dirty: false,
|
||||
dirtyReason: "",
|
||||
lastWarning: "",
|
||||
lastStats: { total: 2, indexed: 2, stale: 0, pending: 0 },
|
||||
},
|
||||
};
|
||||
|
||||
const result = await onClearVectorCacheController({
|
||||
confirm: () => true,
|
||||
getCurrentGraph: () => graph,
|
||||
saveGraphToChat(payload = {}) {
|
||||
savedReason = payload.reason;
|
||||
},
|
||||
setLastVectorStatus(...args) {
|
||||
statuses.push(args);
|
||||
},
|
||||
refreshPanelLiveState() {},
|
||||
toastr: {
|
||||
warning(message) {
|
||||
toasts.push(message);
|
||||
},
|
||||
success(message) {
|
||||
toasts.push(message);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result?.handledToast, true);
|
||||
assert.deepEqual(graph.vectorIndexState.hashToNodeId, {});
|
||||
assert.deepEqual(graph.vectorIndexState.nodeToHash, {});
|
||||
assert.equal(graph.nodes[0].embedding, null);
|
||||
assert.equal(graph.nodes[1].embedding, null);
|
||||
assert.equal(graph.vectorIndexState.currentVectorSpace, null);
|
||||
assert.equal(graph.vectorIndexState.manifest.status, "stale");
|
||||
assert.equal(graph.vectorIndexState.manifest.lastError, "manual-clear-vector-cache");
|
||||
assert.equal(graph.vectorIndexState.dirty, true);
|
||||
assert.equal(graph.vectorIndexState.dirtyReason, "manual-clear-vector-cache");
|
||||
assert.equal(graph.vectorIndexState.lastStats.total, 2);
|
||||
assert.equal(graph.vectorIndexState.lastStats.indexed, 0);
|
||||
assert.equal(graph.vectorIndexState.lastStats.pending, 2);
|
||||
assert.equal(statuses[0]?.[0], "向量需要重建");
|
||||
assert.match(String(statuses[0]?.[1] || ""), /降级为非向量召回/);
|
||||
assert.equal(savedReason, "manual-clear-vector-cache");
|
||||
assert.match(String(toasts[0] || ""), /重建向量/);
|
||||
}
|
||||
|
||||
async function testRebuildVectorBlockedExplainsVisibleChatMismatch() {
|
||||
const toasts = [];
|
||||
const calls = [];
|
||||
|
||||
const result = await onRebuildVectorIndexController({
|
||||
ensureGraphMutationReady(label, options = {}) {
|
||||
calls.push(["ensureGraphMutationReady", label, options]);
|
||||
return false;
|
||||
},
|
||||
getGraphMutationBlockReason: () => "重建向量已暂停:当前尚未进入聊天。",
|
||||
getGraphPersistenceState: () => ({ chatId: "chat-visible" }),
|
||||
toastr: {
|
||||
warning(message) {
|
||||
toasts.push(message);
|
||||
},
|
||||
info(message) {
|
||||
toasts.push(message);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result?.blocked, true);
|
||||
assert.equal(result?.handledToast, true);
|
||||
assert.equal(calls[0]?.[0], "ensureGraphMutationReady");
|
||||
assert.equal(calls[0]?.[1], "重建向量");
|
||||
assert.equal(calls[0]?.[2]?.notify, false);
|
||||
assert.equal(calls[0]?.[2]?.allowRuntimeGraphFallback, true);
|
||||
assert.match(String(toasts[0] || ""), /chat-visible/);
|
||||
assert.match(String(toasts[0] || ""), /重新探测图谱/);
|
||||
}
|
||||
|
||||
async function testManualCompressUsesForcedCompressionAndPersistsRealMutation() {
|
||||
const calls = {
|
||||
forceFlag: null,
|
||||
@@ -8339,6 +8432,8 @@ await testSynopsisUsesPromptMessagesWithoutFallbackSystemPrompt();
|
||||
await testRecallUsesSectionedPromptMessagesForContextAndTarget();
|
||||
await testReflectionUsesPromptMessagesWithoutFallbackSystemPrompt();
|
||||
await testManualCompressSkipsWithoutCandidatesAndDoesNotPretendItRan();
|
||||
await testClearVectorCacheLeavesActionableRebuildState();
|
||||
await testRebuildVectorBlockedExplainsVisibleChatMismatch();
|
||||
await testManualCompressUsesForcedCompressionAndPersistsRealMutation();
|
||||
await testManualCompressUpdatesRuntimeStatusForPanelUi();
|
||||
await testManualEvolveFallsBackToLatestExtractionBatchAfterRefresh();
|
||||
|
||||
@@ -17,13 +17,16 @@ installResolveHooks([
|
||||
]);
|
||||
|
||||
let embeddingDim = 3;
|
||||
let embeddingFailureIndexes = new Set();
|
||||
globalThis.__stBmeTestOverrides = {
|
||||
embedding: {
|
||||
async embedBatch(texts = []) {
|
||||
return texts.map((text, index) =>
|
||||
Array.from({ length: embeddingDim }, (_, dimIndex) =>
|
||||
dimIndex === 0 ? 1 : (index + dimIndex + String(text || "").length) / 100,
|
||||
),
|
||||
embeddingFailureIndexes.has(index)
|
||||
? null
|
||||
: Array.from({ length: embeddingDim }, (_, dimIndex) =>
|
||||
dimIndex === 0 ? 1 : (index + dimIndex + String(text || "").length) / 100,
|
||||
),
|
||||
);
|
||||
},
|
||||
async embedText(text = "") {
|
||||
@@ -103,6 +106,7 @@ const baseConfig = {
|
||||
const graph = createVectorGraph();
|
||||
graph.nodes[0].embedding = [0.1, 0.2, 0.3];
|
||||
embeddingDim = 3;
|
||||
embeddingFailureIndexes = new Set();
|
||||
const changedModelConfig = { ...baseConfig, model: "text-embedding-3-large" };
|
||||
await syncGraphVectorIndex(graph, changedModelConfig, { chatId: graph.historyState.chatId });
|
||||
assert.equal(graph.vectorIndexState.manifest.status, "clean");
|
||||
@@ -112,4 +116,63 @@ const baseConfig = {
|
||||
assert.notDeepEqual(graph.nodes[0].embedding, [0.1, 0.2, 0.3]);
|
||||
}
|
||||
|
||||
{
|
||||
const graph = createVectorGraph();
|
||||
graph.nodes[0].embedding = [0.1, 0.2, 0.3];
|
||||
graph.vectorIndexState.mode = "direct";
|
||||
graph.vectorIndexState.modelScope = getVectorModelScope(baseConfig);
|
||||
graph.vectorIndexState.collectionId = "st-bme-vector-chat-vector-manifest";
|
||||
graph.vectorIndexState.manifest = {
|
||||
status: "clean",
|
||||
vectorSpaceId: "old-space",
|
||||
observedDim: 3,
|
||||
model: baseConfig.model,
|
||||
};
|
||||
embeddingDim = 4;
|
||||
embeddingFailureIndexes = new Set([0]);
|
||||
const changedModelConfig = { ...baseConfig, model: "text-embedding-3-large" };
|
||||
await syncGraphVectorIndex(graph, changedModelConfig, { chatId: graph.historyState.chatId });
|
||||
assert.equal(graph.nodes[0].embedding, null);
|
||||
assert.equal(graph.vectorIndexState.dirty, true);
|
||||
assert.equal(graph.vectorIndexState.dirtyReason, "partial-embedding-failure");
|
||||
assert.equal(graph.vectorIndexState.lastStats.indexed, 0);
|
||||
|
||||
embeddingFailureIndexes = new Set([0]);
|
||||
await syncGraphVectorIndex(graph, changedModelConfig, { chatId: graph.historyState.chatId });
|
||||
assert.equal(
|
||||
graph.vectorIndexState.lastStats.indexed,
|
||||
0,
|
||||
"模型变化后的旧 embedding 不应在后续非 force 同步中被重新登记",
|
||||
);
|
||||
assert.equal(graph.nodes[0].embedding, null);
|
||||
}
|
||||
|
||||
{
|
||||
const graph = createVectorGraph();
|
||||
graph.nodes[0].embedding = [0.1, 0.2, 0.3];
|
||||
graph.vectorIndexState.mode = "direct";
|
||||
graph.vectorIndexState.source = "direct";
|
||||
graph.vectorIndexState.modelScope = getVectorModelScope(baseConfig);
|
||||
graph.vectorIndexState.collectionId = "st-bme-vector-chat-vector-manifest";
|
||||
graph.vectorIndexState.hashToNodeId = { oldHash: "node-a" };
|
||||
graph.vectorIndexState.nodeToHash = { "node-a": "oldHash" };
|
||||
graph.vectorIndexState.manifest = {
|
||||
status: "clean",
|
||||
vectorSpaceId: "old-space",
|
||||
observedDim: 3,
|
||||
model: baseConfig.model,
|
||||
};
|
||||
embeddingDim = 4;
|
||||
embeddingFailureIndexes = new Set([0]);
|
||||
await syncGraphVectorIndex(graph, baseConfig, {
|
||||
chatId: graph.historyState.chatId,
|
||||
force: true,
|
||||
});
|
||||
assert.equal(graph.nodes[0].embedding, null);
|
||||
assert.deepEqual(graph.vectorIndexState.nodeToHash, {});
|
||||
assert.deepEqual(graph.vectorIndexState.hashToNodeId, {});
|
||||
assert.equal(graph.vectorIndexState.lastStats.indexed, 0);
|
||||
assert.equal(graph.vectorIndexState.lastStats.pending, 1);
|
||||
}
|
||||
|
||||
console.log("vector-manifest tests passed");
|
||||
|
||||
Reference in New Issue
Block a user