From 8dc02b6b3576d8f07040ccaf52dc623750a9b5f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timeless=E5=B0=8F=E5=B8=85?= <1018008088@qq.com> Date: Fri, 9 Oct 2026 10:31:06 +0800 Subject: [PATCH] memory: distinguish a failed extraction from an empty one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Probing the extraction path with a real model call showed it returning a zero-candidate outcome — indistinguishable from "the conversation had nothing worth keeping". It was actually an auth failure delivered as a JSON error body rather than a thrown error, which coerceText turned into an empty string, which parseCandidates turned into an empty array. extract() now reports `error` and a `rawText` preview instead of collapsing every failure mode into `{candidates: []}`. An auth error arriving as a return value rather than a throw is the common case, not the exotic one. memory_inspect prints the error and the raw response, so this class of failure now shows up instead of looking like a healthy extractor that rarely fires. Verified: thrown error, error object, empty string, and null each produce a specific message; valid JSON and fenced JSON still parse. --- plugins/memory/extract.ts | 48 +++++++++++++++++++++++++++++++++++---- plugins/memory/index.ts | 13 +++++++++-- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/plugins/memory/extract.ts b/plugins/memory/extract.ts index 83e8b20..4b3f90d 100644 --- a/plugins/memory/extract.ts +++ b/plugins/memory/extract.ts @@ -218,6 +218,15 @@ export function redactSecrets(text: string): string { export interface ExtractionOutcome { candidates: Candidate[] rejected: { candidate: Candidate; reason: string }[] + /** + * Set when the round-trip itself failed. Absent means the model answered. + * Without this, a broken call and a conversation with nothing worth keeping + * both come back as zero candidates — which is how a permanently broken + * extractor can look like a healthy one that simply rarely fires. + */ + error?: string + /** Raw model text, truncated, for diagnosing a surprising empty result. */ + rawText?: string } /** @@ -302,7 +311,13 @@ export interface RunExtractionInput { signal?: AbortSignal } -/** One model round-trip. Returns screened candidates and why things were dropped. */ +/** + * One model round-trip. Returns screened candidates and why things were dropped. + * + * A transport failure is reported in `error` rather than collapsed into an + * empty result: the caller's only other signal would be "no candidates", which + * is also what a correct answer looks like when nothing was worth keeping. + */ export async function extract(input: RunExtractionInput): Promise { const prompt = `${SYSTEM}\n\n---\n\n${input.transcript}` let result: unknown @@ -314,11 +329,34 @@ export async function extract(input: RunExtractionInput): Promise