Files
ForgeLM/docs/plans/2026-04-03-chat-transcript-leakage-fix.md

12 KiB

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