mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-28 21:01:43 +03:00
Memory/QMD: treat plain-text no-results as empty
This commit is contained in:
committed by
Vignesh
parent
4baa43384a
commit
3d343932cf
@@ -91,6 +91,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Memory/QMD: run boot refresh in background by default, add configurable QMD maintenance timeouts, retry QMD after fallback failures, and scope QMD queries to OpenClaw-managed collections. (#9690, #9705, #10042) Thanks @vignesh07.
|
- Memory/QMD: run boot refresh in background by default, add configurable QMD maintenance timeouts, retry QMD after fallback failures, and scope QMD queries to OpenClaw-managed collections. (#9690, #9705, #10042) Thanks @vignesh07.
|
||||||
- Memory/QMD: initialize QMD backend on gateway startup so background update timers restart after process reloads. (#10797) Thanks @vignesh07.
|
- Memory/QMD: initialize QMD backend on gateway startup so background update timers restart after process reloads. (#10797) Thanks @vignesh07.
|
||||||
- Config/Memory: auto-migrate legacy top-level `memorySearch` settings into `agents.defaults.memorySearch`. (#11278, #9143) Thanks @vignesh07.
|
- Config/Memory: auto-migrate legacy top-level `memorySearch` settings into `agents.defaults.memorySearch`. (#11278, #9143) Thanks @vignesh07.
|
||||||
|
- 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.
|
- 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.
|
- State dir: honor `OPENCLAW_STATE_DIR` for default device identity and canvas storage paths. (#4824) Thanks @kossoy.
|
||||||
|
|
||||||
|
|||||||
@@ -733,6 +733,112 @@ describe("QmdMemoryManager", () => {
|
|||||||
await manager.close();
|
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 stdout without punctuation as empty", async () => {
|
||||||
|
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||||
|
if (args[0] === "query") {
|
||||||
|
const child = createMockChild({ autoClose: false });
|
||||||
|
setTimeout(() => {
|
||||||
|
child.stdout.emit("data", "No results found\n\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();
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when stdout is empty without the no-results marker", async () => {
|
||||||
|
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||||
|
if (args[0] === "query") {
|
||||||
|
const child = createMockChild({ autoClose: false });
|
||||||
|
setTimeout(() => {
|
||||||
|
child.stdout.emit("data", " \n");
|
||||||
|
child.stderr.emit("data", "unexpected parser error");
|
||||||
|
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" }),
|
||||||
|
).rejects.toThrow(/qmd query returned invalid JSON/);
|
||||||
|
await manager.close();
|
||||||
|
});
|
||||||
|
|
||||||
describe("model cache symlink", () => {
|
describe("model cache symlink", () => {
|
||||||
let defaultModelsDir: string;
|
let defaultModelsDir: string;
|
||||||
let customModelsDir: string;
|
let customModelsDir: string;
|
||||||
@@ -815,6 +921,7 @@ describe("QmdMemoryManager", () => {
|
|||||||
await manager!.close();
|
await manager!.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function waitForCondition(check: () => boolean, timeoutMs: number): Promise<void> {
|
async function waitForCondition(check: () => boolean, timeoutMs: number): Promise<void> {
|
||||||
|
|||||||
@@ -269,21 +269,16 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
}
|
}
|
||||||
const args = ["query", trimmed, "--json", "-n", String(limit), ...collectionFilterArgs];
|
const args = ["query", trimmed, "--json", "-n", String(limit), ...collectionFilterArgs];
|
||||||
let stdout: string;
|
let stdout: string;
|
||||||
|
let stderr: string;
|
||||||
try {
|
try {
|
||||||
const result = await this.runQmd(args, { timeoutMs: this.qmd.limits.timeoutMs });
|
const result = await this.runQmd(args, { timeoutMs: this.qmd.limits.timeoutMs });
|
||||||
stdout = result.stdout;
|
stdout = result.stdout;
|
||||||
|
stderr = result.stderr;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.warn(`qmd query failed: ${String(err)}`);
|
log.warn(`qmd query failed: ${String(err)}`);
|
||||||
throw err instanceof Error ? err : new Error(String(err));
|
throw err instanceof Error ? err : new Error(String(err));
|
||||||
}
|
}
|
||||||
let parsed: QmdQueryResult[] = [];
|
const parsed = this.parseQmdQueryJson(stdout, stderr);
|
||||||
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 results: MemorySearchResult[] = [];
|
const results: MemorySearchResult[] = [];
|
||||||
for (const entry of parsed) {
|
for (const entry of parsed) {
|
||||||
const doc = await this.resolveDocLocation(entry.docid);
|
const doc = await this.resolveDocLocation(entry.docid);
|
||||||
@@ -981,6 +976,42 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private parseQmdQueryJson(stdout: string, stderr: string): QmdQueryResult[] {
|
||||||
|
const trimmedStdout = stdout.trim();
|
||||||
|
const trimmedStderr = stderr.trim();
|
||||||
|
const stdoutIsMarker = Boolean(trimmedStdout) && this.isQmdNoResultsOutput(trimmedStdout);
|
||||||
|
const stderrIsMarker = Boolean(trimmedStderr) && this.isQmdNoResultsOutput(trimmedStderr);
|
||||||
|
if (stdoutIsMarker || (!trimmedStdout && stderrIsMarker)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!trimmedStdout) {
|
||||||
|
const context = trimmedStderr ? ` (stderr: ${this.summarizeQmdStderr(trimmedStderr)})` : "";
|
||||||
|
const message = `stdout empty${context}`;
|
||||||
|
log.warn(`qmd query returned invalid JSON: ${message}`);
|
||||||
|
throw new Error(`qmd query returned invalid JSON: ${message}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmedStdout) 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.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private summarizeQmdStderr(raw: string): string {
|
||||||
|
return raw.length <= 120 ? raw : `${raw.slice(0, 117)}...`;
|
||||||
|
}
|
||||||
|
|
||||||
private buildCollectionFilterArgs(): string[] {
|
private buildCollectionFilterArgs(): string[] {
|
||||||
const names = this.qmd.collections.map((collection) => collection.name).filter(Boolean);
|
const names = this.qmd.collections.map((collection) => collection.name).filter(Boolean);
|
||||||
if (names.length === 0) {
|
if (names.length === 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user