mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 10:01:24 +00:00
Memory/QMD: prefer exact docid lookup in index
This commit is contained in:
@@ -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.
|
- 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.
|
- 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: 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.
|
- Models/CLI: guard `models status` string trimming paths to prevent crashes from malformed non-string config values. (#16395) Thanks @BinHPdev.
|
||||||
|
|
||||||
## 2026.2.14
|
## 2026.2.14
|
||||||
|
|||||||
@@ -829,6 +829,71 @@ describe("QmdMemoryManager", () => {
|
|||||||
await manager.close();
|
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 () => {
|
it("errors when qmd output exceeds command output safety cap", async () => {
|
||||||
const noisyPayload = "x".repeat(240_000);
|
const noisyPayload = "x".repeat(240_000);
|
||||||
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||||
|
|||||||
@@ -692,9 +692,17 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
const db = this.ensureDb();
|
const db = this.ensureDb();
|
||||||
let row: { collection: string; path: string } | undefined;
|
let row: { collection: string; path: string } | undefined;
|
||||||
try {
|
try {
|
||||||
row = db
|
const exact = db
|
||||||
.prepare("SELECT collection, path FROM documents WHERE hash LIKE ? AND active = 1 LIMIT 1")
|
.prepare("SELECT collection, path FROM documents WHERE hash = ? AND active = 1 LIMIT 1")
|
||||||
.get(`${normalized}%`) as { collection: string; path: string } | undefined;
|
.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) {
|
} catch (err) {
|
||||||
if (this.isSqliteBusyError(err)) {
|
if (this.isSqliteBusyError(err)) {
|
||||||
log.debug(`qmd index is busy while resolving doc path: ${String(err)}`);
|
log.debug(`qmd index is busy while resolving doc path: ${String(err)}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user