Files
ForgeLM/.planning/quick/1-investigar-e-corrigir-bug-de-internal-er/1-PLAN.md
T

12 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
quick 1 execute 1
crates/inference-core/src/session.rs
crates/inference-server/src/routes.rs
tauri-app/src/components/ChatView.tsx
true
truths artifacts key_links
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
path provides
crates/inference-core/src/session.rs decode_prefill com log de erro detalhado e fallback de kv_clear via llama_kv_cache_clear
path provides
crates/inference-server/src/routes.rs StreamItem::Error não emite conteúdo 'internal error' acumulável pelo frontend
path provides
tauri-app/src/components/ChatView.tsx buildApiMessages exclui mensagens de erro do histórico enviado ao backend
from to via pattern
crates/inference-core/src/engine.rs new_session() llama_get_memory / memory_clear KV cache clear antes de cada nova sessão memory_clear
from to via pattern
tauri-app/src/components/ChatView.tsx buildApiMessages SELECT role, content FROM messages histórico de mensagens enviado ao backend 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.

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

@.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:

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

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

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

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

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

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

<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>
Após conclusão, criar `.planning/quick/1-investigar-e-corrigir-bug-de-internal-er/1-SUMMARY.md`