---
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