From 81628fb3232a3cf6fce04f2aa0a8d11a98331c76 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Jul 2026 04:09:28 -0400 Subject: [PATCH] fix(memory): preserve QMD results in noisy command output (#115023) --- .../src/host/qmd-query-parser.test.ts | 38 +++++++++++++ .../src/host/qmd-query-parser.ts | 56 +++++++++++++------ 2 files changed, 78 insertions(+), 16 deletions(-) diff --git a/packages/memory-host-sdk/src/host/qmd-query-parser.test.ts b/packages/memory-host-sdk/src/host/qmd-query-parser.test.ts index ab894abbcd48..10080bef0518 100644 --- a/packages/memory-host-sdk/src/host/qmd-query-parser.test.ts +++ b/packages/memory-host-sdk/src/host/qmd-query-parser.test.ts @@ -25,6 +25,35 @@ complete`, expect(results).toEqual([{ docid: "abc", score: 0.5 }]); }); + it.each([ + { + name: "bracketed log prefix", + stdout: '[qmd] initializing search\n[{"docid":"abc","score":0.5}]', + }, + { + name: "bracketed log with an empty array", + stdout: '[qmd] filters=[]\n[{"docid":"abc","score":0.5}]', + }, + { + name: "bracketed log with result-shaped data", + stdout: '[qmd] previous=[{"docid":"stale","score":1}]\n[{"docid":"abc","score":0.5}]', + }, + { + name: "leading no-results marker", + stdout: 'warning: no results found\n[{"docid":"abc","score":0.5}]', + }, + { + name: "trailing no-results marker", + stdout: '[{"docid":"abc","score":0.5}]\nwarning: no results found', + }, + { + name: "bracketed no-results marker", + stdout: '[qmd] warning: no results found\n[{"docid":"abc","score":0.5}]', + }, + ])("preserves query results after a $name", ({ stdout }) => { + expect(parseQmdQueryJson(stdout, "")).toEqual([{ docid: "abc", score: 0.5 }]); + }); + it("preserves explicit qmd line metadata when present", () => { const results = parseQmdQueryJson( '[{"docid":"abc","score":0.5,"start_line":4,"end_line":6,"snippet":"@@ -10,1\\nignored"}]', @@ -58,6 +87,9 @@ complete`, it("treats prefixed no-results marker output as an empty result set", () => { expect(parseQmdQueryJson("warning: no results found", "")).toStrictEqual([]); expect(parseQmdQueryJson("", "[qmd] warning: no results found\n")).toStrictEqual([]); + expect( + parseQmdQueryJson("[qmd] initializing search\nwarning: no results found", ""), + ).toStrictEqual([]); }); it("keeps bounded stderr context UTF-16 safe", () => { @@ -81,6 +113,12 @@ complete`, ); }); + it("rejects malformed bracket-heavy stdout without repeated rescanning", () => { + expect(() => parseQmdQueryJson("[".repeat(4_096), "")).toThrow( + /qmd query returned invalid JSON/i, + ); + }); + it("routes invalid-output diagnostics through console capture", () => { vi.stubEnv("VITEST", ""); vi.stubEnv("NODE_ENV", "production"); diff --git a/packages/memory-host-sdk/src/host/qmd-query-parser.ts b/packages/memory-host-sdk/src/host/qmd-query-parser.ts index f12e1f7872b7..5fc3384e30f0 100644 --- a/packages/memory-host-sdk/src/host/qmd-query-parser.ts +++ b/packages/memory-host-sdk/src/host/qmd-query-parser.ts @@ -21,9 +21,8 @@ export type QmdQueryResult = { 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)) { + if (!trimmedStdout && stderrIsMarker) { return []; } if (!trimmedStdout) { @@ -39,6 +38,9 @@ export function parseQmdQueryJson(stdout: string, stderr: string): QmdQueryResul } const noisyPayload = extractFirstJsonArray(trimmedStdout); if (!noisyPayload) { + if (isQmdNoResultsOutput(trimmedStdout)) { + return []; + } throw new Error("qmd query JSON response was not an array"); } const fallback = parseQmdQueryResultArray(noisyPayload); @@ -127,26 +129,43 @@ function parseQmdLineNumber(value: unknown): number | undefined { return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined; } -/** Extract the first complete JSON array from noisy stdout. */ +/** Extract the first complete, standalone JSON result array from noisy stdout. */ function extractFirstJsonArray(raw: string): string | null { - const start = raw.indexOf("["); - if (start < 0) { - return null; - } + let start = -1; let depth = 0; let inString = false; let escaped = false; - for (let i = start; i < raw.length; i += 1) { + let atLineStart = true; + + for (let i = 0; i < raw.length; i += 1) { const char = raw[i]; if (char === undefined) { break; } + + if (start < 0) { + if (char === "\n") { + atLineStart = true; + continue; + } + if (atLineStart && (char === " " || char === "\t" || char === "\r")) { + continue; + } + // QMD emits result arrays on their own line; log fields can contain arrays too. + if (!atLineStart || char !== "[") { + atLineStart = false; + continue; + } + start = i; + depth = 1; + atLineStart = false; + continue; + } + if (inString) { if (escaped) { escaped = false; - continue; - } - if (char === "\\") { + } else if (char === "\\") { escaped = true; } else if (char === '"') { inString = false; @@ -159,12 +178,17 @@ function extractFirstJsonArray(raw: string): string | null { } if (char === "[") { depth += 1; - } else if (char === "]") { - depth -= 1; - if (depth === 0) { - return raw.slice(start, i + 1); - } + continue; } + if (char !== "]" || --depth !== 0) { + continue; + } + + const candidate = raw.slice(start, i + 1); + if (parseQmdQueryResultArray(candidate) !== null) { + return candidate; + } + start = -1; } return null; }