fix(memory): preserve qmd lexical search for hyphenated queries (#81423)

This commit is contained in:
Gio Della-Libera
2026-05-17 09:52:04 -07:00
committed by GitHub
parent 893f580072
commit 44c3d8ea2e
3 changed files with 83 additions and 3 deletions

View File

@@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
- Feishu: detect SecretRef top-level credentials as a configured default account instead of treating object-backed app secrets as missing.
- CLI/completion: resolve concrete PowerShell profile paths and reload commands during setup and doctor completion installation. Fixes #44296. (#83059) Thanks @yu-xin-c.
- Providers/Google: preserve and recover Gemini 3 tool-call thought signatures during native replay so function-calling turns no longer fail with missing `thought_signature` 400s. Fixes #72879. (#80358) Thanks @abnershang.
- Memory/QMD: keep lexical search on raw hyphenated queries while normalizing semantic QMD sub-searches, avoiding fallback to the builtin index for dashed identifiers and dates. Fixes #81328.
- Memory-core: distinguish sqlite-vec load failures from missing semantic vector embeddings in degraded `memory index` warnings, so vector recall diagnostics point at unresolved dimensions instead of blaming sqlite-vec when the store is ready. Fixes #75624. (#83056) Thanks @xuruiray and @Noah3521.
- Agents/subagents: preserve sandbox-peer controller ownership while routing completion announcements back to the originating run session, keeping subagent control and completion delivery scoped correctly. Fixes #80201. (#80242) Thanks @Jerry-Xin.
- Gateway: continue restarting remaining channels when one hot-reload channel restart fails, while still reporting aggregate reload failure and rolling back plugin pre-replace stops. Fixes #83054. Thanks @zqchris.

View File

@@ -2698,6 +2698,80 @@ describe("QmdMemoryManager", () => {
await manager.close();
});
it("keeps hyphenated tokens in lexical QMD searches while normalizing semantic searches", async () => {
cfg = {
...cfg,
memory: {
backend: "qmd",
qmd: {
includeDefaultMemory: false,
searchMode: "query",
update: { interval: "0s", debounceMs: 60_000, onBoot: false },
paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }],
mcporter: { enabled: true, serverName: "qmd", startDaemon: false },
},
},
} as OpenClawConfig;
spawnMock.mockImplementation((cmd: string, args: string[]) => {
const child = createMockChild({ autoClose: false });
if (isMcporterCommand(cmd) && args[0] === "call") {
expect(args[1]).toBe("qmd.query");
const callArgs = JSON.parse(args[args.indexOf("--args") + 1]);
expect(callArgs.searches).toEqual([
{ type: "lex", query: "sqlite-vec-qmd backend health 2026-05-04 multi-agent" },
{ type: "vec", query: "sqlite vec qmd backend health 2026 05 04 multi agent" },
{ type: "hyde", query: "sqlite vec qmd backend health 2026 05 04 multi agent" },
]);
emitAndClose(child, "stdout", JSON.stringify({ results: [] }));
return child;
}
emitAndClose(child, "stdout", "[]");
return child;
});
const { manager } = await createManager();
await manager.search("sqlite-vec-qmd backend health 2026-05-04 multi-agent", {
sessionKey: "agent:main:slack:dm:u123",
});
await manager.close();
});
it("normalizes hyphenated tokens for vector-only QMD searches", async () => {
cfg = {
...cfg,
memory: {
backend: "qmd",
qmd: {
includeDefaultMemory: false,
searchMode: "vsearch",
update: { interval: "0s", debounceMs: 60_000, onBoot: false },
paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }],
mcporter: { enabled: true, serverName: "qmd", startDaemon: false },
},
},
} as OpenClawConfig;
spawnMock.mockImplementation((cmd: string, args: string[]) => {
const child = createMockChild({ autoClose: false });
if (isMcporterCommand(cmd) && args[0] === "call") {
expect(args[1]).toBe("qmd.query");
const callArgs = JSON.parse(args[args.indexOf("--args") + 1]);
expect(callArgs.searches).toEqual([{ type: "vec", query: "sqlite vec backend health" }]);
emitAndClose(child, "stdout", JSON.stringify({ results: [] }));
return child;
}
emitAndClose(child, "stdout", "[]");
return child;
});
const { manager } = await createManager();
await manager.search("sqlite-vec backend health", {
sessionKey: "agent:main:slack:dm:u123",
});
await manager.close();
});
it("falls back to QMD <1.1 tool names when query tool is not found", async () => {
// qmdMcpToolVersion is an instance field — each createManager() starts fresh.

View File

@@ -1952,21 +1952,22 @@ export class QmdMemoryManager implements MemorySearchManager {
query: string,
searchCommand?: string,
): Array<{ type: string; query: string }> {
const semanticQuery = normalizeQmdSemanticQuery(query);
switch (searchCommand) {
case "search":
// BM25 keyword search only
return [{ type: "lex", query }];
case "vsearch":
// Vector search only
return [{ type: "vec", query }];
return [{ type: "vec", query: semanticQuery }];
case "query":
case undefined:
default:
// Full hybrid: lex + vec + hyde (query expansion)
return [
{ type: "lex", query },
{ type: "vec", query },
{ type: "hyde", query },
{ type: "vec", query: semanticQuery },
{ type: "hyde", query: semanticQuery },
];
}
}
@@ -3149,3 +3150,7 @@ function resolveQmdManagerRuntimeConfig(
contextLimits: resolveAgentContextLimits(cfg, agentId),
};
}
function normalizeQmdSemanticQuery(query: string): string {
return query.replace(/(\w)-(?=\w)/g, "$1 ");
}