Skip to content

Commit 5e8277e

Browse files
committed
fix(memory): preserve qmd lexical search for hyphenated queries
1 parent 6ebc5e4 commit 5e8277e

3 files changed

Lines changed: 83 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Docs: https://docs.openclaw.ai
4343
- Feishu: detect SecretRef top-level credentials as a configured default account instead of treating object-backed app secrets as missing.
4444
- CLI/completion: resolve concrete PowerShell profile paths and reload commands during setup and doctor completion installation. Fixes #44296. (#83059) Thanks @yu-xin-c.
4545
- 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.
46+
- 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.
4647
- 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.
4748
- 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.
4849
- 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.

extensions/memory-core/src/memory/qmd-manager.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2698,6 +2698,80 @@ describe("QmdMemoryManager", () => {
26982698
await manager.close();
26992699
});
27002700

2701+
it("keeps hyphenated tokens in lexical QMD searches while normalizing semantic searches", async () => {
2702+
cfg = {
2703+
...cfg,
2704+
memory: {
2705+
backend: "qmd",
2706+
qmd: {
2707+
includeDefaultMemory: false,
2708+
searchMode: "query",
2709+
update: { interval: "0s", debounceMs: 60_000, onBoot: false },
2710+
paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }],
2711+
mcporter: { enabled: true, serverName: "qmd", startDaemon: false },
2712+
},
2713+
},
2714+
} as OpenClawConfig;
2715+
2716+
spawnMock.mockImplementation((cmd: string, args: string[]) => {
2717+
const child = createMockChild({ autoClose: false });
2718+
if (isMcporterCommand(cmd) && args[0] === "call") {
2719+
expect(args[1]).toBe("qmd.query");
2720+
const callArgs = JSON.parse(args[args.indexOf("--args") + 1]);
2721+
expect(callArgs.searches).toEqual([
2722+
{ type: "lex", query: "sqlite-vec-qmd backend health 2026-05-04 multi-agent" },
2723+
{ type: "vec", query: "sqlite vec qmd backend health 2026 05 04 multi agent" },
2724+
{ type: "hyde", query: "sqlite vec qmd backend health 2026 05 04 multi agent" },
2725+
]);
2726+
emitAndClose(child, "stdout", JSON.stringify({ results: [] }));
2727+
return child;
2728+
}
2729+
emitAndClose(child, "stdout", "[]");
2730+
return child;
2731+
});
2732+
2733+
const { manager } = await createManager();
2734+
await manager.search("sqlite-vec-qmd backend health 2026-05-04 multi-agent", {
2735+
sessionKey: "agent:main:slack:dm:u123",
2736+
});
2737+
await manager.close();
2738+
});
2739+
2740+
it("normalizes hyphenated tokens for vector-only QMD searches", async () => {
2741+
cfg = {
2742+
...cfg,
2743+
memory: {
2744+
backend: "qmd",
2745+
qmd: {
2746+
includeDefaultMemory: false,
2747+
searchMode: "vsearch",
2748+
update: { interval: "0s", debounceMs: 60_000, onBoot: false },
2749+
paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }],
2750+
mcporter: { enabled: true, serverName: "qmd", startDaemon: false },
2751+
},
2752+
},
2753+
} as OpenClawConfig;
2754+
2755+
spawnMock.mockImplementation((cmd: string, args: string[]) => {
2756+
const child = createMockChild({ autoClose: false });
2757+
if (isMcporterCommand(cmd) && args[0] === "call") {
2758+
expect(args[1]).toBe("qmd.query");
2759+
const callArgs = JSON.parse(args[args.indexOf("--args") + 1]);
2760+
expect(callArgs.searches).toEqual([{ type: "vec", query: "sqlite vec backend health" }]);
2761+
emitAndClose(child, "stdout", JSON.stringify({ results: [] }));
2762+
return child;
2763+
}
2764+
emitAndClose(child, "stdout", "[]");
2765+
return child;
2766+
});
2767+
2768+
const { manager } = await createManager();
2769+
await manager.search("sqlite-vec backend health", {
2770+
sessionKey: "agent:main:slack:dm:u123",
2771+
});
2772+
await manager.close();
2773+
});
2774+
27012775
it("falls back to QMD <1.1 tool names when query tool is not found", async () => {
27022776
// qmdMcpToolVersion is an instance field — each createManager() starts fresh.
27032777

extensions/memory-core/src/memory/qmd-manager.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1952,21 +1952,22 @@ export class QmdMemoryManager implements MemorySearchManager {
19521952
query: string,
19531953
searchCommand?: string,
19541954
): Array<{ type: string; query: string }> {
1955+
const semanticQuery = normalizeQmdSemanticQuery(query);
19551956
switch (searchCommand) {
19561957
case "search":
19571958
// BM25 keyword search only
19581959
return [{ type: "lex", query }];
19591960
case "vsearch":
19601961
// Vector search only
1961-
return [{ type: "vec", query }];
1962+
return [{ type: "vec", query: semanticQuery }];
19621963
case "query":
19631964
case undefined:
19641965
default:
19651966
// Full hybrid: lex + vec + hyde (query expansion)
19661967
return [
19671968
{ type: "lex", query },
1968-
{ type: "vec", query },
1969-
{ type: "hyde", query },
1969+
{ type: "vec", query: semanticQuery },
1970+
{ type: "hyde", query: semanticQuery },
19701971
];
19711972
}
19721973
}
@@ -3149,3 +3150,7 @@ function resolveQmdManagerRuntimeConfig(
31493150
contextLimits: resolveAgentContextLimits(cfg, agentId),
31503151
};
31513152
}
3153+
3154+
function normalizeQmdSemanticQuery(query: string): string {
3155+
return query.replace(/(\w)-(?=\w)/g, "$1 ");
3156+
}

0 commit comments

Comments
 (0)