From 9da25c3688a649235c24165a865c24c3a0a5f640 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Mon, 30 Mar 2026 17:31:35 -0300 Subject: [PATCH] docs(quick-1): bug internal error segunda mensagem do chat --- .planning/STATE.md | 8 +- .../1-PLAN.md | 282 ++++++++++++++++++ 2 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 .planning/quick/1-investigar-e-corrigir-bug-de-internal-er/1-PLAN.md diff --git a/.planning/STATE.md b/.planning/STATE.md index 5f14ec0..a083805 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -28,7 +28,7 @@ See: `.planning/PROJECT.md` (updated 2026-03-28) Phase: 3 of 4 (Unified Runtime Workspace) Plan: 3 of 3 authored for current phase Status: Phase complete — ready for verification -Last activity: 2026-03-29 +Last activity: 2026-03-30 - Completed quick task 1: Investigar e corrigir bug de internal error na segunda mensagem do chat Progress: [█████░░░░░] 50% @@ -91,6 +91,12 @@ None yet. - None currently; Phase 3 can build on the now-stable lifecycle/apply contract. +### Quick Tasks Completed + +| # | Description | Date | Commit | Directory | +|---|-------------|------|--------|-----------| +| 1 | Investigar e corrigir bug de internal error na segunda mensagem do chat | 2026-03-30 | abb9db0 | [1-investigar-e-corrigir-bug-de-internal-er](./quick/1-investigar-e-corrigir-bug-de-internal-er/) | + ## Session Continuity Last session: 2026-03-29T20:49:43.436Z diff --git a/.planning/quick/1-investigar-e-corrigir-bug-de-internal-er/1-PLAN.md b/.planning/quick/1-investigar-e-corrigir-bug-de-internal-er/1-PLAN.md new file mode 100644 index 0000000..637bec8 --- /dev/null +++ b/.planning/quick/1-investigar-e-corrigir-bug-de-internal-er/1-PLAN.md @@ -0,0 +1,282 @@ +--- +phase: quick +plan: 1 +type: execute +wave: 1 +depends_on: [] +files_modified: + - crates/inference-core/src/session.rs + - crates/inference-server/src/routes.rs + - tauri-app/src/components/ChatView.tsx +autonomous: true +requirements: [] +must_haves: + truths: + - "A segunda mensagem do chat recebe resposta válida do modelo, não 'internal error'" + - "Nenhuma mensagem duplicada com erro é inserida no DB após uma geração bem-sucedida" + - "O backend loga claramente a causa quando llama_decode falha no prefill" + artifacts: + - path: "crates/inference-core/src/session.rs" + provides: "decode_prefill com log de erro detalhado e fallback de kv_clear via llama_kv_cache_clear" + - path: "crates/inference-server/src/routes.rs" + provides: "StreamItem::Error não emite conteúdo 'internal error' acumulável pelo frontend" + - path: "tauri-app/src/components/ChatView.tsx" + provides: "buildApiMessages exclui mensagens de erro do histórico enviado ao backend" + key_links: + - from: "crates/inference-core/src/engine.rs new_session()" + to: "llama_get_memory / memory_clear" + via: "KV cache clear antes de cada nova sessão" + pattern: "memory_clear" + - from: "tauri-app/src/components/ChatView.tsx buildApiMessages" + to: "SELECT role, content FROM messages" + via: "histórico de mensagens enviado ao backend" + pattern: "buildApiMessages" +--- + + +Identificar e corrigir o bug que causa "internal error" na segunda mensagem do chat. + +Purpose: A segunda request ao backend falha com llama_decode retornando não-zero no prefill, +provavelmente porque o KV cache não está sendo limpo corretamente quando llama_get_memory +retorna NULL. O StreamItem::Error no frontend então acumula "internal error" como conteúdo +e o salva no DB como mensagem de assistente válida, criando a entrada duplicada observada nos logs. + +Output: Backend com KV cache clear robusto + log de diagnóstico + frontend filtrando mensagens +de erro do histórico da API. + + + +@C:/Users/Evandro/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/Evandro/.claude/get-shit-done/templates/summary.md + + + +@.planning/STATE.md + + + + + + Task 1: Tornar o KV cache clear robusto e adicionar diagnóstico de falha de prefill + + crates/inference-core/src/engine.rs + crates/inference-core/src/session.rs + + +**Problema identificado:** Em `engine.rs::new_session()` (linha ~302), quando `llama_get_memory` +retorna NULL, o KV cache NÃO é limpo e um warning é logado — mas a execução continua. +Na segunda request, o contexto llama ainda tem tokens da sessão anterior nas posições 0..N, +e o prefill da nova request tenta escrever novamente nessas mesmas posições causando +`llama_decode` retornar não-zero (erro KV cache cheio / inconsistência de posições). + +**Fix em `engine.rs`:** No método `new_session()`, após a tentativa com `get_memory/memory_clear`, +adicionar fallback usando `llama_kv_cache_clear(ctx)` diretamente se a memória for NULL. +A função `llama_kv_cache_clear` existe como símbolo separado em versões mais antigas da +llama.cpp e deve estar disponível via `self.lib`. Verificar se `LlamaLib` expõe +`kv_cache_clear` — se sim, chamar como fallback. Se não existir, usar +`(self.lib.memory_clear)(memory, true)` mas garantir que o código falhe explicitamente +(retorne `Err`) quando memory é NULL em vez de continuar silenciosamente. + +Alterar o bloco atual: +```rust +// ANTES (silently continues when memory is NULL): +let memory = (self.lib.get_memory)(self.ctx.ptr); +if memory.is_null() { + tracing::warn!("llama_get_memory returned NULL — KV cache will not be cleared"); +} else { + (self.lib.memory_clear)(memory, true); + tracing::debug!("engine.kv_cache_cleared"); +} +``` + +Para: +```rust +// DEPOIS: bail quando não consegue limpar o cache, evitando decode com estado corrompido +let memory = (self.lib.get_memory)(self.ctx.ptr); +if memory.is_null() { + tracing::error!("llama_get_memory returned NULL — cannot clear KV cache, refusing to create session to avoid decode corruption"); + anyhow::bail!("KV cache clear failed: llama_get_memory returned NULL"); +} else { + (self.lib.memory_clear)(memory, true); + tracing::info!("engine.kv_cache_cleared"); +} +``` + +**Fix em `session.rs`:** Em `decode_prefill()` (linha ~350), quando `lib.decode` retorna não-zero, +melhorar o bail com contexto claro: +```rust +// ANTES: +bail!( + "llama_decode (prefill) returned {} while decoding prompt chunk {:?}", + ret, + plan.range +); + +// DEPOIS: +bail!( + "llama_decode (prefill) returned {} for chunk {:?} — likely KV cache not cleared or context window ({} tokens) exceeded by prompt", + ret, + plan.range, + tokens.len() +); +``` + +Após as mudanças, verificar compilação: `cargo check -p inference-core -p inference-server` + + + cargo check -p inference-core -p inference-server 2>&1 | tail -5 + + + - `cargo check` passa sem erros + - `new_session()` retorna `Err` quando `llama_get_memory` retorna NULL em vez de continuar + - Mensagem de erro de prefill inclui tamanho do prompt para diagnóstico + + + + + Task 2: Impedir que "internal error" seja salvo no DB e excluir mensagens de erro do histórico da API + + crates/inference-server/src/routes.rs + tauri-app/src/components/ChatView.tsx + + +**Problema 1 — routes.rs:** Quando `StreamItem::Error` é enviado (linha ~318-335), o backend +emite um chunk SSE com `content: Some("internal error")`. O frontend acumula esse token em +`fullContent` normalmente (mesmo path do streaming), então salva no DB como assistente com +o texto "internal error". O `StreamItem::Error` deve sinalizar falha sem emitir conteúdo +que o frontend interpreta como resposta válida. + +Fix: Alterar `StreamItem::Error` para emitir um chunk com `finish_reason: Error` e conteúdo +vazio, ou usar um campo de erro fora da estrutura `delta.content`. A solução mais simples +compatível com o frontend atual: emitir o chunk de erro com `content: None` e usar +`finish_reason: Stop` para que o frontend complete o stream, mas não acumule conteúdo. + +```rust +// ANTES: emite "internal error" como content +StreamItem::Error => { + tracing::error!(streamed_tokens, "stream.error: session failed mid-stream"); + let chunk = ChatCompletionChunk { + // ... + choices: vec![ChunkChoice { + delta: Delta { role: None, content: Some("internal error".to_owned()), tool_calls: None }, + finish_reason: Some(FinishReason::Stop), + }], + usage: None, + }; + yield Ok(Event::default().data(serde_json::to_string(&chunk).unwrap())); + yield Ok(Event::default().data("[DONE]")); + break; +} + +// DEPOIS: emite chunk final sem content para não poluir fullContent +StreamItem::Error => { + tracing::error!(streamed_tokens, "stream.error: session failed — sending empty finish chunk"); + let chunk = ChatCompletionChunk { + id: id.clone(), + object: "chat.completion.chunk", + created, + model: resolved_model.clone(), + choices: vec![ChunkChoice { + index: 0, + delta: Delta { role: None, content: None, tool_calls: None }, + finish_reason: Some(FinishReason::Stop), + }], + usage: None, + }; + yield Ok(Event::default().data(serde_json::to_string(&chunk).unwrap())); + yield Ok(Event::default().data("[DONE]")); + break; +} +``` + +**Problema 2 — ChatView.tsx:** Em `buildApiMessages` (linha 126-132), o SELECT não filtra +mensagens de erro. Se alguma mensagem de erro entrou no DB (antes do fix acima), ela vai +no histórico enviado ao backend, corrompendo o contexto de chat. Adicionar filtro: + +```typescript +// ANTES: +const rows = db.query<{ role: string; content: string }>( + "SELECT role, content FROM messages WHERE chat_id = ? ORDER BY created_at DESC LIMIT ?", + [cid, limit] +).reverse(); + +// DEPOIS: excluir mensagens que são erros do frontend (⚠️ prefix) e vazias +const rows = db.query<{ role: string; content: string }>( + "SELECT role, content FROM messages WHERE chat_id = ? AND content != '' AND content NOT LIKE '⚠️ Error:%' AND content != 'internal error' ORDER BY created_at DESC LIMIT ?", + [cid, limit] +).reverse(); +``` + +**Problema 3 — ChatView.tsx:** No catch do `send()` (linha ~261-268), quando o stream termina +com `fullContent` vazio (após o fix do backend), a lógica atual salva a mensagem de erro do +catch com prefix `⚠️ Error:`. Mas quando o stream completa normalmente mas com `fullContent` +vazio (caso de session fail silencioso), a mensagem assistente salva é vazia. Adicionar +guarda antes do INSERT do assistente: + +```typescript +// Antes do INSERT da mensagem assistente, após a geração completar: +// Só salvar se houver conteúdo real +if (!finalContent && !parsed.reasoning) { + console.warn("[ChatView] send: assistant returned empty content — not saving to DB"); +} else { + db.run(`INSERT INTO messages ...`, [...]); +} +``` + +Localizar a linha do INSERT (linha ~241-254 em ChatView.tsx) e envolver com esse guard. + +Após as mudanças: +- `cargo check -p inference-server` +- `cd tauri-app && npx tsc --noEmit` + + + cd D:/Documents/Github/ai-backend-studio && cargo check -p inference-server 2>&1 | tail -3 && cd tauri-app && npx tsc --noEmit 2>&1 | tail -5 + + + - `cargo check` e `tsc --noEmit` passam sem erros + - Chunk SSE de erro não contém `content: "internal error"` + - `buildApiMessages` filtra mensagens de erro/vazias do histórico + - INSERT do assistente não executa quando `fullContent` está vazio + + + + + + Fix de três camadas para o bug de "internal error" na segunda mensagem: + 1. Backend Rust: KV cache clear agora falha explicitamente (Err) quando llama_get_memory + retorna NULL, com log de erro claro em vez de continuar silenciosamente + 2. Backend Rust: StreamItem::Error emite chunk sem content (não polui fullContent) + 3. Frontend: buildApiMessages filtra mensagens de erro do histórico; INSERT do assistente + tem guard contra conteúdo vazio + + + 1. Inicie o runtime local com um modelo GGUF carregado + 2. Abra o ForgeLM e envie uma primeira mensagem — confirme resposta normal + 3. Envie uma segunda mensagem no mesmo chat — confirme que resposta é válida (não "internal error") + 4. Verifique o log do backend no terminal: + - Deve aparecer "engine.kv_cache_cleared" antes de cada sessão + - Se aparecer "llama_get_memory returned NULL", o runtime vai retornar erro 500 (não "internal error" silencioso) + 5. Abra DevTools > Console no app e confirme que não há INSERT com conteúdo "internal error" nos logs [DB] + 6. Envie 3-4 mensagens em sequência e confirme que todas respondem normalmente + + Digite "aprovado" se as respostas estão corretas, ou descreva o comportamento observado + + + + + +- `cargo check -p inference-core -p inference-server` passa +- `cd tauri-app && npx tsc --noEmit` passa +- Segundo envio de mensagem no chat retorna resposta do modelo (não "internal error") +- Logs do backend mostram "engine.kv_cache_cleared" antes de cada sessão + + + +- Enviar 5 mensagens consecutivas no mesmo chat sem receber "internal error" +- DB não contém nenhuma mensagem assistente com conteúdo "internal error" ou vazio após geração bem-sucedida +- Se o KV cache falhar (llama_get_memory NULL), o erro é visível como HTTP 500, não como mensagem de assistente corrupta + + + +Após conclusão, criar `.planning/quick/1-investigar-e-corrigir-bug-de-internal-er/1-SUMMARY.md` +