refactor: deepen indexeddb-first load path and finalize phase ef docs/scripts

This commit is contained in:
Youzini-afk
2026-03-30 20:00:38 +08:00
parent 1c76ad7a11
commit 10c6db258b
4 changed files with 175 additions and 492 deletions

View File

@@ -284,18 +284,21 @@ git clone https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git st-bme
```
ST-BME/
├── index.js # 主入口:事件绑定、流程调度、历史恢复
├── bme-db.js # IndexedDB(Dexie) 持久化层(本地主存储)
├── bme-sync.js # 跨设备同步(/user/files/ 镜像 + 合并)
├── bme-chat-manager.js# chatId -> BmeDatabase 生命周期管理
├── lib/dexie.min.js # vendored Dexie浏览器端动态注入加载
├── graph.js # 图数据模型、序列化、版本迁移
├── extractor.js # 记忆提取、概要、反思
├── retriever.js # 向量候选、图扩散、混合评分、召回
├── injector.js # 召回结果格式化注入
├── runtime-state.js # 运行时状态:楼层 hash、dirty 标记、恢复日志
├── recall-persistence.js # 持久召回记录message.extra.bme_recall
├── recall-message-ui.js # 消息级召回卡片 UI子图渲染 + 侧边栏编辑)
├── recall-persistence.js # 持久召回记录message.extra.bme_recall,不变
├── recall-message-ui.js # 消息级召回卡片 UI子图渲染 + 侧边栏编辑,不变
├── vector-index.js # 向量索引管理backend / direct 双模式)
├── embedding.js # 直连 Embedding API 封装
├── llm.js # 记忆 LLM 请求封装
├── compressor.js # 层级压缩与遗忘
├── evolution.js # 记忆进化A-MEM 风格)
├── diffusion.js # 图扩散算法
├── dynamics.js # 动态调节(重要度衰减等)
├── schema.js # 节点类型定义
@@ -310,19 +313,40 @@ ST-BME/
### 数据存储
- **图谱数据** → `chat_metadata.st_bme_graph`(跟随聊天保存
- **图谱主存储(本地优先)** → `IndexedDB`Dexie
- DB 名固定:`STBME_{chatId}`
- 运行时主读取路径:优先 IndexedDB
- **跨设备同步镜像** → SillyTavern 文件 API `/user/files/`
- 文件名固定:`ST-BME_sync_{sanitizedChatId}.json`
- 冲突合并:`updatedAt` 新者胜tombstone `deletedAt` 优先;`lastProcessedFloor/extractionCount` 取 `max`
- `meta` 为同步 JSON 顶层首字段,`revision` 全程单调递增
- **兼容兜底(迁移窗口)** → `chat_metadata.st_bme_graph`
- 仅用于 legacy 兼容与迁移,不再是主路径
- **墓碑tombstones** → 保留期固定 30 天
- **插件设置** → SillyTavern 的 `extension_settings.st_bme`
- **向量索引** → 后端模式走酒馆 API直连模式存在节点内
- **召回持久注入** → `chat[x].extra.bme_recall`(消息级)
- **召回持久注入(不变)** → `chat[x].extra.bme_recall`(消息级)
### 兼容迁移策略legacy metadata → IndexedDB
- 触发:聊天加载/切换后,若目标 `STBME_{chatId}` 为空且存在 legacy `chat_metadata` 图谱
- 行为:自动一次性迁移到 IndexedDB并立即尝试同步到 `/user/files/`
- 幂等:
- 若 `migrationCompletedAt > 0`,跳过
- 若 IndexedDB 已非空,跳过
- 迁移记录:
- `migrationCompletedAt`
- `migrationSource`(默认 `chat_metadata`
- `legacyRetentionUntil`30 天)
### 事件挂载
| SillyTavern 事件 | 做什么 |
|---|---|
| `CHAT_CHANGED` | 加载对应聊天的图谱 |
| `CHAT_CHANGED` | IndexedDB 优先加载 + 自动同步 |
| `GENERATION_AFTER_COMMANDS` | AI 回复后提取记忆 |
| `GENERATE_BEFORE_COMBINE_PROMPTS` | 生成前召回并注入 |
| `MESSAGE_RECEIVED` | 保存图谱状态 |
| `MESSAGE_RECEIVED` | 触发图谱持久化IndexedDB 主写) |
| 删除 / 编辑 / Swipe | 触发历史变动检测与恢复 |
### 召回流水线

468
index.js
View File

@@ -3836,6 +3836,7 @@ function loadGraphFromChat(options = {}) {
attemptIndex = 0,
expectedChatId = "",
source = "direct-load",
allowMetadataFallback = true,
} = options;
const context = getContext();
const chatIdentity = resolveCurrentChatIdentity(context);
@@ -3861,28 +3862,7 @@ function loadGraphFromChat(options = {}) {
};
}
if (chatId) {
const cachedSnapshot = readCachedIndexedDbSnapshot(chatId);
if (isIndexedDbSnapshotMeaningful(cachedSnapshot)) {
const cachedResult = applyIndexedDbSnapshotToRuntime(chatId, cachedSnapshot, {
source: `${source}:indexeddb-cache`,
attemptIndex,
});
if (cachedResult?.loaded) {
clearPendingGraphLoadRetry();
return cachedResult;
}
}
scheduleIndexedDbGraphProbe(chatId, {
source: `${source}:indexeddb-probe`,
attemptIndex,
allowOverride: false,
});
}
if (!chatId) {
const shouldRetry = attemptIndex < GRAPH_LOAD_RETRY_DELAYS_MS.length;
if (chatIdentity.hasLikelySelectedChat) {
currentGraph = normalizeGraphRuntimeState(createEmptyGraph(), "");
extractionCount = 0;
@@ -3891,35 +3871,27 @@ function loadGraphFromChat(options = {}) {
lastInjectionContent = "";
runtimeStatus = createUiStatus(
"图谱加载中",
"正在等待当前聊天会话 ID 与元数据就绪",
shouldRetry ? "running" : "warning",
"正在等待当前聊天会话 ID 就绪",
"running",
);
lastExtractionStatus = createUiStatus(
"待命",
shouldRetry
? "正在等待当前聊天会话 ID 就绪"
: "当前聊天会话 ID 长时间未就绪,已暂停修改图谱",
shouldRetry ? "idle" : "warning",
"正在等待当前聊天会话 ID 就绪",
"idle",
);
lastVectorStatus = createUiStatus(
"待命",
shouldRetry
? "正在等待当前聊天会话 ID 就绪"
: "当前聊天会话 ID 长时间未就绪,已暂停修改图谱",
shouldRetry ? "idle" : "warning",
"正在等待当前聊天会话 ID 就绪",
"idle",
);
lastRecallStatus = createUiStatus(
"待命",
shouldRetry
? "正在等待当前聊天会话 ID 就绪"
: "当前聊天会话 ID 长时间未就绪,已暂停图谱写回",
shouldRetry ? "idle" : "warning",
"正在等待当前聊天会话 ID 就绪",
"idle",
);
applyGraphLoadState(
shouldRetry ? GRAPH_LOAD_STATES.LOADING : GRAPH_LOAD_STATES.BLOCKED,
{
applyGraphLoadState(GRAPH_LOAD_STATES.LOADING, {
chatId: "",
reason: shouldRetry ? "chat-id-missing" : "chat-id-timeout",
reason: "chat-id-missing",
attemptIndex,
revision: 0,
lastPersistedRevision: 0,
@@ -3930,24 +3902,15 @@ function loadGraphFromChat(options = {}) {
shadowSnapshotRevision: 0,
shadowSnapshotUpdatedAt: "",
shadowSnapshotReason: "",
dbReady: false,
writesBlocked: true,
},
);
if (shouldRetry) {
scheduleGraphLoadRetry("", "chat-id-missing", attemptIndex, {
allowPendingChat: true,
});
} else {
clearPendingGraphLoadRetry();
}
refreshPanelLiveState();
return {
success: false,
loaded: false,
loadState: shouldRetry
? GRAPH_LOAD_STATES.LOADING
: GRAPH_LOAD_STATES.BLOCKED,
reason: shouldRetry ? "chat-id-missing" : "chat-id-timeout",
loadState: GRAPH_LOAD_STATES.LOADING,
reason: "chat-id-missing",
chatId: "",
attemptIndex,
};
@@ -3978,6 +3941,7 @@ function loadGraphFromChat(options = {}) {
shadowSnapshotReason: "",
writesBlocked: true,
});
refreshPanelLiveState();
return {
success: false,
@@ -3989,139 +3953,48 @@ function loadGraphFromChat(options = {}) {
};
}
const hasChatMetadata =
context?.chatMetadata &&
typeof context.chatMetadata === "object" &&
!Array.isArray(context.chatMetadata);
const metadataReady = isHostChatMetadataReady(context);
const savedData = hasChatMetadata
? context.chatMetadata[GRAPH_METADATA_KEY]
: undefined;
const hasOfficialGraph = savedData != null && savedData !== "";
const shadowSnapshot = readGraphShadowSnapshot(chatId);
const shouldRetry = attemptIndex < GRAPH_LOAD_RETRY_DELAYS_MS.length;
const cachedSnapshot = readCachedIndexedDbSnapshot(chatId);
if (isIndexedDbSnapshotMeaningful(cachedSnapshot)) {
const cachedResult = applyIndexedDbSnapshotToRuntime(chatId, cachedSnapshot, {
source: `${source}:indexeddb-cache`,
attemptIndex,
});
if (cachedResult?.loaded) {
clearPendingGraphLoadRetry();
return cachedResult;
}
}
if (hasOfficialGraph) {
const savedData = allowMetadataFallback
? context?.chatMetadata?.[GRAPH_METADATA_KEY]
: undefined;
if (savedData != null && savedData !== "") {
try {
const officialGraph = cloneGraphForPersistence(
normalizeGraphRuntimeState(deserializeGraph(savedData), chatId),
chatId,
);
const officialRevision = Math.max(
1,
getGraphPersistedRevision(officialGraph),
);
const metadataIntegrity = getChatMetadataIntegrity(context);
if (shouldPreferShadowSnapshotOverOfficial(officialGraph, shadowSnapshot)) {
clearPendingGraphLoadRetry();
currentGraph = cloneGraphForPersistence(
normalizeGraphRuntimeState(
deserializeGraph(shadowSnapshot.serializedGraph),
chatId,
),
chatId,
);
extractionCount = Number.isFinite(
currentGraph?.historyState?.extractionCount,
)
? currentGraph.historyState.extractionCount
: 0;
lastExtractedItems = [];
updateLastRecalledItems(currentGraph.lastRecallResult || []);
lastInjectionContent = "";
runtimeStatus = createUiStatus(
"待命",
"已恢复本次会话里较新的图谱,正在补写正式聊天元数据",
"warning",
);
lastExtractionStatus = createUiStatus(
"待命",
"检测到本次会话里有更新的图谱快照,正在补写正式元数据",
"warning",
);
lastVectorStatus = createUiStatus(
"待命",
"检测到本次会话里有更新的图谱快照,正在补写正式元数据",
"warning",
);
lastRecallStatus = createUiStatus(
"待命",
"检测到本次会话里有更新的图谱快照,正在补写正式元数据",
"warning",
);
applyGraphLoadState(GRAPH_LOAD_STATES.LOADED, {
chatId,
reason: "shadow-snapshot-newer-than-official",
attemptIndex,
revision: Math.max(shadowSnapshot.revision || 0, 1),
lastPersistedRevision: officialRevision,
queuedPersistRevision: Math.max(shadowSnapshot.revision || 0, 1),
pendingPersist: true,
shadowSnapshotUsed: true,
shadowSnapshotRevision: Math.max(shadowSnapshot.revision || 0, 1),
shadowSnapshotUpdatedAt: shadowSnapshot.updatedAt,
shadowSnapshotReason: shadowSnapshot.reason,
writesBlocked: false,
});
updateGraphPersistenceState({
metadataIntegrity,
queuedPersistMode: "immediate",
queuedPersistRotateIntegrity: false,
queuedPersistReason: "shadow-snapshot-newer-than-official",
storagePrimary: "metadata",
storageMode: "metadata",
indexedDbRevision: Math.max(graphPersistenceState.indexedDbRevision || 0, officialRevision),
indexedDbLastError: "",
});
const persistResult = maybeFlushQueuedGraphPersist(
"shadow-snapshot-newer-than-official",
);
refreshPanelLiveState();
return {
success: Boolean(persistResult.saved),
loaded: true,
loadState: GRAPH_LOAD_STATES.LOADED,
reason: "shadow-snapshot-newer-than-official",
chatId,
attemptIndex,
shadowSnapshotUsed: true,
};
}
const officialRevision = Math.max(1, getGraphPersistedRevision(officialGraph));
clearPendingGraphLoadRetry();
currentGraph = officialGraph;
extractionCount = Number.isFinite(
currentGraph?.historyState?.extractionCount,
)
extractionCount = Number.isFinite(currentGraph?.historyState?.extractionCount)
? currentGraph.historyState.extractionCount
: 0;
lastExtractedItems = [];
updateLastRecalledItems(currentGraph.lastRecallResult || []);
lastInjectionContent = "";
runtimeStatus = createUiStatus(
"待命",
"已加载聊天图谱,等待下一次任务",
"idle",
);
lastExtractionStatus = createUiStatus(
"待命",
"已加载聊天图谱,等待下一次提取",
"idle",
);
runtimeStatus = createUiStatus("待命", "已从兼容 metadata 加载图谱", "idle");
lastExtractionStatus = createUiStatus("待命", "已加载聊天图谱,等待下一次提取", "idle");
lastVectorStatus = createUiStatus(
"待命",
currentGraph.vectorIndexState?.lastWarning ||
"已加载聊天图谱,等待下一次向量任务",
"idle",
);
lastRecallStatus = createUiStatus(
"待命",
"已加载聊天图谱,等待下一次召回",
currentGraph.vectorIndexState?.lastWarning || "已加载聊天图谱,等待下一次向量任务",
"idle",
);
lastRecallStatus = createUiStatus("待命", "已加载聊天图谱,等待下一次召回", "idle");
applyGraphLoadState(GRAPH_LOAD_STATES.LOADED, {
chatId,
reason: source,
reason: `${source}:metadata-compat`,
attemptIndex,
revision: officialRevision,
lastPersistedRevision: officialRevision,
@@ -4132,281 +4005,66 @@ function loadGraphFromChat(options = {}) {
shadowSnapshotRevision: 0,
shadowSnapshotUpdatedAt: "",
shadowSnapshotReason: "",
dbReady: true,
writesBlocked: false,
});
updateGraphPersistenceState({
metadataIntegrity,
lastPersistMode: "",
queuedPersistMode: "",
queuedPersistRotateIntegrity: false,
queuedPersistReason: "",
metadataIntegrity: getChatMetadataIntegrity(context),
storagePrimary: "metadata",
storageMode: "metadata",
dbReady: true,
indexedDbLastError: "",
});
removeGraphShadowSnapshot(chatId);
console.log("[ST-BME] 从聊天数据加载图谱:", {
chatId,
source,
scheduleIndexedDbGraphProbe(chatId, {
source: `${source}:indexeddb-probe`,
attemptIndex,
...getGraphStats(currentGraph),
allowOverride: true,
applyEmptyState: true,
});
refreshPanelLiveState();
return {
success: true,
loaded: true,
loadState: GRAPH_LOAD_STATES.LOADED,
reason: source,
reason: `${source}:metadata-compat`,
chatId,
attemptIndex,
shadowSnapshotUsed: false,
};
} catch (error) {
console.warn("[ST-BME] 兼容 metadata 图谱读取失败,将回退 IndexedDB:", error);
}
}
if (shadowSnapshot) {
currentGraph = normalizeGraphRuntimeState(
deserializeGraph(shadowSnapshot.serializedGraph),
applyGraphLoadState(GRAPH_LOAD_STATES.LOADING, {
chatId,
);
extractionCount = Number.isFinite(
currentGraph?.historyState?.extractionCount,
)
? currentGraph.historyState.extractionCount
: 0;
lastExtractedItems = [];
updateLastRecalledItems(currentGraph.lastRecallResult || []);
lastInjectionContent = "";
runtimeStatus = createUiStatus("待命", "已从本次会话临时恢复图谱", "idle");
lastExtractionStatus = createUiStatus(
"待命",
"图谱处于临时恢复状态,等待正式元数据",
"warning",
);
lastVectorStatus = createUiStatus(
"待命",
"图谱处于临时恢复状态,等待正式元数据",
"warning",
);
lastRecallStatus = createUiStatus(
"待命",
"图谱处于临时恢复状态,等待正式元数据",
"warning",
);
if (metadataReady) {
applyGraphLoadState(GRAPH_LOAD_STATES.LOADED, {
chatId,
reason: "shadow-snapshot-promoted",
reason: `indexeddb-probe-pending:${String(source || "direct-load")}`,
attemptIndex,
revision: Math.max(shadowSnapshot.revision || 0, 1),
lastPersistedRevision: 0,
queuedPersistRevision: Math.max(shadowSnapshot.revision || 0, 1),
pendingPersist: true,
shadowSnapshotUsed: true,
shadowSnapshotRevision: Math.max(shadowSnapshot.revision || 0, 1),
shadowSnapshotUpdatedAt: shadowSnapshot.updatedAt,
shadowSnapshotReason: shadowSnapshot.reason,
writesBlocked: false,
});
updateGraphPersistenceState({
metadataIntegrity: getChatMetadataIntegrity(context),
queuedPersistMode: "immediate",
queuedPersistRotateIntegrity: false,
queuedPersistReason: "shadow-snapshot-promoted",
storagePrimary: "metadata",
storageMode: "metadata",
indexedDbLastError: "",
});
const persistResult = maybeFlushQueuedGraphPersist(
"shadow-snapshot-promoted",
);
refreshPanelLiveState();
return {
success: Boolean(persistResult.saved),
loaded: true,
loadState: GRAPH_LOAD_STATES.LOADED,
reason: "shadow-snapshot-promoted",
chatId,
attemptIndex,
shadowSnapshotUsed: true,
};
}
const shadowState = shouldRetry
? GRAPH_LOAD_STATES.SHADOW_RESTORED
: GRAPH_LOAD_STATES.BLOCKED;
applyGraphLoadState(shadowState, {
chatId,
reason: shouldRetry
? "shadow-snapshot-restored"
: "shadow-snapshot-blocked",
attemptIndex,
revision: Math.max(shadowSnapshot.revision || 0, 1),
lastPersistedRevision: 0,
queuedPersistRevision: Math.max(shadowSnapshot.revision || 0, 1),
pendingPersist: true,
shadowSnapshotUsed: true,
shadowSnapshotRevision: Math.max(shadowSnapshot.revision || 0, 1),
shadowSnapshotUpdatedAt: shadowSnapshot.updatedAt,
shadowSnapshotReason: shadowSnapshot.reason,
dbReady: false,
writesBlocked: true,
});
updateGraphPersistenceState({
queuedPersistMode: "immediate",
queuedPersistRotateIntegrity: false,
queuedPersistReason: shouldRetry
? "shadow-snapshot-restored"
: "shadow-snapshot-blocked",
storagePrimary: "metadata",
storageMode: "metadata",
storagePrimary: "indexeddb",
storageMode: "indexeddb",
dbReady: false,
indexedDbLastError: "",
});
if (shouldRetry) {
scheduleGraphLoadRetry(
chatId,
hasChatMetadata
? "official-graph-missing-shadow"
: "chat-metadata-missing-shadow",
scheduleIndexedDbGraphProbe(chatId, {
source: `${source}:indexeddb-probe`,
attemptIndex,
);
} else {
clearPendingGraphLoadRetry();
}
refreshPanelLiveState();
return {
success: false,
loaded: false,
loadState: shadowState,
reason: graphPersistenceState.reason,
chatId,
attemptIndex,
shadowSnapshotUsed: true,
};
}
currentGraph = normalizeGraphRuntimeState(createEmptyGraph(), chatId);
extractionCount = 0;
lastExtractedItems = [];
lastRecalledItems = [];
lastInjectionContent = "";
if (!metadataReady) {
runtimeStatus = createUiStatus(
"待命",
shouldRetry
? "正在加载当前聊天图谱,暂不写回图谱元数据"
: "聊天元数据未就绪,已暂停图谱写回",
shouldRetry ? "idle" : "warning",
);
lastExtractionStatus = createUiStatus(
"待命",
shouldRetry
? "正在等待聊天图谱元数据加载"
: "聊天元数据未就绪,暂不允许修改图谱",
shouldRetry ? "idle" : "warning",
);
lastVectorStatus = createUiStatus(
"待命",
shouldRetry
? "正在等待聊天图谱元数据加载"
: "聊天元数据未就绪,暂不允许修改图谱",
shouldRetry ? "idle" : "warning",
);
lastRecallStatus = createUiStatus(
"待命",
shouldRetry
? "正在等待聊天图谱元数据加载"
: "聊天元数据未就绪,图谱处于保护状态",
shouldRetry ? "idle" : "warning",
);
applyGraphLoadState(
shouldRetry ? GRAPH_LOAD_STATES.LOADING : GRAPH_LOAD_STATES.BLOCKED,
{
chatId,
reason: hasChatMetadata
? shouldRetry
? "graph-metadata-missing"
: "graph-metadata-timeout"
: shouldRetry
? "chat-metadata-missing"
: "chat-metadata-timeout",
attemptIndex,
revision: 0,
lastPersistedRevision: 0,
queuedPersistRevision: 0,
queuedPersistChatId: "",
pendingPersist: false,
shadowSnapshotUsed: false,
shadowSnapshotRevision: 0,
shadowSnapshotUpdatedAt: "",
shadowSnapshotReason: "",
writesBlocked: true,
},
);
if (shouldRetry) {
scheduleGraphLoadRetry(
chatId,
hasChatMetadata ? "graph-metadata-missing" : "chat-metadata-missing",
attemptIndex,
);
} else {
clearPendingGraphLoadRetry();
}
refreshPanelLiveState();
return {
success: false,
loaded: false,
loadState: shouldRetry
? GRAPH_LOAD_STATES.LOADING
: GRAPH_LOAD_STATES.BLOCKED,
reason: graphPersistenceState.reason,
chatId,
attemptIndex,
shadowSnapshotUsed: false,
};
}
clearPendingGraphLoadRetry();
const confirmedState = GRAPH_LOAD_STATES.EMPTY_CONFIRMED;
runtimeStatus = createUiStatus("待命", "当前聊天还没有图谱", "idle");
lastExtractionStatus = createUiStatus("待命", "当前聊天尚未执行提取", "idle");
lastVectorStatus = createUiStatus("待命", "当前聊天尚未执行向量任务", "idle");
lastRecallStatus = createUiStatus("待命", "当前聊天尚未建立记忆图谱", "idle");
applyGraphLoadState(confirmedState, {
chatId,
reason: "metadata-confirmed-empty",
attemptIndex,
revision: 0,
lastPersistedRevision: 0,
queuedPersistRevision: 0,
queuedPersistChatId: "",
pendingPersist: false,
shadowSnapshotUsed: false,
shadowSnapshotRevision: 0,
shadowSnapshotUpdatedAt: "",
shadowSnapshotReason: "",
writesBlocked: false,
allowOverride: true,
applyEmptyState: true,
});
updateGraphPersistenceState({
metadataIntegrity: getChatMetadataIntegrity(context),
queuedPersistMode: "",
queuedPersistRotateIntegrity: false,
queuedPersistReason: "",
storagePrimary: "metadata",
storageMode: "metadata",
indexedDbLastError: "",
});
removeGraphShadowSnapshot(chatId);
refreshPanelLiveState();
return {
success: false,
loaded: false,
loadState: confirmedState,
reason: graphPersistenceState.reason,
loadState: GRAPH_LOAD_STATES.LOADING,
reason: "indexeddb-probe-pending",
chatId,
attemptIndex,
shadowSnapshotUsed: false,
};
}

View File

@@ -1,4 +1,14 @@
{
"scripts": {
"test:p0": "node tests/p0-regressions.mjs",
"test:graph-persistence": "node tests/graph-persistence.mjs",
"test:indexeddb-persistence": "node tests/indexeddb-persistence.mjs",
"test:indexeddb-sync": "node tests/indexeddb-sync.mjs",
"test:indexeddb-migration": "node tests/indexeddb-migration.mjs",
"test:indexeddb": "npm run test:indexeddb-persistence && npm run test:indexeddb-sync && npm run test:indexeddb-migration",
"test:all": "npm run test:p0 && npm run test:graph-persistence && npm run test:indexeddb",
"check": "node --check index.js && node --check bme-db.js && node --check panel.js && node --check ui-status.js && node --check event-binding.js"
},
"dependencies": {
"triviumdb": "^0.4.41"
},

View File

@@ -651,12 +651,12 @@ result = {
assert.equal(
result.loadState,
"loading",
"无 integrity 的占位 metadata 不能视作 ready",
"无图谱数据时应进入 IndexedDB 探测等待态",
);
assert.equal(
result.reason,
"graph-metadata-missing",
"应继续等待正式 graph metadata",
"indexeddb-probe-pending",
"应继续等待 IndexedDB 探测结果",
);
assert.equal(live.writesBlocked, true);
}
@@ -673,11 +673,11 @@ result = {
source: "metadata-chatid-ready",
});
assert.equal(result.loadState, "empty-confirmed");
assert.equal(result.loadState, "loading");
assert.equal(
harness.api.getGraphPersistenceLiveState().writesBlocked,
false,
"当 metadata 提供 chatId/sessionId 等强信号时,可进入 ready-empty",
true,
"无 IndexedDB 命中时应维持 loading 等待探测结果",
);
}
@@ -862,11 +862,11 @@ result = {
source: "shadow-test",
});
assert.equal(result.loadState, "shadow-restored");
assert.equal(reader.api.getCurrentGraph().nodes.length, 1);
assert.equal(result.loadState, "loading");
assert.equal(reader.api.getCurrentGraph(), null);
assert.equal(
reader.api.getGraphPersistenceLiveState().shadowSnapshotUsed,
true,
false,
);
assert.equal(reader.api.getGraphPersistenceLiveState().writesBlocked, true);
}
@@ -907,9 +907,9 @@ result = {
"事件-official",
);
assert.equal(
reader.api.readGraphShadowSnapshot("chat-official"),
null,
"正式元数据到位后应清理影子快照",
reader.api.readGraphShadowSnapshot("chat-official")?.reason,
"stale-shadow",
"metadata 兼容加载时保留影子快照仅作为兼容数据,不参与主链路",
);
}
@@ -944,26 +944,21 @@ result = {
});
assert.equal(result.loadState, "loaded");
assert.equal(result.reason, "shadow-snapshot-newer-than-official");
assert.equal(result.reason, "official-older-than-shadow:metadata-compat");
assert.equal(
reader.api.getCurrentGraph().nodes[0]?.fields?.title,
"事件-shadow-newer",
"事件-official-older",
);
assert.equal(reader.runtimeContext.__contextImmediateSaveCalls, 1);
assert.equal(reader.runtimeContext.__contextImmediateSaveCalls, 0);
assert.equal(
reader.runtimeContext.__chatContext.chatMetadata?.st_bme_graph?.nodes?.[0]
?.fields?.title,
"事件-shadow-newer",
"事件-official-older",
);
assert.equal(
reader.runtimeContext.__chatContext.chatMetadata?.integrity,
"integrity-official-older",
"影子快照补写正式图谱时不能改写宿主 metadata.integrity",
);
assert.equal(
reader.api.readGraphShadowSnapshot("chat-shadow-newer"),
null,
"影子快照补写成功后应被清理",
reader.api.readGraphShadowSnapshot("chat-shadow-newer")?.reason,
"pagehide-refresh",
"metadata 兼容加载后影子快照可保留,但不作为主链路恢复来源",
);
}
@@ -980,13 +975,13 @@ result = {
});
const live = harness.api.getGraphPersistenceLiveState();
assert.equal(result.loadState, "empty-confirmed");
assert.equal(live.writesBlocked, false);
assert.equal(live.canWriteToMetadata, true);
assert.equal(harness.api.getCurrentGraph().nodes.length, 0);
assert.equal(result.loadState, "loading");
assert.equal(result.reason, "indexeddb-probe-pending");
assert.equal(live.writesBlocked, true);
assert.equal(harness.api.getCurrentGraph(), null);
assert.equal(
harness.api.readRuntimeDebugSnapshot().graphPersistence?.loadState,
"empty-confirmed",
"loading",
);
}
@@ -1017,7 +1012,7 @@ result = {
assert.equal(
harness.runtimeContext.__chatContext.chatMetadata?.st_bme_graph,
undefined,
"empty-confirmed 状态下不能把空图被动写回 metadata",
"loading 状态下不能把空图被动写回 metadata",
);
}
@@ -1088,20 +1083,16 @@ result = {
});
const live = reader.api.getGraphPersistenceLiveState();
assert.equal(result.loadState, "loaded");
assert.equal(result.loadState, "loading");
assert.equal(
reader.runtimeContext.__chatContext.chatMetadata?.st_bme_graph?.nodes
?.length,
1,
undefined,
);
assert.equal(
reader.runtimeContext.__chatContext.chatMetadata?.integrity,
"meta-ready-promote",
"metadata 就绪后提升影子快照时不能改写宿主 metadata.integrity",
);
assert.equal(reader.runtimeContext.__contextImmediateSaveCalls, 1);
assert.equal(reader.runtimeContext.__chatContext.chatMetadata?.integrity, "meta-ready-promote");
assert.equal(reader.runtimeContext.__contextImmediateSaveCalls, 0);
assert.equal(reader.runtimeContext.__contextSaveCalls, 0);
assert.equal(live.lastPersistedRevision, 9);
assert.equal(live.lastPersistedRevision, 0);
assert.equal(live.pendingPersist, false);
}
@@ -1255,8 +1246,8 @@ result = {
runtimeGraph.nodes[0].fields.title = "runtime-shadow-mutated";
assert.equal(
persistedGraph.nodes[0].fields.title,
"事件-shadow",
"shadow 恢复后的运行时修改不能污染已补写 metadata",
"事件-official-older",
"metadata 兼容加载后的运行时修改不能污染已保存 metadata",
);
}