feat: add memory vector search

This commit is contained in:
Peter Steinberger
2026-01-12 11:22:56 +00:00
parent 8049f33435
commit bf11a42c37
22 changed files with 2923 additions and 94 deletions

98
src/memory/index.test.ts Normal file
View File

@@ -0,0 +1,98 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getMemorySearchManager } from "./index.js";
vi.mock("./embeddings.js", () => {
const embedText = (text: string) => {
const lower = text.toLowerCase();
const alpha = lower.split("alpha").length - 1;
const beta = lower.split("beta").length - 1;
return [alpha, beta, 1];
};
return {
createEmbeddingProvider: async () => ({
requestedProvider: "openai",
provider: {
id: "mock",
model: "mock-embed",
embedQuery: async (text: string) => embedText(text),
embedBatch: async (texts: string[]) => texts.map(embedText),
},
}),
};
});
describe("memory index", () => {
let workspaceDir: string;
let indexPath: string;
beforeEach(async () => {
workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-mem-"));
indexPath = path.join(workspaceDir, "index.sqlite");
await fs.mkdir(path.join(workspaceDir, "memory"));
await fs.writeFile(
path.join(workspaceDir, "memory", "2026-01-12.md"),
"# Log\nAlpha memory line.\nAnother line.",
);
await fs.writeFile(
path.join(workspaceDir, "MEMORY.md"),
"Beta knowledge base entry.",
);
});
afterEach(async () => {
await fs.rm(workspaceDir, { recursive: true, force: true });
});
it("indexes memory files and searches by vector", async () => {
const cfg = {
agents: {
defaults: {
workspace: workspaceDir,
memorySearch: {
provider: "openai",
model: "mock-embed",
store: { path: indexPath },
sync: { watch: false, onSessionStart: false, onSearch: true },
query: { minScore: 0 },
},
},
list: [{ id: "main", default: true }],
},
};
const result = await getMemorySearchManager({ cfg, agentId: "main" });
expect(result.manager).not.toBeNull();
if (!result.manager) throw new Error("manager missing");
await result.manager.sync({ force: true });
const results = await result.manager.search("alpha");
expect(results.length).toBeGreaterThan(0);
expect(results[0]?.path).toContain("memory/2026-01-12.md");
});
it("rejects reading non-memory paths", async () => {
const cfg = {
agents: {
defaults: {
workspace: workspaceDir,
memorySearch: {
provider: "openai",
model: "mock-embed",
store: { path: indexPath },
sync: { watch: false, onSessionStart: false, onSearch: true },
},
},
list: [{ id: "main", default: true }],
},
};
const result = await getMemorySearchManager({ cfg, agentId: "main" });
expect(result.manager).not.toBeNull();
if (!result.manager) throw new Error("manager missing");
await expect(
result.manager.readFile({ relPath: "NOTES.md" }),
).rejects.toThrow("path required");
});
});