mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 18:04:32 +00:00
Memory/QMD: harden no-results parsing
This commit is contained in:
committed by
Vignesh
parent
3d343932cf
commit
2f1f82674a
@@ -733,7 +733,6 @@ describe("QmdMemoryManager", () => {
|
|||||||
await manager.close();
|
await manager.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it("treats plain-text no-results stdout as an empty result set", async () => {
|
it("treats plain-text no-results stdout as an empty result set", async () => {
|
||||||
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||||
if (args[0] === "query") {
|
if (args[0] === "query") {
|
||||||
@@ -838,7 +837,6 @@ describe("QmdMemoryManager", () => {
|
|||||||
).rejects.toThrow(/qmd query returned invalid JSON/);
|
).rejects.toThrow(/qmd query returned invalid JSON/);
|
||||||
await manager.close();
|
await manager.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("model cache symlink", () => {
|
describe("model cache symlink", () => {
|
||||||
let defaultModelsDir: string;
|
let defaultModelsDir: string;
|
||||||
let customModelsDir: string;
|
let customModelsDir: string;
|
||||||
@@ -921,7 +919,6 @@ 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> {
|
||||||
|
|||||||
@@ -24,20 +24,13 @@ import { requireNodeSqlite } from "./sqlite.js";
|
|||||||
|
|
||||||
type SqliteDatabase = import("node:sqlite").DatabaseSync;
|
type SqliteDatabase = import("node:sqlite").DatabaseSync;
|
||||||
import type { ResolvedMemoryBackendConfig, ResolvedQmdConfig } from "./backend-config.js";
|
import type { ResolvedMemoryBackendConfig, ResolvedQmdConfig } from "./backend-config.js";
|
||||||
|
import { parseQmdQueryJson } from "./qmd-query-parser.js";
|
||||||
|
|
||||||
const log = createSubsystemLogger("memory");
|
const log = createSubsystemLogger("memory");
|
||||||
|
|
||||||
const SNIPPET_HEADER_RE = /@@\s*-([0-9]+),([0-9]+)/;
|
const SNIPPET_HEADER_RE = /@@\s*-([0-9]+),([0-9]+)/;
|
||||||
const SEARCH_PENDING_UPDATE_WAIT_MS = 500;
|
const SEARCH_PENDING_UPDATE_WAIT_MS = 500;
|
||||||
|
|
||||||
type QmdQueryResult = {
|
|
||||||
docid?: string;
|
|
||||||
score?: number;
|
|
||||||
file?: string;
|
|
||||||
snippet?: string;
|
|
||||||
body?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CollectionRoot = {
|
type CollectionRoot = {
|
||||||
path: string;
|
path: string;
|
||||||
kind: MemorySource;
|
kind: MemorySource;
|
||||||
@@ -278,7 +271,7 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
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));
|
||||||
}
|
}
|
||||||
const parsed = this.parseQmdQueryJson(stdout, stderr);
|
const parsed = parseQmdQueryJson(stdout, stderr);
|
||||||
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);
|
||||||
@@ -976,42 +969,6 @@ 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) {
|
||||||
|
|||||||
47
src/memory/qmd-query-parser.ts
Normal file
47
src/memory/qmd-query-parser.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||||
|
|
||||||
|
const log = createSubsystemLogger("memory");
|
||||||
|
|
||||||
|
export type QmdQueryResult = {
|
||||||
|
docid?: string;
|
||||||
|
score?: number;
|
||||||
|
file?: string;
|
||||||
|
snippet?: string;
|
||||||
|
body?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parseQmdQueryJson(stdout: string, stderr: string): QmdQueryResult[] {
|
||||||
|
const trimmedStdout = stdout.trim();
|
||||||
|
const trimmedStderr = stderr.trim();
|
||||||
|
const stdoutIsMarker = trimmedStdout.length > 0 && isQmdNoResultsOutput(trimmedStdout);
|
||||||
|
const stderrIsMarker = trimmedStderr.length > 0 && isQmdNoResultsOutput(trimmedStderr);
|
||||||
|
if (stdoutIsMarker || (!trimmedStdout && stderrIsMarker)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!trimmedStdout) {
|
||||||
|
const context = trimmedStderr ? ` (stderr: ${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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isQmdNoResultsOutput(raw: string): boolean {
|
||||||
|
const normalized = raw.trim().toLowerCase().replace(/\s+/g, " ");
|
||||||
|
return normalized === "no results found" || normalized === "no results found.";
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeQmdStderr(raw: string): string {
|
||||||
|
return raw.length <= 120 ? raw : `${raw.slice(0, 117)}...`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user