diff --git a/CHANGELOG.md b/CHANGELOG.md index 79dd243a646..9cde929a9b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -125,6 +125,7 @@ Docs: https://docs.openclaw.ai - Discord: keep REST, webhook, and monitor traffic on the configured proxy, preserve component-only media sends, honor `@everyone` and `@here` mention gates, keep ACK reactions on the active account, and split voice connect/playback timeouts so auto-join is more reliable. (#57465, #60361, #60345) Thanks @geekhuashan. - WhatsApp: restore `channels.whatsapp.blockStreaming` and reset watchdog timeouts after reconnect so quiet chats stop falling into reconnect loops. (#60007, #60069) Thanks @MonkeyLeeT and @mcaxtr. - Memory: keep `memory-core` builtin embedding registration on the already-registered path so selecting `memory-core` no longer recurses through plugin discovery and crashes during startup. (#61402) Thanks @ngutman. +- Memory/QMD: prefer modern `qmd collection add --glob`, accept newer single-line JSON hit metadata while keeping legacy line fields, and refresh QMD docs/model-override guidance without breaking older QMD releases. Thanks @vincentkoc. - MS Teams: download inline DM images via Graph API and preserve channel reply threading in proactive fallback. (#52212, #55198) Thanks @Ted-developer and @hyojin. - MS Teams: replace the deprecated Teams SDK HttpPlugin stub with `httpServerAdapter` so recurring gateway deprecation warnings stop firing and the Express 5 compatibility workaround stays on the supported SDK path. (#60939) Thanks @coolramukaka-sys. - Matrix/exec approvals: anchor seeded approval reactions to the primary Matrix prompt event, resolve them from event metadata instead of prompt text, and clean up chunked approval prompts correctly. (#60931) Thanks @gumadeiras. diff --git a/docs/concepts/memory-qmd.md b/docs/concepts/memory-qmd.md index 3c68baeafbb..82a67ed2cc2 100644 --- a/docs/concepts/memory-qmd.md +++ b/docs/concepts/memory-qmd.md @@ -25,7 +25,7 @@ binary, and can index content beyond your workspace memory files. ### Prerequisites -- Install QMD: `bun install -g @tobilu/qmd` +- Install QMD: `npm install -g @tobilu/qmd` or `bun install -g @tobilu/qmd` - SQLite build that allows extensions (`brew install sqlite` on macOS). - QMD must be on the gateway's `PATH`. - macOS and Linux work out of the box. Windows is best supported via WSL2. @@ -43,6 +43,8 @@ binary, and can index content beyond your workspace memory files. OpenClaw creates a self-contained QMD home under `~/.openclaw/agents//qmd/` and manages the sidecar lifecycle automatically -- collections, updates, and embedding runs are handled for you. +It prefers current QMD collection and MCP query shapes, but still falls back to +legacy `--mask` collection flags and older MCP tool names when needed. ## How the sidecar works @@ -59,6 +61,20 @@ The first search may be slow -- QMD auto-downloads GGUF models (~2 GB) for reranking and query expansion on the first `qmd query` run. +## Model overrides + +QMD model environment variables pass through unchanged from the gateway +process, so you can tune QMD globally without adding new OpenClaw config: + +```bash +export QMD_EMBED_MODEL="hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf" +export QMD_RERANK_MODEL="/absolute/path/to/reranker.gguf" +export QMD_GENERATE_MODEL="/absolute/path/to/generator.gguf" +``` + +After changing the embedding model, rerun embeddings so the index matches the +new vector space. + ## Indexing extra paths Point QMD at additional directories to make them searchable: diff --git a/docs/reference/memory-config.md b/docs/reference/memory-config.md index 76cb60ef7fc..93c3959cba3 100644 --- a/docs/reference/memory-config.md +++ b/docs/reference/memory-config.md @@ -377,6 +377,15 @@ Set `memory.backend = "qmd"` to enable. All QMD settings live under | `sessions.retentionDays` | `number` | -- | Transcript retention | | `sessions.exportDir` | `string` | -- | Export directory | +OpenClaw prefers the current QMD collection and MCP query shapes, but keeps +older QMD releases working by falling back to legacy `--mask` collection flags +and older MCP tool names when needed. + +QMD model overrides stay on the QMD side, not OpenClaw config. If you need to +override QMD's models globally, set environment variables such as +`QMD_EMBED_MODEL`, `QMD_RERANK_MODEL`, and `QMD_GENERATE_MODEL` in the gateway +runtime environment. + ### Update schedule | Key | Type | Default | Description | diff --git a/extensions/memory-core/src/memory/qmd-manager.test.ts b/extensions/memory-core/src/memory/qmd-manager.test.ts index 854248fa73b..ec3e9fb34fe 100644 --- a/extensions/memory-core/src/memory/qmd-manager.test.ts +++ b/extensions/memory-core/src/memory/qmd-manager.test.ts @@ -895,7 +895,7 @@ describe("QmdMemoryManager", () => { ); }); - it("prefers --mask for collection add and falls back to --glob when --mask is rejected", async () => { + it("prefers --glob for collection add and falls back to --mask when --glob is rejected", async () => { cfg = { ...cfg, memory: { @@ -917,10 +917,10 @@ describe("QmdMemoryManager", () => { } if (args[0] === "collection" && args[1] === "add") { const child = createMockChild({ autoClose: false }); - const flag = args.includes("--mask") ? "--mask" : args.includes("--glob") ? "--glob" : ""; + const flag = args.includes("--glob") ? "--glob" : args.includes("--mask") ? "--mask" : ""; addFlagCalls.push(flag); - if (flag === "--mask") { - emitAndClose(child, "stderr", "unknown flag: --mask", 1); + if (flag === "--glob") { + emitAndClose(child, "stderr", "unknown flag: --glob", 1); return child; } queueMicrotask(() => child.closeWith(0)); @@ -932,7 +932,7 @@ describe("QmdMemoryManager", () => { const { manager } = await createManager({ mode: "full" }); await manager.close(); - expect(addFlagCalls).toEqual(["--mask", "--glob", "--glob", "--glob"]); + expect(addFlagCalls).toEqual(["--glob", "--mask", "--mask", "--mask"]); expect(logWarnMock).toHaveBeenCalledWith( expect.stringContaining("retrying with legacy compatibility flag"), ); diff --git a/extensions/memory-core/src/memory/qmd-manager.ts b/extensions/memory-core/src/memory/qmd-manager.ts index cc316f8f8df..d3ea9e501c5 100644 --- a/extensions/memory-core/src/memory/qmd-manager.ts +++ b/extensions/memory-core/src/memory/qmd-manager.ts @@ -274,7 +274,7 @@ export class QmdMemoryManager implements MemorySearchManager { private attemptedNullByteCollectionRepair = false; private attemptedDuplicateDocumentRepair = false; private readonly sessionWarm = new Set(); - private collectionPatternFlag: QmdCollectionPatternFlag | null = "--mask"; + private collectionPatternFlag: QmdCollectionPatternFlag | null = "--glob"; private constructor(params: { cfg: OpenClawConfig; @@ -653,7 +653,7 @@ export class QmdMemoryManager implements MemorySearchManager { private async addCollection(pathArg: string, name: string, pattern: string): Promise { const candidateFlags: QmdCollectionPatternFlag[] = - this.collectionPatternFlag === "--mask" ? ["--mask", "--glob"] : ["--glob", "--mask"]; + this.collectionPatternFlag === "--glob" ? ["--glob", "--mask"] : ["--mask", "--glob"]; let lastError: unknown; for (const flag of candidateFlags) { try { diff --git a/src/memory-host-sdk/host/qmd-query-parser.test.ts b/src/memory-host-sdk/host/qmd-query-parser.test.ts index 34134be5cd4..e16ffa9150e 100644 --- a/src/memory-host-sdk/host/qmd-query-parser.test.ts +++ b/src/memory-host-sdk/host/qmd-query-parser.test.ts @@ -40,6 +40,18 @@ complete`, ]); }); + it("maps single-line qmd line metadata onto both line bounds", () => { + const results = parseQmdQueryJson('[{"docid":"abc","score":0.5,"line":9}]', ""); + expect(results).toEqual([ + { + docid: "abc", + score: 0.5, + startLine: 9, + endLine: 9, + }, + ]); + }); + it("treats plain-text no-results from stderr as an empty result set", () => { const results = parseQmdQueryJson("", "No results found\n"); expect(results).toEqual([]); diff --git a/src/memory-host-sdk/host/qmd-query-parser.ts b/src/memory-host-sdk/host/qmd-query-parser.ts index 680999924e5..3c44f9d86e1 100644 --- a/src/memory-host-sdk/host/qmd-query-parser.ts +++ b/src/memory-host-sdk/host/qmd-query-parser.ts @@ -89,6 +89,9 @@ function parseQmdQueryResultArray(raw: string): QmdQueryResult[] | null { const file = typeof record.file === "string" ? record.file : undefined; const snippet = typeof record.snippet === "string" ? record.snippet : undefined; const body = typeof record.body === "string" ? record.body : undefined; + const line = parseQmdLineNumber(record.line); + const startLine = parseQmdLineNumber(record.start_line ?? record.startLine) ?? line; + const endLine = parseQmdLineNumber(record.end_line ?? record.endLine) ?? line; return { docid, score, @@ -96,8 +99,8 @@ function parseQmdQueryResultArray(raw: string): QmdQueryResult[] | null { file, snippet, body, - startLine: parseQmdLineNumber(record.start_line ?? record.startLine), - endLine: parseQmdLineNumber(record.end_line ?? record.endLine), + startLine, + endLine, } as QmdQueryResult; }); } catch {