Memory: make qmd search-mode flags compatible

This commit is contained in:
Vignesh Natarajan
2026-02-07 19:59:40 -08:00
committed by Vignesh
parent 6d9d4d04ed
commit 36e27ad561
3 changed files with 79 additions and 11 deletions

View File

@@ -142,9 +142,8 @@ out to QMD for retrieval. Key points:
- Searches run via `memory.qmd.searchMode` (default `qmd query --json`; also
supports `search` and `vsearch`). If the selected mode rejects flags on your
QMD build, OpenClaw retries with `qmd query`. If QMD fails or the binary is
missing,
OpenClaw automatically falls back to the builtin SQLite manager so memory tools
keep working.
missing, OpenClaw automatically falls back to the builtin SQLite manager so
memory tools keep working.
- OpenClaw does not expose QMD embed batch-size tuning today; batch behavior is
controlled by QMD itself.
- **First search may be slow**: QMD may download local GGUF models (reranker/query

View File

@@ -316,13 +316,79 @@ describe("QmdMemoryManager", () => {
if (!manager) {
throw new Error("manager missing");
}
const maxResults = resolved.qmd?.limits.maxResults;
if (!maxResults) {
throw new Error("qmd maxResults missing");
}
await expect(
manager.search("test", { sessionKey: "agent:main:slack:dm:u123" }),
).resolves.toEqual([]);
expect(spawnMock.mock.calls.some((call) => call[1]?.[0] === "search")).toBe(true);
const searchCall = spawnMock.mock.calls.find((call) => call[1]?.[0] === "search");
expect(searchCall?.[1]).toEqual(["search", "test", "--json"]);
expect(spawnMock.mock.calls.some((call) => call[1]?.[0] === "query")).toBe(false);
expect(maxResults).toBeGreaterThan(0);
await manager.close();
});
it("retries search with qmd query when configured mode rejects flags", async () => {
cfg = {
...cfg,
memory: {
backend: "qmd",
qmd: {
includeDefaultMemory: false,
searchMode: "search",
update: { interval: "0s", debounceMs: 60_000, onBoot: false },
paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }],
},
},
} as OpenClawConfig;
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
if (args[0] === "search") {
const child = createMockChild({ autoClose: false });
setTimeout(() => {
child.stderr.emit("data", "unknown flag: --json");
child.closeWith(2);
}, 0);
return child;
}
if (args[0] === "query") {
const child = createMockChild({ autoClose: false });
setTimeout(() => {
child.stdout.emit("data", "[]");
child.closeWith(0);
}, 0);
return child;
}
return createMockChild();
});
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
const manager = await QmdMemoryManager.create({ cfg, agentId, resolved });
expect(manager).toBeTruthy();
if (!manager) {
throw new Error("manager missing");
}
const maxResults = resolved.qmd?.limits.maxResults;
if (!maxResults) {
throw new Error("qmd maxResults missing");
}
await expect(
manager.search("test", { sessionKey: "agent:main:slack:dm:u123" }),
).resolves.toEqual([]);
const searchAndQueryCalls = spawnMock.mock.calls
.map((call) => call[1])
.filter(
(args): args is string[] => Array.isArray(args) && ["search", "query"].includes(args[0]),
);
expect(searchAndQueryCalls).toEqual([
["search", "test", "--json"],
["query", "test", "--json", "-n", String(maxResults)],
]);
await manager.close();
});

View File

@@ -261,7 +261,10 @@ export class QmdMemoryManager implements MemorySearchManager {
return [];
}
const qmdSearchCommand = this.qmd.searchMode;
const args = this.buildSearchArgs(qmdSearchCommand, trimmed, limit, collectionFilterArgs);
const args = this.buildSearchArgs(qmdSearchCommand, trimmed, limit);
if (qmdSearchCommand === "query") {
args.push(...collectionFilterArgs);
}
let stdout: string;
let stderr: string;
try {
@@ -274,10 +277,11 @@ export class QmdMemoryManager implements MemorySearchManager {
`qmd ${qmdSearchCommand} does not support configured flags; retrying search with qmd query`,
);
try {
const fallback = await this.runQmd(
this.buildSearchArgs("query", trimmed, limit, collectionFilterArgs),
{ timeoutMs: this.qmd.limits.timeoutMs },
);
const fallbackArgs = this.buildSearchArgs("query", trimmed, limit);
fallbackArgs.push(...collectionFilterArgs);
const fallback = await this.runQmd(fallbackArgs, {
timeoutMs: this.qmd.limits.timeoutMs,
});
stdout = fallback.stdout;
stderr = fallback.stderr;
} catch (fallbackErr) {
@@ -1011,10 +1015,9 @@ export class QmdMemoryManager implements MemorySearchManager {
command: "query" | "search" | "vsearch",
query: string,
limit: number,
collectionFilterArgs: string[],
): string[] {
if (command === "query") {
return ["query", query, "--json", "-n", String(limit), ...collectionFilterArgs];
return ["query", query, "--json", "-n", String(limit)];
}
return [command, query, "--json"];
}