From e38ed4f6404402035a57261bd657ae1af3e8dfba Mon Sep 17 00:00:00 2001 From: vignesh07 Date: Fri, 13 Feb 2026 22:44:56 -0800 Subject: [PATCH] fix(memory): default qmd searchMode to search + scope search/vsearch to collections --- docs/concepts/memory.md | 8 ++++---- src/memory/backend-config.test.ts | 2 +- src/memory/backend-config.ts | 4 +++- src/memory/qmd-manager.test.ts | 32 +++++++++---------------------- src/memory/qmd-manager.ts | 7 ++++--- 5 files changed, 21 insertions(+), 32 deletions(-) diff --git a/docs/concepts/memory.md b/docs/concepts/memory.md index 8907ef28c78..616a24df3b5 100644 --- a/docs/concepts/memory.md +++ b/docs/concepts/memory.md @@ -139,8 +139,8 @@ out to QMD for retrieval. Key points: - Boot refresh now runs in the background by default so chat startup is not blocked; set `memory.qmd.update.waitForBootSync = true` to keep the previous blocking behavior. -- Searches run via `memory.qmd.searchMode` (default `qmd query --json`; also - supports `search` and `vsearch`). If the selected mode rejects flags on your +- Searches run via `memory.qmd.searchMode` (default `qmd search --json`; also + supports `vsearch` and `query`). 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. @@ -178,8 +178,8 @@ out to QMD for retrieval. Key points: **Config surface (`memory.qmd.*`)** - `command` (default `qmd`): override the executable path. -- `searchMode` (default `query`): pick which QMD command backs - `memory_search` (`query`, `search`, `vsearch`). +- `searchMode` (default `search`): pick which QMD command backs + `memory_search` (`search`, `vsearch`, `query`). - `includeDefaultMemory` (default `true`): auto-index `MEMORY.md` + `memory/**/*.md`. - `paths[]`: add extra directories/files (`path`, optional `pattern`, optional stable `name`). diff --git a/src/memory/backend-config.test.ts b/src/memory/backend-config.test.ts index c31c165d30a..78fd73967e5 100644 --- a/src/memory/backend-config.test.ts +++ b/src/memory/backend-config.test.ts @@ -25,7 +25,7 @@ describe("resolveMemoryBackendConfig", () => { expect(resolved.backend).toBe("qmd"); expect(resolved.qmd?.collections.length).toBeGreaterThanOrEqual(3); expect(resolved.qmd?.command).toBe("qmd"); - expect(resolved.qmd?.searchMode).toBe("query"); + expect(resolved.qmd?.searchMode).toBe("search"); expect(resolved.qmd?.update.intervalMs).toBeGreaterThan(0); expect(resolved.qmd?.update.waitForBootSync).toBe(false); expect(resolved.qmd?.update.commandTimeoutMs).toBe(30_000); diff --git a/src/memory/backend-config.ts b/src/memory/backend-config.ts index e08b157a069..53f0a55845b 100644 --- a/src/memory/backend-config.ts +++ b/src/memory/backend-config.ts @@ -66,7 +66,9 @@ const DEFAULT_CITATIONS: MemoryCitationsMode = "auto"; const DEFAULT_QMD_INTERVAL = "5m"; const DEFAULT_QMD_DEBOUNCE_MS = 15_000; const DEFAULT_QMD_TIMEOUT_MS = 4_000; -const DEFAULT_QMD_SEARCH_MODE: MemoryQmdSearchMode = "query"; +// Defaulting to `query` can be extremely slow on CPU-only systems (query expansion + rerank). +// Prefer a faster mode for interactive use; users can opt into `query` for best recall. +const DEFAULT_QMD_SEARCH_MODE: MemoryQmdSearchMode = "search"; const DEFAULT_QMD_EMBED_INTERVAL = "60m"; const DEFAULT_QMD_COMMAND_TIMEOUT_MS = 30_000; const DEFAULT_QMD_UPDATE_TIMEOUT_MS = 120_000; diff --git a/src/memory/qmd-manager.test.ts b/src/memory/qmd-manager.test.ts index 7f166a484c8..11b622f7dae 100644 --- a/src/memory/qmd-manager.test.ts +++ b/src/memory/qmd-manager.test.ts @@ -334,7 +334,7 @@ describe("QmdMemoryManager", () => { ).resolves.toEqual([]); const searchCall = spawnMock.mock.calls.find((call) => call[1]?.[0] === "search"); - expect(searchCall?.[1]).toEqual(["search", "test", "--json"]); + expect(searchCall?.[1]).toEqual(["search", "test", "--json", "-c", "workspace"]); expect(spawnMock.mock.calls.some((call) => call[1]?.[0] === "query")).toBe(false); expect(maxResults).toBeGreaterThan(0); await manager.close(); @@ -388,7 +388,7 @@ describe("QmdMemoryManager", () => { (args): args is string[] => Array.isArray(args) && ["search", "query"].includes(args[0]), ); expect(searchAndQueryCalls).toEqual([ - ["search", "test", "--json"], + ["search", "test", "--json", "-c", "workspace"], ["query", "test", "--json", "-n", String(maxResults), "-c", "workspace"], ]); await manager.close(); @@ -535,7 +535,7 @@ describe("QmdMemoryManager", () => { } as OpenClawConfig; spawnMock.mockImplementation((_cmd: string, args: string[]) => { - if (args[0] === "query") { + if (args[0] === "search") { const child = createMockChild({ autoClose: false }); emitAndClose(child, "stdout", "[]"); return child; @@ -549,24 +549,10 @@ describe("QmdMemoryManager", () => { if (!manager) { throw new Error("manager missing"); } - const maxResults = resolved.qmd?.limits.maxResults; - if (!maxResults) { - throw new Error("qmd maxResults missing"); - } await manager.search("test", { sessionKey: "agent:main:slack:dm:u123" }); - const queryCall = spawnMock.mock.calls.find((call) => call[1]?.[0] === "query"); - expect(queryCall?.[1]).toEqual([ - "query", - "test", - "--json", - "-n", - String(maxResults), - "-c", - "workspace", - "-c", - "notes", - ]); + const searchCall = spawnMock.mock.calls.find((call) => call[1]?.[0] === "search"); + expect(searchCall?.[1]).toEqual(["search", "test", "--json", "-c", "workspace", "-c", "notes"]); await manager.close(); }); @@ -802,7 +788,7 @@ describe("QmdMemoryManager", () => { it("fails search when sqlite index is busy so caller can fallback", async () => { spawnMock.mockImplementation((_cmd: string, args: string[]) => { - if (args[0] === "query") { + if (args[0] === "search") { const child = createMockChild({ autoClose: false }); emitAndClose( child, @@ -839,7 +825,7 @@ describe("QmdMemoryManager", () => { it("treats plain-text no-results stdout as an empty result set", async () => { spawnMock.mockImplementation((_cmd: string, args: string[]) => { - if (args[0] === "query") { + if (args[0] === "search") { const child = createMockChild({ autoClose: false }); emitAndClose(child, "stdout", "No results found."); return child; @@ -862,7 +848,7 @@ describe("QmdMemoryManager", () => { it("treats plain-text no-results stdout without punctuation as empty", async () => { spawnMock.mockImplementation((_cmd: string, args: string[]) => { - if (args[0] === "query") { + if (args[0] === "search") { const child = createMockChild({ autoClose: false }); emitAndClose(child, "stdout", "No results found\n\n"); return child; @@ -885,7 +871,7 @@ describe("QmdMemoryManager", () => { it("treats plain-text no-results stderr as an empty result set", async () => { spawnMock.mockImplementation((_cmd: string, args: string[]) => { - if (args[0] === "query") { + if (args[0] === "search") { const child = createMockChild({ autoClose: false }); emitAndClose(child, "stderr", "No results found.\n"); return child; diff --git a/src/memory/qmd-manager.ts b/src/memory/qmd-manager.ts index 389421ab0b1..86015bf50ab 100644 --- a/src/memory/qmd-manager.ts +++ b/src/memory/qmd-manager.ts @@ -262,9 +262,10 @@ export class QmdMemoryManager implements MemorySearchManager { } const qmdSearchCommand = this.qmd.searchMode; const args = this.buildSearchArgs(qmdSearchCommand, trimmed, limit); - if (qmdSearchCommand === "query") { - args.push(...collectionFilterArgs); - } + + // Always scope to managed collections (default + custom). Even for `search`/`vsearch`, + // pass collection filters; if a given QMD build rejects these flags, we fall back to `query`. + args.push(...collectionFilterArgs); let stdout: string; let stderr: string; try {