docs(quick-1): bug internal error segunda mensagem do chat
This commit is contained in:
1 parent
abb9db0b77
commit
9da25c3688
2 files changed
+289
-1
No files matched your search
+7
-1
@@ -28,7 +28,7 @@ See: `.planning/PROJECT.md` (updated 2026-03-28)
|
|||||||
Phase: 3 of 4 (Unified Runtime Workspace)
|
Phase: 3 of 4 (Unified Runtime Workspace)
|
||||||
Plan: 3 of 3 authored for current phase
|
Plan: 3 of 3 authored for current phase
|
||||||
Status: Phase complete — ready for verification
|
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%
|
Progress: [█████░░░░░] 50%
|
||||||
|
|
||||||
@@ -91,6 +91,12 @@ None yet.
|
|||||||
|
|
||||||
- None currently; Phase 3 can build on the now-stable lifecycle/apply contract.
|
- 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
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-03-29T20:49:43.436Z
|
Last session: 2026-03-29T20:49:43.436Z
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in new issue
Block a user