mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 09:41:24 +00:00
Memory/QMD: harden multi-collection search and embed scheduling
This commit is contained in:
@@ -156,17 +156,17 @@ describe("QmdMemoryManager", () => {
|
||||
const baselineCalls = spawnMock.mock.calls.length;
|
||||
|
||||
await manager.sync({ reason: "manual" });
|
||||
expect(spawnMock.mock.calls.length).toBe(baselineCalls + 2);
|
||||
expect(spawnMock.mock.calls.length).toBe(baselineCalls + 1);
|
||||
|
||||
await manager.sync({ reason: "manual-again" });
|
||||
expect(spawnMock.mock.calls.length).toBe(baselineCalls + 2);
|
||||
expect(spawnMock.mock.calls.length).toBe(baselineCalls + 1);
|
||||
|
||||
(manager as unknown as { lastUpdateAt: number | null }).lastUpdateAt =
|
||||
Date.now() - (resolved.qmd?.update.debounceMs ?? 0) - 10;
|
||||
|
||||
await manager.sync({ reason: "after-wait" });
|
||||
// By default we refresh embeddings less frequently than index updates.
|
||||
expect(spawnMock.mock.calls.length).toBe(baselineCalls + 3);
|
||||
// `search` mode does not require qmd embed side effects.
|
||||
expect(spawnMock.mock.calls.length).toBe(baselineCalls + 2);
|
||||
|
||||
await manager.close();
|
||||
});
|
||||
@@ -359,7 +359,7 @@ describe("QmdMemoryManager", () => {
|
||||
expect(addSessions?.[2]).toBe(path.join(stateDir, "agents", devAgentId, "qmd", "sessions"));
|
||||
});
|
||||
|
||||
it("rebinds sessions collection when qmd only reports collection names", async () => {
|
||||
it("rebinds managed collections when qmd only reports collection names", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
@@ -396,6 +396,11 @@ describe("QmdMemoryManager", () => {
|
||||
args[0] === "collection" && args[1] === "remove" && args[2] === sessionCollectionName,
|
||||
);
|
||||
expect(removeSessions).toBeDefined();
|
||||
const removeWorkspace = commands.find(
|
||||
(args) =>
|
||||
args[0] === "collection" && args[1] === "remove" && args[2] === `workspace-${agentId}`,
|
||||
);
|
||||
expect(removeWorkspace).toBeDefined();
|
||||
|
||||
const addSessions = commands.find((args) => {
|
||||
if (args[0] !== "collection" || args[1] !== "add") {
|
||||
@@ -415,6 +420,7 @@ describe("QmdMemoryManager", () => {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: false,
|
||||
searchMode: "query",
|
||||
update: {
|
||||
interval: "0s",
|
||||
debounceMs: 0,
|
||||
@@ -792,23 +798,16 @@ describe("QmdMemoryManager", () => {
|
||||
const { manager, resolved } = await createManager();
|
||||
|
||||
await manager.search("test", { sessionKey: "agent:main:slack:dm:u123" });
|
||||
const searchCall = spawnMock.mock.calls.find(
|
||||
(call: unknown[]) => (call[1] as string[])?.[0] === "search",
|
||||
);
|
||||
const maxResults = resolved.qmd?.limits.maxResults;
|
||||
if (!maxResults) {
|
||||
throw new Error("qmd maxResults missing");
|
||||
}
|
||||
expect(searchCall?.[1]).toEqual([
|
||||
"search",
|
||||
"test",
|
||||
"--json",
|
||||
"-n",
|
||||
String(maxResults),
|
||||
"-c",
|
||||
"workspace-main",
|
||||
"-c",
|
||||
"notes-main",
|
||||
const searchCalls = spawnMock.mock.calls
|
||||
.map((call: unknown[]) => call[1] as string[])
|
||||
.filter((args: string[]) => args[0] === "search");
|
||||
expect(searchCalls).toEqual([
|
||||
["search", "test", "--json", "-n", String(maxResults), "-c", "workspace-main"],
|
||||
["search", "test", "--json", "-n", String(maxResults), "-c", "notes-main"],
|
||||
]);
|
||||
await manager.close();
|
||||
});
|
||||
@@ -904,17 +903,7 @@ describe("QmdMemoryManager", () => {
|
||||
.map((call: unknown[]) => call[1] as string[])
|
||||
.filter((args: string[]) => args[0] === "search" || args[0] === "query");
|
||||
expect(searchAndQueryCalls).toEqual([
|
||||
[
|
||||
"search",
|
||||
"test",
|
||||
"--json",
|
||||
"-n",
|
||||
String(maxResults),
|
||||
"-c",
|
||||
"workspace-main",
|
||||
"-c",
|
||||
"notes-main",
|
||||
],
|
||||
["search", "test", "--json", "-n", String(maxResults), "-c", "workspace-main"],
|
||||
["query", "test", "--json", "-n", String(maxResults), "-c", "workspace-main"],
|
||||
["query", "test", "--json", "-n", String(maxResults), "-c", "notes-main"],
|
||||
]);
|
||||
@@ -984,6 +973,70 @@ describe("QmdMemoryManager", () => {
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("skips qmd embed in search mode even for forced sync", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: false,
|
||||
searchMode: "search",
|
||||
update: { interval: "0s", debounceMs: 0, onBoot: false },
|
||||
paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const { manager } = await createManager({ mode: "status" });
|
||||
await manager.sync({ reason: "manual", force: true });
|
||||
|
||||
const commandCalls = spawnMock.mock.calls
|
||||
.map((call: unknown[]) => call[1] as string[])
|
||||
.filter((args: string[]) => args[0] === "update" || args[0] === "embed");
|
||||
expect(commandCalls).toEqual([["update"]]);
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("retries boot update when qmd reports a retryable lock error", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: false,
|
||||
searchMode: "search",
|
||||
update: {
|
||||
interval: "0s",
|
||||
debounceMs: 60_000,
|
||||
onBoot: true,
|
||||
waitForBootSync: true,
|
||||
},
|
||||
paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
let updateCalls = 0;
|
||||
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||
if (args[0] === "update") {
|
||||
updateCalls += 1;
|
||||
const child = createMockChild({ autoClose: false });
|
||||
if (updateCalls === 1) {
|
||||
emitAndClose(child, "stderr", "SQLITE_BUSY: database is locked", 2);
|
||||
} else {
|
||||
emitAndClose(child, "stdout", "", 0);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
return createMockChild();
|
||||
});
|
||||
|
||||
const { manager } = await createManager({ mode: "full" });
|
||||
|
||||
expect(updateCalls).toBe(2);
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("scopes by channel for agent-prefixed session keys", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
@@ -1170,7 +1223,7 @@ describe("QmdMemoryManager", () => {
|
||||
};
|
||||
inner.db = {
|
||||
prepare: () => ({
|
||||
get: () => {
|
||||
all: () => {
|
||||
throw new Error("SQLITE_BUSY: database is locked");
|
||||
},
|
||||
}),
|
||||
@@ -1198,11 +1251,11 @@ describe("QmdMemoryManager", () => {
|
||||
|
||||
const { manager } = await createManager();
|
||||
const inner = manager as unknown as {
|
||||
db: { prepare: () => { get: () => never }; close: () => void } | null;
|
||||
db: { prepare: () => { all: () => never }; close: () => void } | null;
|
||||
};
|
||||
inner.db = {
|
||||
prepare: () => ({
|
||||
get: () => {
|
||||
all: () => {
|
||||
throw new Error("SQLITE_BUSY: database is locked");
|
||||
},
|
||||
}),
|
||||
@@ -1235,19 +1288,19 @@ describe("QmdMemoryManager", () => {
|
||||
const { manager } = await createManager();
|
||||
|
||||
const inner = manager as unknown as {
|
||||
db: { prepare: (query: string) => { get: (arg: unknown) => unknown }; close: () => void };
|
||||
db: { prepare: (query: string) => { all: (arg: unknown) => unknown }; close: () => void };
|
||||
};
|
||||
inner.db = {
|
||||
prepare: (query: string) => {
|
||||
prepareCalls.push(query);
|
||||
return {
|
||||
get: (arg: unknown) => {
|
||||
all: (arg: unknown) => {
|
||||
if (query.includes("hash = ?")) {
|
||||
return undefined;
|
||||
return [];
|
||||
}
|
||||
if (query.includes("hash LIKE ?")) {
|
||||
expect(arg).toBe(`${exactDocid}%`);
|
||||
return { collection: "workspace-main", path: "notes/welcome.md" };
|
||||
return [{ collection: "workspace-main", path: "notes/welcome.md" }];
|
||||
}
|
||||
throw new Error(`unexpected sqlite query: ${query}`);
|
||||
},
|
||||
@@ -1274,6 +1327,76 @@ describe("QmdMemoryManager", () => {
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("prefers collection hint when resolving duplicate qmd document hashes", async () => {
|
||||
cfg = {
|
||||
...cfg,
|
||||
memory: {
|
||||
backend: "qmd",
|
||||
qmd: {
|
||||
includeDefaultMemory: false,
|
||||
update: { interval: "0s", debounceMs: 60_000, onBoot: false },
|
||||
paths: [
|
||||
{ path: workspaceDir, pattern: "**/*.md", name: "workspace" },
|
||||
{ path: path.join(workspaceDir, "notes"), pattern: "**/*.md", name: "notes" },
|
||||
],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const duplicateDocid = "dup-123";
|
||||
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||
if (args[0] === "search" && args.includes("workspace-main")) {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
emitAndClose(
|
||||
child,
|
||||
"stdout",
|
||||
JSON.stringify([
|
||||
{ docid: duplicateDocid, score: 0.9, snippet: "@@ -3,1\nworkspace hit" },
|
||||
]),
|
||||
);
|
||||
return child;
|
||||
}
|
||||
if (args[0] === "search" && args.includes("notes-main")) {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
emitAndClose(child, "stdout", "[]");
|
||||
return child;
|
||||
}
|
||||
return createMockChild();
|
||||
});
|
||||
|
||||
const { manager } = await createManager();
|
||||
const inner = manager as unknown as {
|
||||
db: { prepare: (query: string) => { all: (arg: unknown) => unknown }; close: () => void };
|
||||
};
|
||||
inner.db = {
|
||||
prepare: (_query: string) => ({
|
||||
all: (arg: unknown) => {
|
||||
if (typeof arg === "string" && arg.startsWith(duplicateDocid)) {
|
||||
return [
|
||||
{ collection: "stale-workspace", path: "notes/welcome.md" },
|
||||
{ collection: "workspace-main", path: "notes/welcome.md" },
|
||||
];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
}),
|
||||
close: () => {},
|
||||
};
|
||||
|
||||
const results = await manager.search("workspace", { sessionKey: "agent:main:slack:dm:u123" });
|
||||
expect(results).toEqual([
|
||||
{
|
||||
path: "notes/welcome.md",
|
||||
startLine: 3,
|
||||
endLine: 3,
|
||||
score: 0.9,
|
||||
snippet: "@@ -3,1\nworkspace hit",
|
||||
source: "memory",
|
||||
},
|
||||
]);
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("errors when qmd output exceeds command output safety cap", async () => {
|
||||
const noisyPayload = "x".repeat(240_000);
|
||||
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||
|
||||
Reference in New Issue
Block a user