From 4bf571ba37f89592544ea92f00a20a0928c35d2f Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sun, 29 Mar 2026 21:39:58 +0800 Subject: [PATCH] fix: harden metadata readiness and add persistence self-heal reconcile --- event-binding.js | 11 +++++ index.js | 35 ++++++++++++--- tests/graph-persistence.mjs | 85 +++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 5 deletions(-) diff --git a/event-binding.js b/event-binding.js index 197b3ff..677ce18 100644 --- a/event-binding.js +++ b/event-binding.js @@ -263,6 +263,17 @@ export async function onBeforeCombinePromptsController(runtime) { } export function onMessageReceivedController(runtime) { + const loadState = runtime.getGraphPersistenceState?.()?.loadState || ""; + if ( + loadState === "loading" || + loadState === "shadow-restored" || + loadState === "blocked" + ) { + runtime.syncGraphLoadFromLiveContext?.({ + source: "message-received-reconcile", + }); + } + if (runtime.getCurrentGraph()) { if ( runtime.getGraphPersistenceState()?.pendingPersist && diff --git a/index.js b/index.js index 53892a2..83b9d43 100644 --- a/index.js +++ b/index.js @@ -1780,6 +1780,32 @@ function hasLikelySelectedChatContext(context = getContext()) { ); } +function hasHostMetadataReadySignal(metadata = {}) { + if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) { + return false; + } + + if (normalizeChatIdCandidate(metadata.integrity)) { + return true; + } + + const chatIdentityCandidates = [ + metadata.chat_id, + metadata.chatId, + metadata.session_id, + metadata.sessionId, + ]; + if ( + chatIdentityCandidates.some((candidate) => + Boolean(normalizeChatIdCandidate(candidate)), + ) + ) { + return true; + } + + return false; +} + function isHostChatMetadataReady(context = getContext()) { if ( !context?.chatMetadata || @@ -1790,12 +1816,10 @@ function isHostChatMetadataReady(context = getContext()) { } const metadata = context.chatMetadata; - // SillyTavern 在 CHAT_CHANGED 之前会为已加载聊天补上 integrity。 - if (normalizeChatIdCandidate(metadata.integrity)) { - return true; - } + // 仅接受宿主“强信号”,避免把中间态/占位 metadata 误判为 ready。 + if (hasHostMetadataReadySignal(metadata)) return true; - return Object.keys(metadata).length > 0; + return false; } function resolveCurrentChatIdentity(context = getContext()) { @@ -5139,6 +5163,7 @@ function onMessageReceived() { isAssistantChatMessage, isFreshRecallInputRecord, isGraphMetadataWriteAllowed, + syncGraphLoadFromLiveContext, maybeCaptureGraphShadowSnapshot, maybeFlushQueuedGraphPersist, notifyExtractionIssue, diff --git a/tests/graph-persistence.mjs b/tests/graph-persistence.mjs index 4f23d76..9e609f9 100644 --- a/tests/graph-persistence.mjs +++ b/tests/graph-persistence.mjs @@ -526,6 +526,52 @@ result = { assert.equal(result.loadState, "empty-confirmed"); } +{ + const harness = await createGraphPersistenceHarness({ + chatId: "chat-metadata-placeholder", + chatMetadata: { + placeholder: "host-loading", + }, + }); + const result = harness.api.loadGraphFromChat({ + attemptIndex: 0, + source: "metadata-placeholder-not-ready", + }); + const live = harness.api.getGraphPersistenceLiveState(); + + assert.equal( + result.loadState, + "loading", + "无 integrity 的占位 metadata 不能视作 ready", + ); + assert.equal( + result.reason, + "graph-metadata-missing", + "应继续等待正式 graph metadata", + ); + assert.equal(live.writesBlocked, true); +} + +{ + const harness = await createGraphPersistenceHarness({ + chatId: "chat-metadata-chatid-ready", + chatMetadata: { + chatId: "chat-metadata-chatid-ready", + }, + }); + const result = harness.api.loadGraphFromChat({ + attemptIndex: 0, + source: "metadata-chatid-ready", + }); + + assert.equal(result.loadState, "empty-confirmed"); + assert.equal( + harness.api.getGraphPersistenceLiveState().writesBlocked, + false, + "当 metadata 提供 chatId/sessionId 等强信号时,可进入 ready-empty", + ); +} + { const harness = await createGraphPersistenceHarness({ chatId: "", @@ -640,6 +686,45 @@ result = { ); } +{ + const harness = await createGraphPersistenceHarness({ + chatId: "chat-late-reconcile", + chatMetadata: undefined, + }); + harness.api.setCurrentGraph( + normalizeGraphRuntimeState(createEmptyGraph(), "chat-late-reconcile"), + ); + harness.api.setGraphPersistenceState({ + loadState: "blocked", + chatId: "chat-late-reconcile", + reason: "chat-metadata-timeout", + revision: 2, + writesBlocked: true, + }); + harness.api.setChatContext({ + ...harness.api.getChatContext(), + chatId: "chat-late-reconcile", + chatMetadata: { + integrity: "chat-late-reconcile-ready", + st_bme_graph: createMeaningfulGraph("chat-late-reconcile", "late-official"), + }, + }); + + harness.api.onMessageReceived(); + + const live = harness.api.getGraphPersistenceLiveState(); + assert.equal( + live.loadState, + "loaded", + "BLOCKED 后 onMessageReceived 应触发元数据重探测并自动恢复", + ); + assert.equal(live.writesBlocked, false); + assert.equal( + harness.api.getCurrentGraph().nodes[0]?.fields?.title, + "事件-late-official", + ); +} + { const sharedSession = new Map(); const writer = await createGraphPersistenceHarness({