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 |
|
true |
|
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
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-servercd tauri-app && npx tsc --noEmitcd D:/Documents/Github/ai-backend-studio && cargo check -p inference-server 2>&1 | tail -3 && cd tauri-app && npx tsc --noEmit 2>&1 | tail -5cargo checketsc --noEmitpassam sem erros- Chunk SSE de erro não contém
content: "internal error" buildApiMessagesfiltra mensagens de erro/vazias do histórico- INSERT do assistente não executa quando
fullContentestá vazio
<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>