diff --git a/index.js b/index.js index d46833b..9aebc59 100644 --- a/index.js +++ b/index.js @@ -320,6 +320,42 @@ function syncCommitMarkerToPersistenceState(context = getContext()) { return marker; } +function clearCurrentChatCommitMarker( + { + context = getContext(), + reason = "manual-clear-commit-marker", + immediate = true, + } = {}, +) { + if (!context) { + return { + cleared: false, + reason: "missing-context", + saveMode: "", + marker: null, + }; + } + + const marker = getChatCommitMarker(context); + writeChatMetadataPatch(context, { + [GRAPH_COMMIT_MARKER_KEY]: null, + }); + const saveMode = triggerChatMetadataSave(context, { immediate }); + updateGraphPersistenceState({ + commitMarker: null, + persistMismatchReason: "", + lastPersistReason: String(reason || "manual-clear-commit-marker"), + lastPersistMode: `commit-marker-clear:${saveMode}`, + }); + + return { + cleared: Boolean(marker), + reason: String(reason || "manual-clear-commit-marker"), + saveMode, + marker: cloneRuntimeDebugValue(marker, null), + }; +} + function isAcceptedPersistTier(storageTier = "none") { const normalizedTier = String(storageTier || "none").trim().toLowerCase(); return normalizedTier === "indexeddb" || normalizedTier === "chat-state"; @@ -13700,6 +13736,7 @@ const _cleanupRuntime = () => ({ }, clearCachedIndexedDbSnapshot, clearAllCachedIndexedDbSnapshots, + clearCurrentChatCommitMarker, deleteRemoteSyncFile: (chatId) => deleteRemoteSyncFile(chatId, { fetch: globalThis.fetch?.bind(globalThis), getRequestHeaders: typeof getRequestHeaders === "function" ? getRequestHeaders : undefined, diff --git a/tests/p0-regressions.mjs b/tests/p0-regressions.mjs index 6d4adc4..83d1241 100644 --- a/tests/p0-regressions.mjs +++ b/tests/p0-regressions.mjs @@ -67,6 +67,7 @@ import { shouldRunRecallForTransaction, } from "../ui/ui-status.js"; import { + onDeleteCurrentIdbController, onManualCompressController, onManualEvolveController, onManualSleepController, @@ -2036,6 +2037,93 @@ async function testBackendVectorQueryFailureMarksStateDirty() { } } +async function testDeleteCurrentIdbClearsCommitMarkerBeforeReload() { + const originalIndexedDb = globalThis.indexedDB; + const callLog = []; + globalThis.indexedDB = { + deleteDatabase(name) { + callLog.push(["delete-db", String(name || "")]); + const request = { + onsuccess: null, + onerror: null, + onblocked: null, + }; + queueMicrotask(() => { + request.onsuccess?.(); + }); + return request; + }, + }; + + try { + const runtime = { + confirm() { + return true; + }, + getCurrentChatId() { + return "chat-delete-idb"; + }, + buildBmeDbName(chatId) { + return `STBME_${chatId}`; + }, + buildRestoreSafetyDbName(chatId) { + return `STBME___restore__${chatId}`; + }, + async closeBmeDb(chatId) { + callLog.push(["close-db", chatId]); + }, + clearCachedIndexedDbSnapshot(chatId) { + callLog.push(["clear-indexeddb-cache", chatId]); + }, + clearCurrentChatCommitMarker(options = {}) { + callLog.push([ + "clear-commit-marker", + String(options.reason || ""), + options.immediate === true, + ]); + }, + syncGraphLoadFromLiveContext(options = {}) { + callLog.push([ + "sync-graph-load", + String(options.source || ""), + options.force === true, + ]); + }, + refreshPanelLiveState() { + callLog.push(["refresh-panel"]); + }, + toastr: { + success(message) { + callLog.push(["toast-success", String(message || "")]); + }, + warning(message) { + callLog.push(["toast-warning", String(message || "")]); + }, + error(message) { + callLog.push(["toast-error", String(message || "")]); + }, + }, + }; + + const result = await onDeleteCurrentIdbController(runtime); + assert.equal(result?.handledToast, true); + const clearMarkerIndex = callLog.findIndex( + (entry) => entry[0] === "clear-commit-marker", + ); + const syncLoadIndex = callLog.findIndex( + (entry) => entry[0] === "sync-graph-load", + ); + assert.ok(clearMarkerIndex >= 0, "删除当前 IDB 后应清理 commit marker"); + assert.ok(syncLoadIndex >= 0, "删除当前 IDB 后应重新同步图谱加载状态"); + assert.ok( + clearMarkerIndex < syncLoadIndex, + "应先清理 commit marker,再触发图谱重探测", + ); + } finally { + globalThis.indexedDB = originalIndexedDb; + } +} + async function testCompressTypeAcceptsTopLevelFieldsResult() { const graph = createEmptyGraph(); const typeDef = { @@ -6778,6 +6866,7 @@ async function testManualSleepExplainsThatItIsLocalOnlyWhenNothingChanges() { await testCompressorMigratesEdgesToCompressedNode(); await testVectorIndexKeepsDirtyOnDirectPartialEmbeddingFailure(); await testBackendVectorQueryFailureMarksStateDirty(); +await testDeleteCurrentIdbClearsCommitMarkerBeforeReload(); await testCompressTypeAcceptsTopLevelFieldsResult(); await testExtractorFailsOnUnknownOperation(); await testExtractorNormalizesFlatCreateOperation(); diff --git a/ui/ui-actions-controller.js b/ui/ui-actions-controller.js index 96930c7..e5d199d 100644 --- a/ui/ui-actions-controller.js +++ b/ui/ui-actions-controller.js @@ -1193,6 +1193,10 @@ export async function onDeleteCurrentIdbController(runtime) { }); } runtime.clearCachedIndexedDbSnapshot?.(chatId); + runtime.clearCurrentChatCommitMarker?.({ + reason: "manual-delete-current-idb", + immediate: true, + }); runtime.syncGraphLoadFromLiveContext?.({ source: "manual-delete-current-idb", force: true, @@ -1245,6 +1249,10 @@ export async function onDeleteAllIdbController(runtime) { runtime.clearAllCachedIndexedDbSnapshots?.(); const activeChatId = runtime.getCurrentChatId?.(); if (activeChatId) { + runtime.clearCurrentChatCommitMarker?.({ + reason: "manual-delete-all-idb", + immediate: true, + }); runtime.syncGraphLoadFromLiveContext?.({ source: "manual-delete-all-idb", force: true,