fix: clear stale accepted commit marker after deleting local IndexedDB caches

When users delete local BME IndexedDB via UI actions (delete current/all
IDB), the chat metadata's st_bme_commit_marker was not cleared. This left
an accepted high-revision promise with no local DB backing, causing
persist-mismatch:indexeddb-behind-commit-marker and blocking graph load
indefinitely.

- index.js: add clearCurrentChatCommitMarker() helper and expose via runtime
- ui-actions-controller.js: call clearCurrentChatCommitMarker before
  syncGraphLoadFromLiveContext after IDB deletion
- p0-regressions.mjs: regression test asserting marker is cleared before
  reload after current-IDB deletion
This commit is contained in:
Youzini-afk
2026-04-12 20:06:51 +08:00
parent 4643e0ad75
commit 05083ef5f0
3 changed files with 134 additions and 0 deletions

View File

@@ -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,

View File

@@ -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();

View File

@@ -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,