fix(memory): harden extractLastUserText + add missing configureCta i18n key

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.
This commit is contained in:
diegosouzapw
2026-05-28 22:07:08 -03:00
parent 516a7e5520
commit 00bb0416d3
3 changed files with 36 additions and 10 deletions

View File

@@ -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<string, unknown> | 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<string, unknown>;
// 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);
}

View File

@@ -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",

View File

@@ -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",