fix(memory-qmd): restore qmd compatibility defaults

This commit is contained in:
Vincent Koc 2026-04-06 01:31:19 +01:00
parent ca462fb928
commit 098f4eeebb
7 changed files with 51 additions and 10 deletions

View File

@ -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.

View File

@ -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/<agentId>/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.
</Info>
## 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:

View File

@ -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 |

View File

@ -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"),
);

View File

@ -274,7 +274,7 @@ export class QmdMemoryManager implements MemorySearchManager {
private attemptedNullByteCollectionRepair = false;
private attemptedDuplicateDocumentRepair = false;
private readonly sessionWarm = new Set<string>();
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<void> {
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 {

View File

@ -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([]);

View File

@ -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 {