From a35237ae6ddf55d1fd989670207af3d9763aca83 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Fri, 17 Apr 2026 20:17:25 +0800 Subject: [PATCH] fix: preserve history frontier in opfs delta commits --- sync/bme-db.js | 49 ++++++++--- tests/opfs-write-serialization.mjs | 134 +++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 10 deletions(-) diff --git a/sync/bme-db.js b/sync/bme-db.js index 3abc6c7..2f54e3a 100644 --- a/sync/bme-db.js +++ b/sync/bme-db.js @@ -205,18 +205,47 @@ function normalizePersistSnapshotView(snapshot = {}) { }; } + const hasExplicitMeta = + snapshot.meta && + typeof snapshot.meta === "object" && + !Array.isArray(snapshot.meta); + const hasExplicitState = + snapshot.state && + typeof snapshot.state === "object" && + !Array.isArray(snapshot.state); + const shouldDeriveFromRuntimeGraph = + !hasExplicitMeta && + !hasExplicitState && + (Object.prototype.hasOwnProperty.call(snapshot, "historyState") || + Object.prototype.hasOwnProperty.call(snapshot, "vectorIndexState") || + Object.prototype.hasOwnProperty.call(snapshot, "batchJournal") || + Object.prototype.hasOwnProperty.call(snapshot, "maintenanceJournal") || + Object.prototype.hasOwnProperty.call(snapshot, "summaryState") || + Object.prototype.hasOwnProperty.call(snapshot, "knowledgeState") || + Object.prototype.hasOwnProperty.call(snapshot, "regionState") || + Object.prototype.hasOwnProperty.call(snapshot, "timelineState") || + Object.prototype.hasOwnProperty.call(snapshot, "lastRecallResult") || + Object.prototype.hasOwnProperty.call(snapshot, "lastProcessedSeq")); + const derivedRuntimeSnapshot = shouldDeriveFromRuntimeGraph + ? buildSnapshotFromGraph(snapshot, { + chatId: normalizeChatId(snapshot?.historyState?.chatId || ""), + }) + : null; + return { - meta: - snapshot.meta && - typeof snapshot.meta === "object" && - !Array.isArray(snapshot.meta) - ? snapshot.meta + meta: hasExplicitMeta + ? snapshot.meta + : derivedRuntimeSnapshot?.meta && + typeof derivedRuntimeSnapshot.meta === "object" && + !Array.isArray(derivedRuntimeSnapshot.meta) + ? derivedRuntimeSnapshot.meta : {}, - state: - snapshot.state && - typeof snapshot.state === "object" && - !Array.isArray(snapshot.state) - ? snapshot.state + state: hasExplicitState + ? snapshot.state + : derivedRuntimeSnapshot?.state && + typeof derivedRuntimeSnapshot.state === "object" && + !Array.isArray(derivedRuntimeSnapshot.state) + ? derivedRuntimeSnapshot.state : {}, nodes: toArray(snapshot.nodes), edges: toArray(snapshot.edges), diff --git a/tests/opfs-write-serialization.mjs b/tests/opfs-write-serialization.mjs index 8d31647..9cf6dff 100644 --- a/tests/opfs-write-serialization.mjs +++ b/tests/opfs-write-serialization.mjs @@ -1,11 +1,80 @@ import assert from "node:assert/strict"; +import { buildPersistDelta } from "../sync/bme-db.js"; import { BME_GRAPH_LOCAL_STORAGE_MODE_OPFS_PRIMARY, OpfsGraphStore, } from "../sync/bme-opfs-store.js"; import { createMemoryOpfsRoot } from "./helpers/memory-opfs.mjs"; +function createRuntimeGraphLikeSnapshot({ + chatId, + lastProcessedFloor, + extractionCount, + nodes = [], + edges = [], + batchJournal = [], +} = {}) { + return { + version: 9, + lastProcessedSeq: lastProcessedFloor, + nodes, + edges, + historyState: { + chatId, + lastProcessedAssistantFloor: lastProcessedFloor, + extractionCount, + processedMessageHashVersion: 2, + processedMessageHashes: {}, + processedMessageHashesNeedRefresh: false, + historyDirtyFrom: null, + lastMutationReason: "", + lastMutationSource: "", + lastRecoveryResult: null, + lastBatchStatus: null, + lastExtractedRegion: "", + activeRegion: "", + activeRegionSource: "", + activeStorySegmentId: "", + activeStoryTimeLabel: "", + activeStoryTimeSource: "", + lastExtractedStorySegmentId: "", + activeCharacterPovOwner: "", + activeUserPovOwner: "", + activeRecallOwnerKey: "", + recentRecallOwnerKeys: [], + }, + vectorIndexState: { + mode: "backend", + collectionId: `st-bme::${chatId}`, + source: "", + modelScope: "", + hashToNodeId: {}, + nodeToHash: {}, + dirty: false, + replayRequiredNodeIds: [], + dirtyReason: "", + pendingRepairFromFloor: null, + lastSyncAt: 0, + lastStats: { + total: 0, + indexed: 0, + stale: 0, + pending: 0, + }, + lastWarning: "", + lastIntegrityIssue: null, + }, + batchJournal, + maintenanceJournal: [], + knowledgeState: {}, + regionState: {}, + timelineState: {}, + summaryState: {}, + lastRecallResult: null, + }; +} + async function testCommitDeltaAndPatchMetaSerialize() { const rootDirectory = createMemoryOpfsRoot({ writeDelayMs: 5, @@ -128,6 +197,71 @@ async function testImportSnapshotAndClearAllSerialize() { assert.equal(snapshot.state.extractionCount, 4); } +async function testGraphLikeDeltaPreservesHistoryFrontier() { + const rootDirectory = createMemoryOpfsRoot(); + const store = new OpfsGraphStore("chat-opfs-graph-like-delta", { + rootDirectoryFactory: async () => rootDirectory, + storeMode: BME_GRAPH_LOCAL_STORAGE_MODE_OPFS_PRIMARY, + }); + await store.open(); + + const beforeGraph = createRuntimeGraphLikeSnapshot({ + chatId: "chat-opfs-graph-like-delta", + lastProcessedFloor: -1, + extractionCount: 0, + }); + const afterGraph = createRuntimeGraphLikeSnapshot({ + chatId: "chat-opfs-graph-like-delta", + lastProcessedFloor: 12, + extractionCount: 3, + nodes: [ + { + id: "node-graph-like", + type: "event", + fields: { title: "graph-like" }, + archived: false, + updatedAt: 12, + }, + ], + batchJournal: [ + { + id: "journal-1", + processedRange: [12, 12], + }, + ], + }); + + const delta = buildPersistDelta(beforeGraph, afterGraph, { + useNativeDelta: false, + }); + assert.equal(delta.runtimeMetaPatch.lastProcessedFloor, 12); + assert.equal(delta.runtimeMetaPatch.extractionCount, 3); + assert.equal( + delta.runtimeMetaPatch.runtimeHistoryState?.lastProcessedAssistantFloor, + 12, + ); + + await store.commitDelta(delta, { + reason: "graph-like-delta", + requestedRevision: 1, + markSyncDirty: true, + }); + + const reopenedStore = new OpfsGraphStore("chat-opfs-graph-like-delta", { + rootDirectoryFactory: async () => rootDirectory, + storeMode: BME_GRAPH_LOCAL_STORAGE_MODE_OPFS_PRIMARY, + }); + await reopenedStore.open(); + const snapshot = await reopenedStore.exportSnapshot(); + assert.equal(snapshot.state.lastProcessedFloor, 12); + assert.equal(snapshot.state.extractionCount, 3); + assert.equal( + snapshot.meta.runtimeHistoryState?.lastProcessedAssistantFloor, + 12, + ); +} + await testCommitDeltaAndPatchMetaSerialize(); await testImportSnapshotAndClearAllSerialize(); +await testGraphLikeDeltaPreservesHistoryFrontier(); console.log("opfs-write-serialization tests passed");