From f9946eb0695020ead117566b97d53c8bcef06248 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 14:56:40 +0100 Subject: [PATCH] fix(memory): parse qmd vector status variants --- CHANGELOG.md | 1 + .../src/memory/qmd-manager.test.ts | 58 +++++++++++++++++++ .../memory-core/src/memory/qmd-manager.ts | 14 +++-- 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 706469d3a84..508d00a6513 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,7 @@ Docs: https://docs.openclaw.ai - Ollama/onboarding: de-dupe suggested bare local models against installed `:latest` tags and skip redundant pulls, so setup shows the installed model once and no longer says it is downloading an already available model. Fixes #68952. Thanks @tleyden. - Memory-core/doctor: keep `doctor.memory.status` on the cached path by default and only run live embedding pings for explicit deep probes, preventing slow local embedding backends from blocking Gateway status checks. Fixes #71568. Thanks @apex-system. - Memory/QMD: group same-source collections into one QMD search invocation when the installed QMD supports multiple `-c` filters, while keeping older QMD builds on the per-collection fallback. Fixes #72484; supersedes #72485 and #69583. Thanks @BsnizND and @zeroaltitude. +- Memory/QMD: accept QMD status vector-count variants such as `Vectors = 42`, `Vectors:42`, and `Vectors: 42 embedded`, so `memory status --deep` no longer reports embeddings unavailable for healthy QMD wrappers. Fixes #63652; carries forward #63678. Thanks @apoapostolov and @WarrenJones. - Memory/QMD: skip QMD vector status probes and embedding maintenance in lexical `searchMode: "search"`, so BM25-only QMD setups on ARM do not trigger llama.cpp/Vulkan builds during status checks or embed cycles. Fixes #59234 and #67113. Thanks @PrinceOfEgypt, @Vksh07, @Snipe76, @NomLom, @t4r3e2q1-commits, and @dmak. - Memory/QMD: report the live watcher dirty state in memory status, so changed QMD-backed memory files show as dirty until the queued sync finishes. Fixes #60244. Thanks @xinzf. - Compaction: skip oversized pre-compaction checkpoint snapshots and prune duplicate long user turns from compaction input and rotated successor transcripts, preventing retry storms from being preserved across checkpoint cycles. Fixes #72780. Thanks @SweetSophia. diff --git a/extensions/memory-core/src/memory/qmd-manager.test.ts b/extensions/memory-core/src/memory/qmd-manager.test.ts index 87408589f2b..a72862ce5ae 100644 --- a/extensions/memory-core/src/memory/qmd-manager.test.ts +++ b/extensions/memory-core/src/memory/qmd-manager.test.ts @@ -4798,6 +4798,64 @@ describe("QmdMemoryManager", () => { await manager.close(); }); + it.each([ + ["equals separator", "Documents: 12\nVectors = 42\n"], + ["tab separator", "Documents: 12\nVectors:\t42\n"], + ["compact separator", "Documents: 12\nVectors:42\n"], + ["embedded suffix", "Documents: 12\nVectors: 42 embedded\n"], + ])("reports vector availability as ready for qmd status %s", async (_name, statusOutput) => { + spawnMock.mockImplementation((_cmd: string, args: string[]) => { + if (args[0] === "status") { + const child = createMockChild({ autoClose: false }); + emitAndClose(child, "stdout", statusOutput); + return child; + } + return createMockChild(); + }); + + const { manager } = await createManager({ + cfg: { + ...cfg, + memory: { + ...cfg.memory, + qmd: { ...cfg.memory?.qmd, searchMode: "query" }, + }, + } as OpenClawConfig, + }); + + await expect(manager.probeVectorAvailability()).resolves.toBe(true); + await manager.close(); + }); + + it("does not parse unrelated qmd status vector-like fields", async () => { + spawnMock.mockImplementation((_cmd: string, args: string[]) => { + if (args[0] === "status") { + const child = createMockChild({ autoClose: false }); + emitAndClose(child, "stdout", "Documents: 12\nMaxVectors: 42\nVector index: yes\n"); + return child; + } + return createMockChild(); + }); + + const { manager } = await createManager({ + cfg: { + ...cfg, + memory: { + ...cfg.memory, + qmd: { ...cfg.memory?.qmd, searchMode: "query" }, + }, + } as OpenClawConfig, + }); + + await expect(manager.probeVectorAvailability()).resolves.toBe(false); + expect(manager.status().vector).toEqual({ + enabled: true, + available: false, + loadError: "Could not determine QMD vector status from `qmd status`", + }); + await manager.close(); + }); + it("skips qmd status vector probes for lexical search mode", async () => { const { manager } = await createManager({ cfg: { diff --git a/extensions/memory-core/src/memory/qmd-manager.ts b/extensions/memory-core/src/memory/qmd-manager.ts index 0cfb77b3b27..3a6eb598632 100644 --- a/extensions/memory-core/src/memory/qmd-manager.ts +++ b/extensions/memory-core/src/memory/qmd-manager.ts @@ -154,12 +154,16 @@ function normalizeHanBm25Query(query: string): string { } function parseQmdStatusVectorCount(raw: string): number | null { - const match = raw.match(/(?:^|\n)\s*Vectors:\s*(\d+)\b/i); - if (!match) { - return null; + for (const line of raw.split(/\r?\n/)) { + const match = line.match(/^\s*Vectors(?:\s*[:=]\s*|\s+)(\d+)\b/i); + if (match?.[1]) { + const count = Number.parseInt(match[1], 10); + if (Number.isFinite(count)) { + return count; + } + } } - const count = Number.parseInt(match[1] ?? "", 10); - return Number.isFinite(count) ? count : null; + return null; } function resolveStableJitterMs(params: { seed: string; windowMs: number }): number {