Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/functions/export-import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
41 changes: 41 additions & 0 deletions test/export-import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
});
});