docs(quick-1): bug internal error segunda mensagem do chat

This commit is contained in:
Evandro Fonseca Junior committed 2026-03-30 17:31:35 -03:00
1 parent abb9db0b77
commit 9da25c3688
2 files changed
+289 -1

No files matched your search

@@ -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"
---
<objective>
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.
</objective>
<execution_context>
@C:/Users/Evandro/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/Evandro/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/STATE.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Tornar o KV cache clear robusto e adicionar diagnóstico de falha de prefill</name>
<files>
crates/inference-core/src/engine.rs
crates/inference-core/src/session.rs
</files>
<action>
**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`
</action>
<verify>
<automated>cargo check -p inference-core -p inference-server 2>&1 | tail -5</automated>
</verify>
<done>
- `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
</done>
</task>
<task type="auto">
<name>Task 2: Impedir que "internal error" seja salvo no DB e excluir mensagens de erro do histórico da API</name>
<files>
crates/inference-server/src/routes.rs
tauri-app/src/components/ChatView.tsx
</files>
<action>
**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`
</action>
<verify>
<automated>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</automated>
</verify>
<done>
- `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
</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
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
</what-built>
<how-to-verify>
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
</how-to-verify>
<resume-signal>Digite "aprovado" se as respostas estão corretas, ou descreva o comportamento observado</resume-signal>
</task>
</tasks>
<verification>
- `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
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
Após conclusão, criar `.planning/quick/1-investigar-e-corrigir-bug-de-internal-er/1-SUMMARY.md`
</output>