mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 22:54:33 +00:00
Memory/QMD: robustly parse noisy qmd JSON output
This commit is contained in:
@@ -69,6 +69,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- 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.
|
- Memory/QMD: query QMD index using exact docid matches before falling back to prefix lookup for better recall correctness and index efficiency.
|
||||||
|
- Memory/QMD: make QMD result JSON parsing resilient to noisy command output by extracting the first JSON array from noisy `stdout`.
|
||||||
- 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
|
||||||
|
|||||||
37
src/memory/qmd-query-parser.test.ts
Normal file
37
src/memory/qmd-query-parser.test.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { parseQmdQueryJson } from "./qmd-query-parser.js";
|
||||||
|
|
||||||
|
describe("parseQmdQueryJson", () => {
|
||||||
|
it("parses clean qmd JSON output", () => {
|
||||||
|
const results = parseQmdQueryJson('[{"docid":"abc","score":1,"snippet":"@@ -1,1\\none"}]', "");
|
||||||
|
expect(results).toEqual([
|
||||||
|
{
|
||||||
|
docid: "abc",
|
||||||
|
score: 1,
|
||||||
|
snippet: "@@ -1,1\none",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("extracts embedded result arrays from noisy stdout", () => {
|
||||||
|
const results = parseQmdQueryJson(
|
||||||
|
`initializing
|
||||||
|
{"payload":"ok"}
|
||||||
|
[{"docid":"abc","score":0.5}]
|
||||||
|
complete`,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
expect(results).toEqual([{ docid: "abc", score: 0.5 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats plain-text no-results from stderr as an empty result set", () => {
|
||||||
|
const results = parseQmdQueryJson("", "No results found\n");
|
||||||
|
expect(results).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when stdout cannot be interpreted as qmd JSON", () => {
|
||||||
|
expect(() => parseQmdQueryJson("this is not json", "")).toThrow(
|
||||||
|
/qmd query returned invalid JSON/i,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -25,11 +25,19 @@ export function parseQmdQueryJson(stdout: string, stderr: string): QmdQueryResul
|
|||||||
throw new Error(`qmd query returned invalid JSON: ${message}`);
|
throw new Error(`qmd query returned invalid JSON: ${message}`);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(trimmedStdout) as unknown;
|
const parsed = parseQmdQueryResultArray(trimmedStdout);
|
||||||
if (!Array.isArray(parsed)) {
|
if (parsed !== null) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
const noisyPayload = extractFirstJsonArray(trimmedStdout);
|
||||||
|
if (!noisyPayload) {
|
||||||
throw new Error("qmd query JSON response was not an array");
|
throw new Error("qmd query JSON response was not an array");
|
||||||
}
|
}
|
||||||
return parsed as QmdQueryResult[];
|
const fallback = parseQmdQueryResultArray(noisyPayload);
|
||||||
|
if (fallback !== null) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
throw new Error("qmd query JSON response was not an array");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
log.warn(`qmd query returned invalid JSON: ${message}`);
|
log.warn(`qmd query returned invalid JSON: ${message}`);
|
||||||
@@ -45,3 +53,56 @@ function isQmdNoResultsOutput(raw: string): boolean {
|
|||||||
function summarizeQmdStderr(raw: string): string {
|
function summarizeQmdStderr(raw: string): string {
|
||||||
return raw.length <= 120 ? raw : `${raw.slice(0, 117)}...`;
|
return raw.length <= 120 ? raw : `${raw.slice(0, 117)}...`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseQmdQueryResultArray(raw: string): QmdQueryResult[] | null {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as unknown;
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsed as QmdQueryResult[];
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFirstJsonArray(raw: string): string | null {
|
||||||
|
const start = raw.indexOf("[");
|
||||||
|
if (start < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let depth = 0;
|
||||||
|
let inString = false;
|
||||||
|
let escaped = false;
|
||||||
|
for (let i = start; i < raw.length; i += 1) {
|
||||||
|
const char = raw[i];
|
||||||
|
if (char === undefined) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (inString) {
|
||||||
|
if (escaped) {
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === "\\") {
|
||||||
|
escaped = true;
|
||||||
|
} else if (char === '"') {
|
||||||
|
inString = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === '"') {
|
||||||
|
inString = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === "[") {
|
||||||
|
depth += 1;
|
||||||
|
} else if (char === "]") {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth === 0) {
|
||||||
|
return raw.slice(start, i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user