From c66717d218beab0bfc3d9f061f187c824c504222 Mon Sep 17 00:00:00 2001 From: serhiizghama Date: Tue, 23 Jun 2026 03:11:51 +0700 Subject: [PATCH 1/2] fix: reindex search after import so KV and indices stay in sync A replace import wipes KV but leaves the BM25/vector indices holding the deleted entries, and data imported by any strategy is written straight to KV without being indexed. Until the next process restart, smart-search returns ghosts of replaced-out records and misses everything just imported. Rebuild both indices from KV at the end of a successful import via the existing rebuildIndex helper, which clears and repopulates them to match KV exactly. A reindex failure is logged but does not fail the import, since the imported data is already durable in KV. --- src/functions/export-import.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/functions/export-import.ts b/src/functions/export-import.ts index 327117b26..e96c9bbae 100644 --- a/src/functions/export-import.ts +++ b/src/functions/export-import.ts @@ -29,6 +29,7 @@ import { KV } from "../state/schema.js"; import { StateKV } from "../state/kv.js"; import { VERSION } from "../version.js"; import { recordAudit } from "./audit.js"; +import { rebuildIndex } from "./search.js"; import { logger } from "../logger.js"; export function registerExportImportFunction(sdk: ISdk, kv: StateKV): void { @@ -576,6 +577,24 @@ export function registerExportImportFunction(sdk: ISdk, kv: StateKV): void { } } + // KV is now the source of truth, but the in-memory search indices still + // reflect the pre-import state: a "replace" wipe leaves ghosts of deleted + // entries in BM25/vector, and freshly imported entries from any strategy + // were written straight to KV without being indexed. Rebuild both indices + // from KV so smart-search reflects the imported data immediately instead + // of only after a process restart. + try { + const indexed = await rebuildIndex(kv); + logger.info("Import reindex complete", { strategy, indexed }); + } catch (err) { + // The import itself succeeded and is durable in KV; a reindex failure + // must not fail the import. Surface it so the gap is diagnosable. + logger.warn("Import reindex failed", { + strategy, + error: err instanceof Error ? err.message : String(err), + }); + } + logger.info("Import complete", { strategy, ...stats }); await recordAudit(kv, "import", "mem::import", [], { strategy, From 5870b7ea92c6b744d0a1665dcb067d3977145f07 Mon Sep 17 00:00:00 2001 From: serhiizghama Date: Tue, 23 Jun 2026 03:11:51 +0700 Subject: [PATCH 2/2] test: cover search reindex after replace import Asserts that a replaced-out memory no longer surfaces from search and an imported memory is searchable immediately, without a process restart. --- test/export-import.test.ts | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/export-import.test.ts b/test/export-import.test.ts index 394986269..da3574a91 100644 --- a/test/export-import.test.ts +++ b/test/export-import.test.ts @@ -5,6 +5,7 @@ vi.mock("../src/logger.js", () => ({ })); import { registerExportImportFunction } from "../src/functions/export-import.js"; +import { getSearchIndex, rebuildIndex } from "../src/functions/search.js"; import type { Session, CompressedObservation, @@ -249,4 +250,44 @@ describe("Export/Import Functions", () => { expect(result.success).toBe(false); expect(result.error).toContain("Unsupported export version"); }); + + it("replace import reindexes search: drops ghosts and indexes imported data", async () => { + // Live search index starts reflecting the pre-import KV state. + const index = getSearchIndex(); + index.clear(); + await rebuildIndex(kv); + // Sanity: the existing memory ("Always validate tokens") is searchable. + expect(index.search("validate").length).toBeGreaterThan(0); + + const importedMemory: Memory = { + ...testMemory, + id: "mem_imported", + title: "Database connection pooling", + content: "Reuse postgres connections efficiently", + concepts: ["database"], + }; + const exportData: ExportData = { + version: "0.3.0", + exportedAt: new Date().toISOString(), + sessions: [], + observations: {}, + memories: [importedMemory], + summaries: [], + }; + + const result = (await sdk.trigger("mem::import", { + exportData, + strategy: "replace", + })) as { success: boolean; memories: number }; + + expect(result.success).toBe(true); + expect(result.memories).toBe(1); + + // Ghost gone: the replaced-out memory must no longer surface from search. + expect(index.search("validate")).toEqual([]); + // Imported data is searchable immediately, without a process restart. + const hits = index.search("postgres"); + expect(hits.length).toBeGreaterThan(0); + expect(hits.some((h) => h.obsId === "mem_imported")).toBe(true); + }); });