348 lines
12 KiB
Markdown
348 lines
12 KiB
Markdown
# Chat Transcript Leakage + Thinking-Only Fix Implementation Plan
|
|
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
|
|
**Goal:** Eliminar o vazamento de transcript/prompt (`# System: ...`), impedir que respostas corrompidas voltem ao contexto e garantir que o chat exiba a resposta útil além do thinking.
|
|
|
|
**Architecture:** A correção será feita em três camadas complementares: (1) parser/sanitização no frontend para detectar transcript leakage em formatos markdown e role-prefix tradicionais; (2) contenção na persistência e reidratação do histórico para impedir reinfecção do contexto; (3) endurecimento do backend para detectar stops por sufixo acumulado, em vez de comparação token-a-token. O rollout deve ser guiado por testes primeiro, com mudanças pequenas e verificadas por camada.
|
|
|
|
**Tech Stack:** React + TypeScript + Vitest no frontend; Rust no backend (`inference-server`, `inference-core`); SSE streaming; SQLite/local DB.
|
|
|
|
---
|
|
|
|
## Estratégia de implementação
|
|
|
|
### Abordagem A — Parser defensivo no frontend
|
|
Usar uma função única de sanitização que reconheça:
|
|
- `System:` / `User:` / `Assistant:` / `Developer:`
|
|
- `# System:` / `## System:` etc.
|
|
- blocos fenced que comecem com transcript vazado
|
|
- prefixos de prompt conhecido (`You are a precise coding assistant running locally...`)
|
|
|
|
Essa abordagem protege a UI mesmo quando o backend ainda falhar.
|
|
|
|
### Abordagem B — Quarentena do histórico persistido
|
|
Antes de salvar ou reenviar mensagens do assistente:
|
|
- limpar transcript leakage
|
|
- marcar como corrompida quando só restar prompt echo / transcript
|
|
- não reenviar mensagens contaminadas para o modelo
|
|
|
|
Essa abordagem interrompe o ciclo de reinjeção.
|
|
|
|
### Abordagem C — Stop detection real no backend
|
|
Trocar a verificação de stop sequence baseada em token isolado por verificação baseada em buffer/sufixo acumulado da saída gerada.
|
|
|
|
Essa abordagem reduz a chance do vazamento nascer.
|
|
|
|
### Ordem recomendada
|
|
1. Frontend parser + testes
|
|
2. Histórico/persistência + testes
|
|
3. Backend stop detection + testes
|
|
4. Validação integrada streaming/non-streaming
|
|
|
|
---
|
|
|
|
## Critérios de aceitação
|
|
|
|
- Respostas contendo `# System:` não aparecem mais como conteúdo visível ao usuário.
|
|
- Respostas contendo apenas `<think>...</think>` seguido de transcript leakage não são persistidas como resposta válida.
|
|
- Mensagens contaminadas previamente salvas não são reenviadas ao modelo em `buildApiMessages()`.
|
|
- O streaming continua mostrando thinking em tempo real, mas a resposta final visível aparece corretamente quando existir.
|
|
- O backend corta transcript leakage por stop sequence mesmo quando ele chega fragmentado em múltiplos tokens.
|
|
- Testes frontend e backend cobrem os formatos reais do bug.
|
|
|
|
---
|
|
|
|
### Task 1: Criar matriz de casos reais do vazamento
|
|
|
|
**Files:**
|
|
- Modify: `tauri-app/src/lib/chat.test.ts`
|
|
- Modify: `tauri-app/src/prompts/response-rules.ts`
|
|
|
|
**Step 1: Escrever testes falhos para transcript leakage em formatos reais**
|
|
|
|
Adicionar casos para:
|
|
- `# System: You are a precise coding assistant...`
|
|
- ````md
|
|
```markdown
|
|
# System: ...
|
|
```
|
|
````
|
|
- `<think>...</think>` seguido de `# System:`
|
|
- transcript tradicional `Assistant:` / `User:`
|
|
- resposta normal com markdown legítimo que **não** deve ser cortada
|
|
|
|
**Step 2: Rodar os testes para confirmar falha**
|
|
|
|
Run: `cd tauri-app && npm test -- src/lib/chat.test.ts`
|
|
Expected: FAIL nos novos casos de `# System:` / fenced leakage.
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add tauri-app/src/lib/chat.test.ts tauri-app/src/prompts/response-rules.ts
|
|
git commit -m "test: cover transcript leakage variants"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: Centralizar a detecção de transcript leakage no frontend
|
|
|
|
**Files:**
|
|
- Modify: `tauri-app/src/prompts/response-rules.ts`
|
|
- Modify: `tauri-app/src/lib/chat.ts`
|
|
- Test: `tauri-app/src/lib/chat.test.ts`
|
|
|
|
**Step 1: Expandir os padrões de transcript leakage**
|
|
|
|
Em `tauri-app/src/prompts/response-rules.ts`:
|
|
- substituir `TRANSCRIPT_LEAKAGE_PATTERN` por versão que aceite prefixos markdown opcionais:
|
|
- `(?:^|\n)\s*(?:#{1,6}\s*)?(?:System|User|Assistant|Human|Developer)\s*:`
|
|
- adicionar padrão explícito para prompt echo atual:
|
|
- `/you are a precise coding assistant running locally/i`
|
|
|
|
**Step 2: Criar sanitização mais forte em `chat.ts`**
|
|
|
|
Em `tauri-app/src/lib/chat.ts`:
|
|
- extrair helper como `stripPromptEchoAndTranscriptLeakage(text: string): string`
|
|
- cortar conteúdo ao encontrar:
|
|
- role-prefix simples
|
|
- role-prefix markdown (`# System:`)
|
|
- prompt echo conhecido no início da seção visível
|
|
- garantir que markdown legítimo com `# Heading` continue funcionando se não for um role marker.
|
|
|
|
**Step 3: Ajustar `isLoopedOrCorruptedResponse()`**
|
|
|
|
Adicionar o novo padrão de corrupção (`precise coding assistant running locally`).
|
|
|
|
**Step 4: Rodar os testes**
|
|
|
|
Run: `cd tauri-app && npm test -- src/lib/chat.test.ts`
|
|
Expected: PASS
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add tauri-app/src/prompts/response-rules.ts tauri-app/src/lib/chat.ts tauri-app/src/lib/chat.test.ts
|
|
git commit -m "fix: sanitize transcript leakage in assistant responses"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: Impedir reinfecção do histórico em `buildApiMessages()`
|
|
|
|
**Files:**
|
|
- Modify: `tauri-app/src/components/ChatView.tsx`
|
|
- Create: `tauri-app/src/components/chat-history-sanitizer.test.ts` (ou incluir no teste existente mais próximo)
|
|
|
|
**Step 1: Escrever teste falho para reidratação contaminada**
|
|
|
|
Cobrir cenário:
|
|
- mensagem do assistant salva com `# System: ...`
|
|
- `buildApiMessages()` não deve reenviar essa parte
|
|
- se a mensagem toda for corrupção, ela deve ser descartada do contexto
|
|
|
|
**Step 2: Extrair helper reutilizável de sanitização do histórico**
|
|
|
|
Em `ChatView.tsx`:
|
|
- substituir regex inline em `buildApiMessages()` por uso do helper central de `chat.ts`
|
|
- regra recomendada:
|
|
- assistant com conteúdo limpo vazio => descartar do contexto
|
|
- assistant com conteúdo útil + vazamento => manter só parte útil
|
|
|
|
**Step 3: Garantir simetria com persistência**
|
|
|
|
Não usar regex diferente no histórico. Tudo deve passar pelo mesmo parser/sanitizer.
|
|
|
|
**Step 4: Rodar testes**
|
|
|
|
Run: `cd tauri-app && npm test -- src/components/chat-history-sanitizer.test.ts src/components/ChatView.test.tsx`
|
|
Expected: PASS
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add tauri-app/src/components/ChatView.tsx tauri-app/src/components/chat-history-sanitizer.test.ts tauri-app/src/components/ChatView.test.tsx
|
|
git commit -m "fix: prevent transcript leakage from reentering chat context"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: Corrigir persistência da resposta final vs thinking-only
|
|
|
|
**Files:**
|
|
- Modify: `tauri-app/src/components/ChatView.tsx`
|
|
- Modify: `tauri-app/src/components/ChatView.test.tsx`
|
|
- Possibly Modify: `tauri-app/src/components/StreamingBubble.tsx`
|
|
|
|
**Step 1: Escrever testes falhos para dois casos críticos**
|
|
|
|
Caso A:
|
|
- stream = `<think>analise</think>Resposta útil`
|
|
- deve salvar `content = "Resposta útil"` e `reasoning_content = "analise"`
|
|
|
|
Caso B:
|
|
- stream = `<think>analise</think># System: ...`
|
|
- não deve salvar resposta visível corrompida
|
|
- opcionalmente deve mostrar placeholder/erro controlado
|
|
|
|
**Step 2: Ajustar regra de persistência**
|
|
|
|
Em `ChatView.tsx`, no trecho final de `send()`:
|
|
- calcular `sanitizedVisibleContent`
|
|
- se `sanitizedVisibleContent` for vazio **e** reasoning existir, tratar como resposta sem conteúdo útil
|
|
- não salvar transcript leak como `content`
|
|
- salvar `reasoning_content` apenas quando existir conteúdo útil ou quando a UX desejada permitir thinking-only explícito
|
|
|
|
**Step 3: Definir comportamento de UX para thinking-only corrompido**
|
|
|
|
Abordagem recomendada:
|
|
- se só sobrar reasoning e o conteúdo visível estiver corrompido/vazio, salvar mensagem de erro controlado:
|
|
- `⚠️ O modelo gerou apenas raciocínio interno e nenhuma resposta utilizável.`
|
|
- não persistir o prompt echo bruto
|
|
|
|
**Step 4: Rodar testes**
|
|
|
|
Run: `cd tauri-app && npm test -- src/components/ChatView.test.tsx`
|
|
Expected: PASS
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add tauri-app/src/components/ChatView.tsx tauri-app/src/components/ChatView.test.tsx tauri-app/src/components/StreamingBubble.tsx
|
|
git commit -m "fix: handle thinking-only and corrupted final responses"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5: Corrigir stop detection no backend
|
|
|
|
**Files:**
|
|
- Modify: `crates/inference-core/src/session.rs`
|
|
- Add test in: `crates/inference-core/src/session.rs` or dedicated module test file
|
|
|
|
**Step 1: Escrever teste falho para stop fragmentado**
|
|
|
|
Criar teste para simular emissão fragmentada de:
|
|
- `\n`
|
|
- `System`
|
|
- `:`
|
|
|
|
O detector deve reconhecer que o sufixo acumulado forma `\nSystem:` mesmo quebrado em múltiplos tokens.
|
|
|
|
**Step 2: Implementar buffer de sufixo acumulado**
|
|
|
|
Em `session.rs`:
|
|
- adicionar campo para acumular os últimos N caracteres emitidos (`recent_output_suffix`)
|
|
- a cada token detokenizado:
|
|
- concatenar ao buffer
|
|
- truncar buffer para tamanho máximo do maior stop + margem pequena
|
|
- verificar `ends_with(stop)` no buffer acumulado
|
|
|
|
**Step 3: Manter comportamento atual de streaming**
|
|
|
|
Não parar por igualdade de token isolado; parar por sufixo acumulado.
|
|
|
|
**Step 4: Rodar testes do crate**
|
|
|
|
Run: `cargo test -p inference-core session -- --nocapture`
|
|
Expected: PASS
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add crates/inference-core/src/session.rs
|
|
git commit -m "fix: detect stop sequences across fragmented tokens"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6: Endurecer stops do template genérico e alinhar com parser
|
|
|
|
**Files:**
|
|
- Modify: `crates/inference-server/src/compat.rs`
|
|
- Test: `crates/inference-server/src/compat.rs`
|
|
|
|
**Step 1: Adicionar variantes markdown aos `extra_stop_sequences`**
|
|
|
|
Em `prepare_prompt()` para `ChatTemplate::Generic`, incluir:
|
|
- `\n# System:`
|
|
- `\n# User:`
|
|
- `\n# Assistant:`
|
|
- `\n## System:` (opcional)
|
|
|
|
**Step 2: Adicionar teste de contrato**
|
|
|
|
Garantir que o template genérico injeta essas variantes.
|
|
|
|
**Step 3: Rodar testes**
|
|
|
|
Run: `cargo test -p inference-server compat -- --nocapture`
|
|
Expected: PASS nos testes do módulo `compat` relevantes.
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
git add crates/inference-server/src/compat.rs
|
|
git commit -m "fix: add markdown transcript stops for generic prompt template"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 7: Validação integrada frontend + backend
|
|
|
|
**Files:**
|
|
- No code required initially
|
|
- Possibly add/update integration tests after manual verification
|
|
|
|
**Step 1: Rodar suíte frontend completa**
|
|
|
|
Run: `cd tauri-app && npm test`
|
|
Expected: PASS
|
|
|
|
**Step 2: Rodar build frontend**
|
|
|
|
Run: `cd tauri-app && npm run build`
|
|
Expected: PASS
|
|
|
|
**Step 3: Rodar testes backend focados**
|
|
|
|
Run:
|
|
```bash
|
|
cargo test -p inference-core -- --nocapture
|
|
cargo test -p inference-server compat -- --nocapture
|
|
```
|
|
Expected: PASS nos testes tocados pela mudança.
|
|
|
|
**Step 4: Verificação manual**
|
|
|
|
Cenários manuais:
|
|
1. resposta normal sem `<think>`
|
|
2. resposta com `<think>` e resposta útil
|
|
3. resposta com `<think>` + `# System:`
|
|
4. mensagem previamente contaminada no DB não deve ser reenviada ao contexto
|
|
|
|
**Step 5: Commit final**
|
|
|
|
```bash
|
|
git add .
|
|
git commit -m "fix: stop transcript leakage from corrupting chat responses"
|
|
```
|
|
|
|
---
|
|
|
|
## Observações de implementação
|
|
|
|
- Não criar regex diferentes em cada arquivo. A sanitização precisa ter **uma fonte de verdade**.
|
|
- O parser do frontend e a limpeza do histórico devem compartilhar helpers.
|
|
- O backend deve reduzir a chance do bug, mas o frontend ainda precisa ser resiliente porque modelos locais podem ignorar formato.
|
|
- Não confiar apenas em `isLoopedOrCorruptedResponse()`; usar também sanitização determinística do conteúdo.
|
|
|
|
## Resultado esperado final
|
|
|
|
Depois dessas tarefas:
|
|
- o usuário não verá mais blocos `# System: ...` nas mensagens;
|
|
- o thinking continuará disponível, mas não substituirá a resposta útil;
|
|
- respostas corrompidas não contaminarão o histórico;
|
|
- o backend interromperá transcript leakage com mais robustez.
|