diff --git a/mem0-ts/src/oss/src/config/manager.ts b/mem0-ts/src/oss/src/config/manager.ts index c58bd01ec..4a35539f7 100644 --- a/mem0-ts/src/oss/src/config/manager.ts +++ b/mem0-ts/src/oss/src/config/manager.ts @@ -95,16 +95,34 @@ export class ConfigManager { })(), }, historyDbPath: - userConfig.historyDbPath || DEFAULT_MEMORY_CONFIG.historyDbPath, + userConfig.historyDbPath || + userConfig.historyStore?.config?.historyDbPath || + DEFAULT_MEMORY_CONFIG.historyStore?.config?.historyDbPath, customPrompt: userConfig.customPrompt, graphStore: { ...DEFAULT_MEMORY_CONFIG.graphStore, ...userConfig.graphStore, }, - historyStore: { - ...DEFAULT_MEMORY_CONFIG.historyStore, - ...userConfig.historyStore, - }, + historyStore: (() => { + const defaultHistoryStore = DEFAULT_MEMORY_CONFIG.historyStore!; + const historyProvider = + userConfig.historyStore?.provider || defaultHistoryStore.provider; + const isSqlite = historyProvider.toLowerCase() === "sqlite"; + + // Precedence: explicit historyStore.config > top-level historyDbPath > default + return { + ...defaultHistoryStore, + ...userConfig.historyStore, + provider: historyProvider, + config: { + ...(isSqlite ? defaultHistoryStore.config : {}), + ...(isSqlite && userConfig.historyDbPath + ? { historyDbPath: userConfig.historyDbPath } + : {}), + ...userConfig.historyStore?.config, + }, + }; + })(), disableHistory: userConfig.disableHistory || DEFAULT_MEMORY_CONFIG.disableHistory, enableGraph: userConfig.enableGraph || DEFAULT_MEMORY_CONFIG.enableGraph, diff --git a/mem0-ts/src/oss/src/memory/index.ts b/mem0-ts/src/oss/src/memory/index.ts index 46ca8bf2a..9eabc8357 100644 --- a/mem0-ts/src/oss/src/memory/index.ts +++ b/mem0-ts/src/oss/src/memory/index.ts @@ -70,20 +70,10 @@ export class Memory { if (this.config.disableHistory) { this.db = new DummyHistoryManager(); } else { - const defaultConfig = { - provider: "sqlite", - config: { - historyDbPath: this.config.historyDbPath || ":memory:", - }, - }; - - this.db = - this.config.historyStore && !this.config.disableHistory - ? HistoryManagerFactory.create( - this.config.historyStore.provider, - this.config.historyStore, - ) - : HistoryManagerFactory.create("sqlite", defaultConfig); + this.db = HistoryManagerFactory.create( + this.config.historyStore!.provider, + this.config.historyStore!, + ); } this.collectionName = this.config.vectorStore.config.collectionName; diff --git a/mem0-ts/src/oss/src/storage/SQLiteManager.ts b/mem0-ts/src/oss/src/storage/SQLiteManager.ts index 89c2a6d17..6950093a4 100644 --- a/mem0-ts/src/oss/src/storage/SQLiteManager.ts +++ b/mem0-ts/src/oss/src/storage/SQLiteManager.ts @@ -1,5 +1,6 @@ import Database from "better-sqlite3"; import { HistoryManager } from "./base"; +import { ensureSQLiteDirectory } from "../utils/sqlite"; export class SQLiteManager implements HistoryManager { private db: Database.Database; @@ -7,6 +8,7 @@ export class SQLiteManager implements HistoryManager { private stmtSelect!: Database.Statement; constructor(dbPath: string) { + ensureSQLiteDirectory(dbPath); this.db = new Database(dbPath); this.init(); } diff --git a/mem0-ts/src/oss/src/tests/sqlite-backward-compat.test.ts b/mem0-ts/src/oss/src/tests/sqlite-backward-compat.test.ts new file mode 100644 index 000000000..3c81f8953 --- /dev/null +++ b/mem0-ts/src/oss/src/tests/sqlite-backward-compat.test.ts @@ -0,0 +1,400 @@ +/** + * Backward-compatibility tests for SQLite path handling changes. + * + * These tests verify that every documented and common usage pattern + * from before the fix continues to work identically after the change. + */ +import fs from "fs"; +import os from "os"; +import path from "path"; +import { ConfigManager } from "../config/manager"; +import { SQLiteManager } from "../storage/SQLiteManager"; +import { MemoryVectorStore } from "../vector_stores/memory"; +import { + ensureSQLiteDirectory, + getDefaultVectorStoreDbPath, +} from "../utils/sqlite"; + +function normalize(vector: number[]): number[] { + const norm = Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0)); + return vector.map((value) => value / norm); +} + +// --------------------------------------------------------------------------- +// 1. Config merging – existing patterns must keep working +// --------------------------------------------------------------------------- + +describe("backward compat: ConfigManager.mergeConfig", () => { + it("empty config returns all expected defaults", () => { + const cfg = ConfigManager.mergeConfig({}); + + expect(cfg.version).toBe("v1.1"); + expect(cfg.embedder.provider).toBe("openai"); + expect(cfg.vectorStore.provider).toBe("memory"); + expect(cfg.vectorStore.config.collectionName).toBe("memories"); + expect(cfg.vectorStore.config.dimension).toBe(1536); + expect(cfg.llm.provider).toBe("openai"); + expect(cfg.historyStore).toBeDefined(); + expect(cfg.historyStore!.provider).toBe("sqlite"); + expect(cfg.historyStore!.config.historyDbPath).toBe("memory.db"); + expect(cfg.disableHistory).toBe(false); + expect(cfg.enableGraph).toBe(false); + }); + + it("workaround: explicit historyStore still works (existing user pattern)", () => { + // This is the documented workaround from all three issues + const cfg = ConfigManager.mergeConfig({ + historyStore: { + provider: "sqlite", + config: { historyDbPath: "/tmp/workaround.db" }, + }, + }); + expect(cfg.historyStore!.provider).toBe("sqlite"); + expect(cfg.historyStore!.config.historyDbPath).toBe("/tmp/workaround.db"); + }); + + it("disableHistory: true still works", () => { + const cfg = ConfigManager.mergeConfig({ disableHistory: true }); + expect(cfg.disableHistory).toBe(true); + }); + + it("supabase historyStore config is preserved", () => { + const cfg = ConfigManager.mergeConfig({ + historyStore: { + provider: "supabase", + config: { + supabaseUrl: "https://abc.supabase.co", + supabaseKey: "secret-key", + tableName: "custom_history", + }, + }, + }); + expect(cfg.historyStore!.provider).toBe("supabase"); + expect(cfg.historyStore!.config.supabaseUrl).toBe( + "https://abc.supabase.co", + ); + expect(cfg.historyStore!.config.supabaseKey).toBe("secret-key"); + expect(cfg.historyStore!.config.tableName).toBe("custom_history"); + }); + + it("custom embedder, llm, vectorStore configs pass through unchanged", () => { + const cfg = ConfigManager.mergeConfig({ + embedder: { + provider: "ollama", + config: { model: "nomic-embed-text", url: "http://localhost:11434" }, + }, + llm: { + provider: "ollama", + config: { model: "llama3.1:8b" }, + }, + vectorStore: { + provider: "qdrant", + config: { + collectionName: "test", + dimension: 768, + }, + }, + }); + expect(cfg.embedder.provider).toBe("ollama"); + expect(cfg.embedder.config.model).toBe("nomic-embed-text"); + expect(cfg.llm.provider).toBe("ollama"); + expect(cfg.llm.config.model).toBe("llama3.1:8b"); + expect(cfg.vectorStore.provider).toBe("qdrant"); + expect(cfg.vectorStore.config.collectionName).toBe("test"); + expect(cfg.vectorStore.config.dimension).toBe(768); + }); + + it("graphStore config passes through unchanged", () => { + const cfg = ConfigManager.mergeConfig({ + enableGraph: true, + graphStore: { + provider: "neo4j", + config: { + url: "neo4j://custom:7687", + username: "admin", + password: "pass", + }, + }, + }); + expect(cfg.enableGraph).toBe(true); + expect(cfg.graphStore!.config.url).toBe("neo4j://custom:7687"); + }); + + it("customPrompt passes through unchanged", () => { + const cfg = ConfigManager.mergeConfig({ + customPrompt: "You are a helpful assistant", + }); + expect(cfg.customPrompt).toBe("You are a helpful assistant"); + }); + + it("version override passes through unchanged", () => { + const cfg = ConfigManager.mergeConfig({ version: "v1.0" }); + expect(cfg.version).toBe("v1.0"); + }); +}); + +// --------------------------------------------------------------------------- +// 2. SQLiteManager – existing behavior preserved +// --------------------------------------------------------------------------- + +describe("backward compat: SQLiteManager", () => { + it("relative path still works (resolves from CWD)", async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-")); + const originalCwd = process.cwd(); + + try { + process.chdir(tempDir); + const manager = new SQLiteManager("memory.db"); + await manager.addHistory("m1", null, "value", "ADD"); + const history = await manager.getHistory("m1"); + + expect(history).toHaveLength(1); + expect(fs.existsSync(path.join(tempDir, "memory.db"))).toBe(true); + manager.close(); + } finally { + process.chdir(originalCwd); + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("absolute path still works", async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-")); + const dbPath = path.join(tempDir, "history.db"); + + try { + const manager = new SQLiteManager(dbPath); + await manager.addHistory("m1", null, "value", "ADD"); + expect(fs.existsSync(dbPath)).toBe(true); + manager.close(); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it(":memory: still works", async () => { + const manager = new SQLiteManager(":memory:"); + await manager.addHistory("m1", null, "value", "ADD"); + const history = await manager.getHistory("m1"); + expect(history).toHaveLength(1); + manager.close(); + }); + + it("reset clears history and allows re-use", async () => { + const manager = new SQLiteManager(":memory:"); + await manager.addHistory("m1", null, "val", "ADD"); + await manager.reset(); + const history = await manager.getHistory("m1"); + expect(history).toHaveLength(0); + await manager.addHistory("m2", null, "new-val", "ADD"); + const history2 = await manager.getHistory("m2"); + expect(history2).toHaveLength(1); + manager.close(); + }); +}); + +// --------------------------------------------------------------------------- +// 3. MemoryVectorStore – existing API preserved +// --------------------------------------------------------------------------- + +describe("backward compat: MemoryVectorStore", () => { + const originalCwd = process.cwd(); + + afterEach(() => { + process.chdir(originalCwd); + jest.restoreAllMocks(); + }); + + it("explicit dbPath still works (the existing config.dbPath feature)", async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-vs-")); + const dbPath = path.join(tempDir, "my_vectors.db"); + + try { + const store = new MemoryVectorStore({ dimension: 3, dbPath }); + await store.insert( + [normalize([1, 0, 0])], + ["id1"], + [{ text: "hello" }], + ); + + expect(fs.existsSync(dbPath)).toBe(true); + + const result = await store.get("id1"); + expect(result).not.toBeNull(); + expect(result!.payload.text).toBe("hello"); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("insert, search, get, update, delete, list all work", async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-vs-")); + const dbPath = path.join(tempDir, "test.db"); + + try { + const store = new MemoryVectorStore({ dimension: 3, dbPath }); + const v1 = normalize([1, 0, 0]); + const v2 = normalize([0, 1, 0]); + + // insert + await store.insert([v1, v2], ["a", "b"], [{ t: "a" }, { t: "b" }]); + + // get + const a = await store.get("a"); + expect(a!.payload.t).toBe("a"); + + // search + const results = await store.search(v1, 2); + expect(results).toHaveLength(2); + expect(results[0].id).toBe("a"); // closest to v1 + + // update + await store.update("a", v2, { t: "updated" }); + const updated = await store.get("a"); + expect(updated!.payload.t).toBe("updated"); + + // list + const [listed, count] = await store.list(); + expect(count).toBe(2); + expect(listed).toHaveLength(2); + + // delete + await store.delete("a"); + const deleted = await store.get("a"); + expect(deleted).toBeNull(); + + // deleteCol + await store.deleteCol(); + const [afterDrop] = await store.list(); + expect(afterDrop).toHaveLength(0); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("dimension mismatch on insert still throws", async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-vs-")); + const dbPath = path.join(tempDir, "test.db"); + + try { + const store = new MemoryVectorStore({ dimension: 3, dbPath }); + await expect( + store.insert([[1, 0]], ["id1"], [{ t: "x" }]), + ).rejects.toThrow("Vector dimension mismatch"); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("dimension mismatch on search still throws", async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-vs-")); + const dbPath = path.join(tempDir, "test.db"); + + try { + const store = new MemoryVectorStore({ dimension: 3, dbPath }); + await expect(store.search([1, 0], 1)).rejects.toThrow( + "Query dimension mismatch", + ); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("default dimension is 1536 when not specified", () => { + const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-home-")); + try { + jest.spyOn(os, "homedir").mockReturnValue(fakeHome); + const store = new MemoryVectorStore({}); + // Verify by trying to insert a 1536-dim vector + const vec = new Array(1536).fill(0); + vec[0] = 1; + expect(store.insert([vec], ["id1"], [{ t: "x" }])).resolves.not.toThrow(); + } finally { + fs.rmSync(fakeHome, { recursive: true, force: true }); + } + }); + + it("search with filters still works", async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-vs-")); + const dbPath = path.join(tempDir, "test.db"); + + try { + const store = new MemoryVectorStore({ dimension: 3, dbPath }); + await store.insert( + [normalize([1, 0, 0]), normalize([0, 1, 0])], + ["a", "b"], + [ + { text: "hello", userId: "user1" }, + { text: "world", userId: "user2" }, + ], + ); + + const results = await store.search(normalize([1, 0, 0]), 10, { + userId: "user2", + }); + expect(results).toHaveLength(1); + expect(results[0].id).toBe("b"); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); +}); + +// --------------------------------------------------------------------------- +// 4. VectorStoreConfig type – dbPath is optional, existing configs work +// --------------------------------------------------------------------------- + +describe("backward compat: VectorStoreConfig type", () => { + it("config without dbPath still works (no required field breakage)", () => { + const cfg = ConfigManager.mergeConfig({ + vectorStore: { + provider: "memory", + config: { collectionName: "test", dimension: 512 }, + }, + }); + expect(cfg.vectorStore.config.dbPath).toBeUndefined(); + expect(cfg.vectorStore.config.collectionName).toBe("test"); + expect(cfg.vectorStore.config.dimension).toBe(512); + }); + + it("config with client instance passes through unchanged", () => { + const fakeClient = { connect: () => {} }; + const cfg = ConfigManager.mergeConfig({ + vectorStore: { + provider: "qdrant", + config: { client: fakeClient, dimension: 768 }, + }, + }); + expect(cfg.vectorStore.config.client).toBe(fakeClient); + expect(cfg.vectorStore.config.dimension).toBe(768); + }); +}); + +// --------------------------------------------------------------------------- +// 5. ensureSQLiteDirectory – does not break existing paths +// --------------------------------------------------------------------------- + +describe("backward compat: ensureSQLiteDirectory", () => { + it("no-ops for already existing directory", () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-existing-")); + try { + // Should not throw even though directory already exists + expect(() => + ensureSQLiteDirectory(path.join(tempDir, "test.db")), + ).not.toThrow(); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("handles path with trailing slash gracefully", () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-trailing-")); + try { + // path.dirname of "dir/sub/" is "dir/sub", mkdirSync should handle it + expect(() => + ensureSQLiteDirectory(path.join(tempDir, "sub", "test.db")), + ).not.toThrow(); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); +}); diff --git a/mem0-ts/src/oss/src/tests/sqlite-path-resolution.test.ts b/mem0-ts/src/oss/src/tests/sqlite-path-resolution.test.ts new file mode 100644 index 000000000..ebd464769 --- /dev/null +++ b/mem0-ts/src/oss/src/tests/sqlite-path-resolution.test.ts @@ -0,0 +1,290 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import { ConfigManager } from "../config/manager"; +import { SQLiteManager } from "../storage/SQLiteManager"; +import { MemoryVectorStore } from "../vector_stores/memory"; +import { + ensureSQLiteDirectory, + getDefaultVectorStoreDbPath, +} from "../utils/sqlite"; + +function normalize(vector: number[]): number[] { + const norm = Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0)); + return vector.map((value) => value / norm); +} + +// --------------------------------------------------------------------------- +// Config merging – historyDbPath +// --------------------------------------------------------------------------- + +describe("ConfigManager.mergeConfig – historyDbPath handling", () => { + it("propagates top-level historyDbPath into historyStore.config", () => { + const cfg = ConfigManager.mergeConfig({ + historyDbPath: "/tmp/custom/history.db", + }); + expect(cfg.historyDbPath).toBe("/tmp/custom/history.db"); + expect(cfg.historyStore?.provider).toBe("sqlite"); + expect(cfg.historyStore?.config.historyDbPath).toBe( + "/tmp/custom/history.db", + ); + }); + + it("explicit historyStore.config.historyDbPath takes precedence over top-level", () => { + const cfg = ConfigManager.mergeConfig({ + historyDbPath: "/tmp/shorthand.db", + historyStore: { + provider: "sqlite", + config: { historyDbPath: "/tmp/explicit.db" }, + }, + }); + expect(cfg.historyStore?.config.historyDbPath).toBe("/tmp/explicit.db"); + }); + + it("preserves default memory.db when nothing is provided", () => { + const cfg = ConfigManager.mergeConfig({}); + expect(cfg.historyStore?.provider).toBe("sqlite"); + expect(cfg.historyStore?.config.historyDbPath).toBe("memory.db"); + }); + + it("respects only historyStore.config when top-level is absent", () => { + const cfg = ConfigManager.mergeConfig({ + historyStore: { + provider: "sqlite", + config: { historyDbPath: "/tmp/nested-only.db" }, + }, + }); + expect(cfg.historyStore?.config.historyDbPath).toBe("/tmp/nested-only.db"); + }); + + it("does not leak historyDbPath into non-sqlite providers", () => { + const cfg = ConfigManager.mergeConfig({ + historyDbPath: "/tmp/should-not-apply.db", + historyStore: { + provider: "supabase", + config: { + supabaseUrl: "https://x.supabase.co", + supabaseKey: "key", + }, + }, + }); + expect(cfg.historyStore?.provider).toBe("supabase"); + expect(cfg.historyStore?.config.historyDbPath).toBeUndefined(); + }); + + it("disableHistory does not prevent historyStore config from merging", () => { + const cfg = ConfigManager.mergeConfig({ + disableHistory: true, + historyDbPath: "/tmp/disabled.db", + }); + expect(cfg.disableHistory).toBe(true); + expect(cfg.historyStore?.config.historyDbPath).toBe("/tmp/disabled.db"); + }); +}); + +// --------------------------------------------------------------------------- +// SQLiteManager – directory creation & DB operations +// --------------------------------------------------------------------------- + +describe("SQLiteManager – directory auto-creation", () => { + it("creates nested parent directories and writes to the DB", async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-sqlite-")); + const dbPath = path.join(tempDir, "a", "b", "c", "history.db"); + let manager: SQLiteManager | undefined; + + try { + manager = new SQLiteManager(dbPath); + await manager.addHistory("mem-1", null, "test value", "ADD"); + const history = await manager.getHistory("mem-1"); + + expect(fs.existsSync(dbPath)).toBe(true); + expect(history).toHaveLength(1); + expect(history[0].new_value).toBe("test value"); + } finally { + manager?.close(); + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("end-to-end: mergeConfig + SQLiteManager at configured path", async () => { + const tempDir = fs.mkdtempSync( + path.join(os.tmpdir(), "mem0-history-path-"), + ); + const historyDbPath = path.join(tempDir, "nested", "history.db"); + let manager: SQLiteManager | undefined; + + try { + const mergedConfig = ConfigManager.mergeConfig({ historyDbPath }); + + manager = new SQLiteManager( + mergedConfig.historyStore!.config.historyDbPath!, + ); + await manager.addHistory("memory-1", null, "remember me", "ADD"); + + expect(fs.existsSync(historyDbPath)).toBe(true); + } finally { + manager?.close(); + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("works with :memory: without attempting directory creation", () => { + const manager = new SQLiteManager(":memory:"); + expect(manager).toBeDefined(); + manager.close(); + }); +}); + +// --------------------------------------------------------------------------- +// MemoryVectorStore – path handling +// --------------------------------------------------------------------------- + +describe("MemoryVectorStore – path handling", () => { + const originalCwd = process.cwd(); + + afterEach(() => { + process.chdir(originalCwd); + jest.restoreAllMocks(); + }); + + it("uses ~/.mem0/vector_store.db by default", () => { + const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-home-")); + try { + jest.spyOn(os, "homedir").mockReturnValue(fakeHome); + new MemoryVectorStore({ dimension: 4 }); + expect( + fs.existsSync(path.join(fakeHome, ".mem0", "vector_store.db")), + ).toBe(true); + } finally { + fs.rmSync(fakeHome, { recursive: true, force: true }); + } + }); + + it("respects explicit dbPath config", async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-vs-")); + const dbPath = path.join(tempDir, "custom", "vectors.db"); + + try { + const store = new MemoryVectorStore({ dimension: 4, dbPath }); + await store.insert( + [normalize([1, 0, 0, 0])], + ["v1"], + [{ text: "hello" }], + ); + + expect(fs.existsSync(dbPath)).toBe(true); + const results = await store.search(normalize([1, 0, 0, 0]), 1); + expect(results).toHaveLength(1); + expect(results[0].payload.text).toBe("hello"); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("works when CWD is read-only", async () => { + const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-home-")); + const readOnlyCwd = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-ro-")); + + try { + fs.chmodSync(readOnlyCwd, 0o555); + jest.spyOn(os, "homedir").mockReturnValue(fakeHome); + process.chdir(readOnlyCwd); + + const store = new MemoryVectorStore({ dimension: 4 }); + await store.insert( + [normalize([0, 1, 0, 0])], + ["v2"], + [{ text: "works" }], + ); + + expect( + fs.existsSync(path.join(fakeHome, ".mem0", "vector_store.db")), + ).toBe(true); + expect( + fs.existsSync(path.join(readOnlyCwd, "vector_store.db")), + ).toBe(false); + } finally { + fs.chmodSync(readOnlyCwd, 0o755); + fs.rmSync(fakeHome, { recursive: true, force: true }); + fs.rmSync(readOnlyCwd, { recursive: true, force: true }); + } + }); + + it("emits migration warning when old CWD-based vector_store.db exists", () => { + const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-home-")); + const tempCwd = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-cwd-")); + + try { + fs.writeFileSync(path.join(tempCwd, "vector_store.db"), ""); + jest.spyOn(os, "homedir").mockReturnValue(fakeHome); + const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); + process.chdir(tempCwd); + + new MemoryVectorStore({ dimension: 4 }); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("Default vector_store.db location changed"), + ); + } finally { + fs.rmSync(fakeHome, { recursive: true, force: true }); + fs.rmSync(tempCwd, { recursive: true, force: true }); + } + }); + + it("does NOT emit migration warning when dbPath is explicitly set", () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-vs-")); + const tempCwd = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-cwd-")); + + try { + fs.writeFileSync(path.join(tempCwd, "vector_store.db"), ""); + const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); + process.chdir(tempCwd); + + new MemoryVectorStore({ + dimension: 4, + dbPath: path.join(tempDir, "explicit.db"), + }); + + expect(warnSpy).not.toHaveBeenCalled(); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + fs.rmSync(tempCwd, { recursive: true, force: true }); + } + }); +}); + +// --------------------------------------------------------------------------- +// Utils +// --------------------------------------------------------------------------- + +describe("ensureSQLiteDirectory", () => { + it("creates nested directories", () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-ensure-")); + const target = path.join(tempDir, "x", "y", "z", "test.db"); + try { + ensureSQLiteDirectory(target); + expect(fs.existsSync(path.join(tempDir, "x", "y", "z"))).toBe(true); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("skips :memory:", () => { + expect(() => ensureSQLiteDirectory(":memory:")).not.toThrow(); + }); + + it("skips file: URIs", () => { + expect(() => ensureSQLiteDirectory("file::memory:")).not.toThrow(); + }); + + it("skips empty string", () => { + expect(() => ensureSQLiteDirectory("")).not.toThrow(); + }); +}); + +describe("getDefaultVectorStoreDbPath", () => { + it("returns path under homedir/.mem0", () => { + const result = getDefaultVectorStoreDbPath(); + expect(result).toBe(path.join(os.homedir(), ".mem0", "vector_store.db")); + }); +}); diff --git a/mem0-ts/src/oss/src/types/index.ts b/mem0-ts/src/oss/src/types/index.ts index 6119806d2..a5beaa1a2 100644 --- a/mem0-ts/src/oss/src/types/index.ts +++ b/mem0-ts/src/oss/src/types/index.ts @@ -23,6 +23,7 @@ export interface EmbeddingConfig { export interface VectorStoreConfig { collectionName?: string; dimension?: number; + dbPath?: string; client?: any; instance?: any; [key: string]: any; @@ -129,6 +130,7 @@ export const MemoryConfigSchema = z.object({ .object({ collectionName: z.string().optional(), dimension: z.number().optional(), + dbPath: z.string().optional(), client: z.any().optional(), }) .passthrough(), diff --git a/mem0-ts/src/oss/src/utils/sqlite.ts b/mem0-ts/src/oss/src/utils/sqlite.ts new file mode 100644 index 000000000..7da81c6c8 --- /dev/null +++ b/mem0-ts/src/oss/src/utils/sqlite.ts @@ -0,0 +1,15 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; + +export function getDefaultVectorStoreDbPath(): string { + return path.join(os.homedir(), ".mem0", "vector_store.db"); +} + +export function ensureSQLiteDirectory(dbPath: string): void { + if (!dbPath || dbPath === ":memory:" || dbPath.startsWith("file:")) { + return; + } + + fs.mkdirSync(path.dirname(dbPath), { recursive: true }); +} diff --git a/mem0-ts/src/oss/src/vector_stores/memory.ts b/mem0-ts/src/oss/src/vector_stores/memory.ts index 4aed96838..62a825507 100644 --- a/mem0-ts/src/oss/src/vector_stores/memory.ts +++ b/mem0-ts/src/oss/src/vector_stores/memory.ts @@ -1,7 +1,12 @@ import { VectorStore } from "./base"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import Database from "better-sqlite3"; +import fs from "fs"; import path from "path"; +import { + ensureSQLiteDirectory, + getDefaultVectorStoreDbPath, +} from "../utils/sqlite"; interface MemoryVector { id: string; @@ -16,10 +21,19 @@ export class MemoryVectorStore implements VectorStore { constructor(config: VectorStoreConfig) { this.dimension = config.dimension || 1536; // Default OpenAI dimension - this.dbPath = path.join(process.cwd(), "vector_store.db"); - if (config.dbPath) { - this.dbPath = config.dbPath; + this.dbPath = config.dbPath || getDefaultVectorStoreDbPath(); + + if (!config.dbPath) { + const oldDefault = path.join(process.cwd(), "vector_store.db"); + if (fs.existsSync(oldDefault) && oldDefault !== this.dbPath) { + console.warn( + `[mem0] Default vector_store.db location changed from ${oldDefault} to ${this.dbPath}. ` + + `Move your existing file or set vectorStore.config.dbPath explicitly.`, + ); + } } + + ensureSQLiteDirectory(this.dbPath); this.db = new Database(this.dbPath); this.init(); }