From 00bb0416d35342df2c2cf895697ab26140c94593 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 28 May 2026 22:07:08 -0300 Subject: [PATCH] fix(memory): harden extractLastUserText + add missing configureCta i18n key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third code-review pass on plan 21 found two follow-up issues from the previous round. 1. extractLastUserText accepted Responses API items with role===undefined regardless of their type. function_call_output / tool_call_output / reasoning items would slip through and be treated as user query input, leaking the tool's reply or the model's chain of thought into the memory retrieval query. Fix: when role is undefined, skip items whose type is in a denylist of non-user item types (function_call, function_call_output, tool_call, tool_call_output, reasoning, computer_call, computer_call_output, web_search_call, file_search_call). Also reject non-text content parts inside multi-modal arrays (image_url, tool_use, ...) so that image-only or tool-only user messages do not produce a query made of irrelevant fragments. 2. MemoryEngineStatus introduced t("engine.configureCta") in round 2 but the key was never added to en.json / pt-BR.json — even with the new EN fallback merger, the CTA would render the literal key path. Added "Configure" / "Configurar" to both locales. Verified: typecheck:core clean; vitest UI 46/46; cli-memory-commands, memory-settings, and mcp-memory-tools-strategy isolated sanity all green; grep audit of memory.* i18n keys used by the UI confirms zero missing keys in en.json. --- open-sse/handlers/chatCore.ts | 40 ++++++++++++++++++++++++++++------- src/i18n/messages/en.json | 3 ++- src/i18n/messages/pt-BR.json | 3 ++- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index b2d202f2e6..34987b9de6 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -2230,18 +2230,32 @@ export async function handleChatCore({ // tier-2) never fire from the chat hot path — they only fire in the // Playground (retrievePreview, which gets `query` as a positional arg). const lastUserQuery = ((): string => { + // Responses API item types that are NOT user input — never accept + // their text as the retrieval query (e.g. function_call_output is the + // tool's reply, reasoning is the model's chain of thought). + const NON_USER_TYPES = new Set([ + "function_call", + "function_call_output", + "tool_call", + "tool_call_output", + "reasoning", + "computer_call", + "computer_call_output", + "web_search_call", + "file_search_call", + ]); + function pickFrom(arr: unknown[]): string { for (let i = arr.length - 1; i >= 0; i--) { const item = arr[i] as Record | undefined; if (!item) continue; - // Chat API: role==="user"; Responses API: role on input items, or - // type==="input_text"; some clients omit role entirely on the - // first input item — accept those too as a last resort. - if ( - item.role !== undefined && - item.role !== "user" - ) { - continue; + // Chat API: only role==="user" items. Responses API items often + // have type instead of role — skip non-user types like + // function_call_output so the tool's reply doesn't leak into the + // memory query. + if (item.role !== undefined && item.role !== "user") continue; + if (item.role === undefined && typeof item.type === "string") { + if (NON_USER_TYPES.has(item.type)) continue; } const content = item.content ?? item.text; if (typeof content === "string" && content.trim().length > 0) { @@ -2254,6 +2268,16 @@ export async function handleChatCore({ parts.push(p); } else if (p && typeof p === "object") { const pp = p as Record; + // Skip non-text content parts (image_url, tool_use, etc.) + const ptype = typeof pp.type === "string" ? pp.type : ""; + if ( + ptype && + ptype !== "text" && + ptype !== "input_text" && + ptype !== "output_text" + ) { + continue; + } const t = pp.text ?? pp.input_text; if (typeof t === "string") parts.push(t); } diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 5b6b6c2d7b..e835d8b925 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -3044,7 +3044,8 @@ "qdrantDisabled": "Disabled", "qdrantOk": "Healthy ({latencyMs}ms)", "qdrantError": "Connection error", - "needsReindex": "{count} memory(ies) need reindexing" + "needsReindex": "{count} memory(ies) need reindexing", + "configureCta": "Configure" }, "episodic": "Episodic", "export": "Export", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index b0400b230e..b1df0e2f2b 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -3041,7 +3041,8 @@ "qdrantDisabled": "Desabilitado", "qdrantOk": "Saudável ({latencyMs}ms)", "qdrantError": "Erro de conexão", - "needsReindex": "{count} memória(s) precisam de reindexação" + "needsReindex": "{count} memória(s) precisam de reindexação", + "configureCta": "Configurar" }, "episodic": "Episódica", "export": "Exportar",