Memory/QMD: prefer exact docid lookup in index

This commit is contained in:
Vignesh Natarajan
2026-02-14 14:56:15 -08:00
parent f9f816d139
commit 6bf333bf31
3 changed files with 77 additions and 3 deletions

View File

@@ -68,6 +68,7 @@ Docs: https://docs.openclaw.ai
- Discord: prefer gateway guild id when logging inbound messages so cached-miss guilds do not appear as `guild=dm`. Thanks @thewilloftheshadow.
- TUI: refactor searchable select list description layout and add regression coverage for ANSI-highlight width bounds.
- Memory/QMD: cap QMD command output buffering to prevent memory exhaustion from pathological `qmd` command output.
- Memory/QMD: query QMD index using exact docid matches before falling back to prefix lookup for better recall correctness and index efficiency.
- Models/CLI: guard `models status` string trimming paths to prevent crashes from malformed non-string config values. (#16395) Thanks @BinHPdev.
## 2026.2.14

View File

@@ -829,6 +829,71 @@ describe("QmdMemoryManager", () => {
await manager.close();
});
it("prefers exact docid match before prefix fallback for qmd document lookups", async () => {
const prepareCalls: string[] = [];
const exactDocid = "abc123";
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
if (args[0] === "search") {
const child = createMockChild({ autoClose: false });
emitAndClose(
child,
"stdout",
JSON.stringify([
{ docid: exactDocid, score: 1, snippet: "@@ -5,2\nremember this\nnext line" },
]),
);
return child;
}
return createMockChild();
});
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
const manager = await QmdMemoryManager.create({ cfg, agentId, resolved });
expect(manager).toBeTruthy();
if (!manager) {
throw new Error("manager missing");
}
const inner = manager as unknown as {
db: { prepare: (query: string) => { get: (arg: unknown) => unknown }; close: () => void };
};
inner.db = {
prepare: (query: string) => {
prepareCalls.push(query);
return {
get: (arg: unknown) => {
if (query.includes("hash = ?")) {
return undefined;
}
if (query.includes("hash LIKE ?")) {
expect(arg).toBe(`${exactDocid}%`);
return { collection: "workspace", path: "notes/welcome.md" };
}
throw new Error(`unexpected sqlite query: ${query}`);
},
};
},
close: () => {},
};
const results = await manager.search("test", { sessionKey: "agent:main:slack:dm:u123" });
expect(results).toEqual([
{
path: "notes/welcome.md",
startLine: 5,
endLine: 6,
score: 1,
snippet: "@@ -5,2\nremember this\nnext line",
source: "memory",
},
]);
expect(prepareCalls).toHaveLength(2);
expect(prepareCalls[0]).toContain("hash = ?");
expect(prepareCalls[1]).toContain("hash LIKE ?");
await manager.close();
});
it("errors when qmd output exceeds command output safety cap", async () => {
const noisyPayload = "x".repeat(240_000);
spawnMock.mockImplementation((_cmd: string, args: string[]) => {

View File

@@ -692,9 +692,17 @@ export class QmdMemoryManager implements MemorySearchManager {
const db = this.ensureDb();
let row: { collection: string; path: string } | undefined;
try {
row = db
.prepare("SELECT collection, path FROM documents WHERE hash LIKE ? AND active = 1 LIMIT 1")
.get(`${normalized}%`) as { collection: string; path: string } | undefined;
const exact = db
.prepare("SELECT collection, path FROM documents WHERE hash = ? AND active = 1 LIMIT 1")
.get(normalized) as { collection: string; path: string } | undefined;
row = exact;
if (!row) {
row = db
.prepare(
"SELECT collection, path FROM documents WHERE hash LIKE ? AND active = 1 LIMIT 1",
)
.get(`${normalized}%`) as { collection: string; path: string } | undefined;
}
} catch (err) {
if (this.isSqliteBusyError(err)) {
log.debug(`qmd index is busy while resolving doc path: ${String(err)}`);