mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-09 05:19:32 +08:00
Memory/QMD: treat plain-text no-results as empty
This commit is contained in:
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/CLI: when `gateway.bind=lan`, use a LAN IP for probe URLs and Control UI links. (#11448) Thanks @AnonO6.
|
||||
- Memory: set Voyage embeddings `input_type` for improved retrieval. (#10818) Thanks @mcinteerj.
|
||||
- Memory/QMD: run boot refresh in background by default, add configurable QMD maintenance timeouts, and retry QMD after fallback failures. (#9690, #9705)
|
||||
- Memory/QMD: treat plain-text `No results found` output from QMD as an empty result instead of throwing invalid JSON errors. (#9824)
|
||||
- Media understanding: recognize `.caf` audio attachments for transcription. (#10982) Thanks @succ985.
|
||||
- State dir: honor `OPENCLAW_STATE_DIR` for default device identity and canvas storage paths. (#4824) Thanks @kossoy.
|
||||
- Tests: harden flaky hotspots by removing timer sleeps, consolidating onboarding provider-auth coverage, and improving memory test realism. (#11598) Thanks @gumadeiras.
|
||||
|
||||
@@ -547,6 +547,58 @@ describe("QmdMemoryManager", () => {
|
||||
).rejects.toThrow("qmd index busy while reading results");
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("treats plain-text no-results stdout as an empty result set", async () => {
|
||||
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||
if (args[0] === "query") {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
setTimeout(() => {
|
||||
child.stdout.emit("data", "No results found.");
|
||||
child.closeWith(0);
|
||||
}, 0);
|
||||
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");
|
||||
}
|
||||
|
||||
await expect(
|
||||
manager.search("missing", { sessionKey: "agent:main:slack:dm:u123" }),
|
||||
).resolves.toEqual([]);
|
||||
await manager.close();
|
||||
});
|
||||
|
||||
it("treats plain-text no-results stderr as an empty result set", async () => {
|
||||
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||
if (args[0] === "query") {
|
||||
const child = createMockChild({ autoClose: false });
|
||||
setTimeout(() => {
|
||||
child.stderr.emit("data", "No results found.\n");
|
||||
child.closeWith(0);
|
||||
}, 0);
|
||||
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");
|
||||
}
|
||||
|
||||
await expect(
|
||||
manager.search("missing", { sessionKey: "agent:main:slack:dm:u123" }),
|
||||
).resolves.toEqual([]);
|
||||
await manager.close();
|
||||
});
|
||||
});
|
||||
|
||||
async function waitForCondition(check: () => boolean, timeoutMs: number): Promise<void> {
|
||||
|
||||
@@ -255,21 +255,16 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
);
|
||||
const args = ["query", trimmed, "--json", "-n", String(limit)];
|
||||
let stdout: string;
|
||||
let stderr: string;
|
||||
try {
|
||||
const result = await this.runQmd(args, { timeoutMs: this.qmd.limits.timeoutMs });
|
||||
stdout = result.stdout;
|
||||
stderr = result.stderr;
|
||||
} catch (err) {
|
||||
log.warn(`qmd query failed: ${String(err)}`);
|
||||
throw err instanceof Error ? err : new Error(String(err));
|
||||
}
|
||||
let parsed: QmdQueryResult[] = [];
|
||||
try {
|
||||
parsed = JSON.parse(stdout);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
log.warn(`qmd query returned invalid JSON: ${message}`);
|
||||
throw new Error(`qmd query returned invalid JSON: ${message}`, { cause: err });
|
||||
}
|
||||
const parsed = this.parseQmdQueryJson(stdout, stderr);
|
||||
const results: MemorySearchResult[] = [];
|
||||
for (const entry of parsed) {
|
||||
const doc = await this.resolveDocLocation(entry.docid);
|
||||
@@ -895,4 +890,30 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
new Promise<void>((resolve) => setTimeout(resolve, SEARCH_PENDING_UPDATE_WAIT_MS)),
|
||||
]);
|
||||
}
|
||||
|
||||
private parseQmdQueryJson(stdout: string, stderr: string): QmdQueryResult[] {
|
||||
if (this.isQmdNoResultsOutput(stdout) || this.isQmdNoResultsOutput(stderr)) {
|
||||
return [];
|
||||
}
|
||||
const trimmed = stdout.trim();
|
||||
if (!trimmed) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as unknown;
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error("qmd query JSON response was not an array");
|
||||
}
|
||||
return parsed as QmdQueryResult[];
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
log.warn(`qmd query returned invalid JSON: ${message}`);
|
||||
throw new Error(`qmd query returned invalid JSON: ${message}`, { cause: err });
|
||||
}
|
||||
}
|
||||
|
||||
private isQmdNoResultsOutput(raw: string): boolean {
|
||||
const normalized = raw.trim().toLowerCase().replace(/\s+/g, " ");
|
||||
return normalized === "no results found" || normalized === "no results found.";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user