Files
openclaw/extensions/memory-lancedb/lancedb-store.test.ts
Shubhankar Tripathy 6390edec25 fix(memory-lancedb): prevent cross-agent memory leakage (#103799)
* fix(memory-lancedb): gate auto-recall and auto-capture on per-agent memorySearch.enabled (#103590)

The before_prompt_build auto-recall hook only checked the plugin-level
autoRecall flag, so agents configured with memorySearch.enabled: false
still received <relevant-memories> injected from the shared LanceDB store
- leaking one agent's private memories into another agent's prompts. Gate
both recall injection and agent_end auto-capture on the current agent's
memorySearch.enabled (per-agent entry wins over agents.defaults; unset
means enabled), mirroring core resolveMemorySearchConfig semantics.

* fix(memory-lancedb): normalize agent ids before the memorySearch gate

Review follow-up on #103799: a configured id like 'XiaoHuo' or one with
surrounding whitespace missed the exact-match per-agent override and
inherited the enabled default, leaving the disclosure path active.
Normalize both the hook agent id and configured entry ids with the SDK
normalizeAgentId before comparing.

* fix(memory-lancedb): resolve the per-agent memorySearch gate via resolveAgentConfig

* fix(memory): isolate LanceDB rows by agent

Co-authored-by: Shubhankar Tripathy <reach2shubhankar@gmail.com>

* fix(memory): isolate LanceDB rows by agent

Co-authored-by: Shubhankar Tripathy <reach2shubhankar@gmail.com>

* refactor(memory): keep LanceDB store types private

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-16 03:33:08 -07:00

75 lines
2.6 KiB
TypeScript

import * as lancedb from "@lancedb/lancedb";
import { describe, expect, test } from "vitest";
import { MemoryDB } from "./lancedb-store.js";
import { installTmpDirHarness } from "./test-helpers.js";
describe("MemoryDB agent isolation", () => {
const { getDbPath } = installTmpDirHarness({ prefix: "openclaw-memory-scope-" });
test("scopes store, search, list, query, count, delete, and restart reads", async () => {
const db = new MemoryDB(getDbPath(), 2);
const alpha = await db.store("alpha", {
text: "alpha private preference",
vector: [1, 0],
importance: 0.8,
category: "preference",
});
await db.store("beta", {
text: "beta private preference",
vector: [1, 0],
importance: 0.9,
category: "preference",
});
await expect(db.search("alpha", [1, 0], 5, 0)).resolves.toMatchObject([
{ entry: { id: alpha.id, text: "alpha private preference" } },
]);
await expect(db.list("beta")).resolves.toMatchObject([{ text: "beta private preference" }]);
await expect(db.count("alpha")).resolves.toBe(1);
await expect(
db.query("alpha", {
columns: ["id", "text"],
filter: { column: "category", operator: "=", value: "preference" },
}),
).resolves.toMatchObject([{ id: alpha.id, text: "alpha private preference" }]);
await expect(db.delete("beta", alpha.id)).resolves.toBe(false);
await expect(db.count("alpha")).resolves.toBe(1);
db.close();
const reopened = new MemoryDB(getDbPath(), 2);
await expect(reopened.list("alpha")).resolves.toMatchObject([
{ id: alpha.id, text: "alpha private preference" },
]);
await expect(reopened.list("beta")).resolves.toMatchObject([
{ text: "beta private preference" },
]);
reopened.close();
});
test("refuses an unscoped legacy table until doctor migrates it", async () => {
const connection = await lancedb.connect(getDbPath());
const table = await connection.createTable("memories", [
{
id: "11111111-1111-4111-8111-111111111111",
text: "legacy shared memory",
vector: [1, 0],
importance: 0.7,
category: "fact",
createdAt: 1,
},
]);
table.close();
connection.close();
const db = new MemoryDB(getDbPath(), 2);
await expect(db.count("main")).rejects.toThrow(
'Run "openclaw doctor --fix" to assign legacy rows to the default agent',
);
await expect(db.count("main")).rejects.toThrow(
'Run "openclaw doctor --fix" to assign legacy rows to the default agent',
);
db.close();
});
});