fix(memory): parse qmd vector status variants

This commit is contained in:
Peter Steinberger
2026-04-27 14:56:40 +01:00
parent 1f7b7c249a
commit f9946eb069
3 changed files with 68 additions and 5 deletions

View File

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

View File

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

View File

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