iachat/docs/superpowers/specs/2026-04-18-captain-semantic-memory-design.md

502 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Spec — Memória Semântica Episódica do Cliente (Captain)
**Data:** 2026-04-18
**Branch:** `feat/captain-semantic-memory`
**Épico:** A (memória). Épico B (evolução do orchestrator) será spec separado após este entrar em produção.
---
## 1. Contexto e motivação
Hoje o Captain (agente de WhatsApp da fazer.ai para o Grupo 1001 Noites) tem 3 camadas de memória funcionais:
| Camada | Onde | Natureza |
|---|---|---|
| Identity | `contact.custom_attributes` (cpf, total_reservas, ultima_suite…) | Key-value, determinístico |
| Working memory | últimas N mensagens da conversa (bruto) | Literal, volátil |
| Knowledge | `captain_assistant_responses.embedding` (FAQ) | Semântica (guardrail) |
**Falta a Camada 3 — memória episódica do cliente:** fatos acumulados ao longo do relacionamento (preferências, datas comemorativas, reclamações, padrões). Sem ela, clientes recorrentes são tratados como novos; experiência é genérica; o valor de "grupo que conhece o hóspede" não existe.
**Objetivo:** adicionar Camada 3 ao Captain, sem alterar as outras 3, com memória semântica real (embeddings + cosine + pgvector), fatos extraídos automaticamente ao fim de cada conversa, injetados no contexto do LLM via top-K retrieval.
**Fora de escopo:** refatorar as 3 camadas existentes; trocar gem `agents` por LangChain/LangGraph; outbound proativo (campanhas) — virarão features derivadas após este épico estável.
## 2. Problema concreto que resolvemos
- Cliente volta em 6 meses → agente começa do zero, pede dados de novo.
- Cliente mencionou preferência por Stilo com hidro 5x → agente nunca oferece proativamente.
- Cliente reclamou de ar-condicionado → na próxima reserva, agente não sabe evitar a mesma suíte.
- Cliente passa em Prime Ceilândia, depois Prime Águas Lindas → agente da 2ª unidade não sabe quem é.
- Grupo não consegue gerar relatórios de *"top reclamações por unidade nos últimos 90 dias"* porque esse dado não está estruturado.
## 3. Decisões tomadas no brainstorming
| # | Decisão | Valor |
|---|---|---|
| 1 | **Quando extrair** | Ao resolver conversa OU após silêncio > 30min |
| 2 | **Validação** | 100% automático com salvaguardas (evidence obrigatória, confidence ≥ 0.5). Alternativas B/C/D documentadas como plano de contingência |
| 3 | **Scope** | Global no recall (experiência unificada no grupo), atribuição por unidade no registro (`source_unit_id` pra relatórios) |
| 4 | **Taxonomia** | 9 tipos fixos iniciais (ver §5) |
| 5 | **Limites** | Até 5 fatos por conversa, até 50 ativos por contato (LRU ao atingir) |
| 6 | **Envelhecimento** | TTL por tipo, soft-delete após expirar, redução de peso no recall antes de deletar |
| 7 | **Conflitos** | Supersedência automática via checker de contradição semântica + lexical |
| 8 | **LGPD** | Soft-delete 30d → hard-delete via cron diário. Botão "esquecer tudo" no painel |
| 9 | **Feature flags** | `extraction_enabled` e `recall_enabled` independentes, por account, default OFF |
| 10 | **Decomposição** | Épico A (este) e Épico B (evolução orchestrator) serão sequenciais, sem esperar 30 dias entre um e outro |
## 4. Arquitetura — 4 camadas integradas em runtime
```
mensagem entra
┌─────────────────┐ key-value, ~1ms, determinístico
│ 1. IDENTITY │ ← contact.custom_attributes (hoje, inalterado)
└─────────────────┘
┌─────────────────┐ literal, ~5ms, volátil
│ 2. WORKING MEM │ ← últimas N mensagens brutas (hoje, inalterado)
└─────────────────┘
┌─────────────────┐ semântica, ~100-200ms, top-5 ← NOVO
│ 3. EPISODIC │ ← captain_contact_memories por cosine
└─────────────────┘
┌─────────────────┐ semântica (guardrail pós-geração)
│ 4. KNOWLEDGE │ ← captain_assistant_responses (hoje, inalterado)
└─────────────────┘
LLM responde
```
Camadas 1 e 2 rodam antes do LLM; Camada 3 também (novo); Camada 4 roda depois como guardrail. Só a Camada 3 é adicionada — nenhuma outra é modificada.
## 5. Taxonomia dos fatos — 9 tipos iniciais
| `memory_type` | Exemplo | Scope default | TTL | Ao expirar |
|---|---|---|---|---|
| `preferencia` | "Prefere Stilo com hidromassagem" | global | 365d | Reduz peso no recall (×0.7), não deleta |
| `data_comemorativa` | "Aniversário casamento 14/02" | global | Nunca | — |
| `vinculo_social` | "Vem com esposa Mariana" | global | 730d | Deleta |
| `padrao_comportamental` | "Chega entre 21h-22h" | unit | 365d | Reduz peso |
| `reclamacao` | "Ar-condicionado da 204 barulhento em mar/26" | unit | 180d | Deleta |
| `feedback_positivo` | "Elogiou a Dona Cida do café" | unit | 365d | Deleta |
| `restricao` | "Alérgico a amendoim" | global | Nunca | — |
| `vinculo_comercial` | "Funcionário Caixa, desconto corp" | global | 365d | Reduz peso |
| `contexto_pessoal` | "Trabalha viajando, hospedagem rápida" | global | 365d | Reduz peso |
Tipos novos podem ser adicionados depois sem migration (campo string no schema).
## 6. Modelo de dados
### 6.1 Tabela `captain_contact_memories`
```sql
CREATE TABLE captain_contact_memories (
id BIGSERIAL PRIMARY KEY,
account_id BIGINT NOT NULL REFERENCES accounts(id),
contact_id BIGINT NOT NULL REFERENCES contacts(id),
memory_type VARCHAR NOT NULL,
content TEXT NOT NULL,
evidence TEXT NOT NULL,
confidence FLOAT NOT NULL,
scope VARCHAR NOT NULL DEFAULT 'global',
embedding vector(1536),
source_conversation_id BIGINT,
source_unit_id BIGINT,
source_inbox_id BIGINT,
expires_at TIMESTAMP,
last_verified_at TIMESTAMP NOT NULL,
superseded_at TIMESTAMP,
superseded_by_id BIGINT,
deleted_at TIMESTAMP,
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
```
**Observação — FKs secundárias:** `source_conversation_id`, `source_unit_id`, `source_inbox_id` e `superseded_by_id` são `BIGINT` simples sem `FOREIGN KEY`. Manter a referência mesmo após o registro de origem ser hard-deletado é essencial pra auditoria LGPD (§10) e pra relatórios históricos por unidade (§3 decisão 3). App-layer precisa tratar referências órfãs graciosamente.
### 6.2 Índices
```sql
CREATE INDEX idx_ccm_recall ON captain_contact_memories (account_id, contact_id)
WHERE deleted_at IS NULL AND superseded_at IS NULL;
CREATE INDEX idx_ccm_embedding ON captain_contact_memories
USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
CREATE INDEX idx_ccm_analytics ON captain_contact_memories
(source_unit_id, memory_type, created_at);
CREATE INDEX idx_ccm_hard_delete ON captain_contact_memories (deleted_at)
WHERE deleted_at IS NOT NULL;
CREATE INDEX idx_ccm_superseded ON captain_contact_memories (superseded_by_id)
WHERE superseded_at IS NOT NULL;
```
### 6.3 Validações no model
- `memory_type` ∈ enum dos 9 tipos.
- `evidence` presence (garantia anti-hallucination — fato sem trecho literal = rejeitado no extractor).
- `confidence` entre 0 e 1.
- `content` non-empty, max 1000 chars (fatos devem ser concisos).
## 7. Componentes (arquivos novos)
### 7.1 Model
**`enterprise/app/models/captain/contact_memory.rb`**
- `has_neighbors :embedding` (gem `neighbor`)
- Enum `memory_type` com os 9 valores
- Scopes:
- `.active``WHERE deleted_at IS NULL AND superseded_at IS NULL AND (expires_at IS NULL OR expires_at > NOW())`
- `.for_contact(id)`, `.by_type(t)`, `.scope_compatible(unit_id)`
- Instance methods:
- `soft_delete!` — seta `deleted_at = now`
- `supersede_by!(other)` — seta `superseded_at`, `superseded_by_id`
- `recall_weight` — retorna 1.0 ou 0.7 (reduz peso se expired)
- Callbacks:
- `after_commit on: [:create, :update] if saved_change_to_content?` → enfileira `UpdateEmbeddingJob`
- `after_create_commit` → enfileira `ContradictionCheckerJob`
### 7.2 Services
**`enterprise/app/services/captain/contact_memories/extraction_service.rb`**
- `initialize(conversation:)`
- `#call` → retorna array de hashes dos fatos válidos. NÃO persiste (separação).
- Monta prompt incluindo: histórico completo da conversa, taxonomia dos 9 tipos, exemplos de evidence bem-feito, JSON schema esperado.
- LLM: `gpt-4o-mini`, JSON mode, temperature 0.
- Valida cada fato retornado: `type` válido, `evidence` non-empty, `confidence` ≥ 0.5, `content` dentro do limite.
- Descarta silenciosamente os inválidos. Retorna no máximo 5.
**`enterprise/app/services/captain/contact_memories/recall_service.rb`**
- `initialize(contact:, query_text:, unit_id: nil)`
- `#call` → retorna array ordenado de top-K memories (default K=5).
- Gera embedding da query.
- `Captain::ContactMemory.active.for_contact(contact.id).scope_compatible(unit_id).nearest_neighbors(:embedding, embedding, distance: 'cosine').limit(K)`.
- Aplica `recall_weight` ao cosine score final.
- Timeout de 500ms via `Timeout::timeout`. Em caso de erro ou timeout, retorna `[]`.
**`enterprise/app/services/captain/contact_memories/contradiction_checker_service.rb`**
- `initialize(memory:)`
- `#call` → marca fatos anteriores como `superseded_by` se detectar contradição.
- Busca candidatos: `active.for_contact.by_type(memory.memory_type).where.not(id: memory.id).where('embedding <=> ? < 0.6', memory.embedding)`.
- Para cada candidato (max 3), chama LLM barato (gpt-4o-mini, 1 pergunta binária): *"Esses 2 fatos se contradizem? sim/não"*.
- Se "sim": `candidate.supersede_by!(memory)`.
**`enterprise/app/services/captain/contact_memories/prompt_injection_service.rb`**
- `initialize(memories:)`
- `#call` → retorna string XML formatada pra injetar no system prompt.
- Formato:
```xml
<memoria_cliente>
<preferencia confidence="0.92">Prefere Stilo com hidromassagem</preferencia>
<data_comemorativa>Aniversário casamento 14/02</data_comemorativa>
</memoria_cliente>
```
- Se `memories.empty?` → retorna string vazia.
### 7.3 Jobs
**`enterprise/app/jobs/captain/contact_memories/extract_from_conversation_job.rb`**
- Argumentos: `conversation_id`.
- Passos:
1. Checa feature flag `captain_contact_memory_extraction_enabled` na account. Se OFF, early return.
2. Carrega conversation.
3. Chama `ExtractionService`.
4. Persiste os fatos válidos com `source_conversation_id`, `source_unit_id` (vem de `conversation.captain_unit_id` ou inferido via inbox), `source_inbox_id`.
5. Cada fato criado aciona `UpdateEmbeddingJob` via `after_commit`.
- Idempotência: se já existe `ContactMemory` com o mesmo `source_conversation_id`, skip (não re-extrai).
**`enterprise/app/jobs/captain/contact_memories/update_embedding_job.rb`**
- Argumento: `memory_id`.
- Gera embedding via `RubyLLM.embed(content, model: 'text-embedding-3-small')`.
- Salva `embedding`.
- Retry 3x, backoff exponencial. Se todas falharem, deixa `embedding = NULL` (fato fica invisível no recall).
**`enterprise/app/jobs/captain/contact_memories/contradiction_checker_job.rb`**
- Argumento: `memory_id`.
- Chama `ContradictionCheckerService`.
- Roda APÓS `update_embedding_job` concluir (encadeado).
**`enterprise/app/jobs/captain/contact_memories/silence_detector_job.rb`**
- Cron a cada 10 min (Sidekiq-Cron).
- Busca conversas ativas cuja última mensagem foi há mais de 30 minutos E que ainda não têm `ContactMemory` com `source_conversation_id` igual.
- Enfileira `ExtractFromConversationJob` pra cada uma.
**`enterprise/app/jobs/captain/contact_memories/aging_job.rb`**
- Cron semanal (domingo madrugada).
- Itera tipos com TTL:
- Para tipos com "Deleta": `WHERE expires_at < NOW() AND deleted_at IS NULL` → soft-delete.
- Para tipos com "Reduz peso": marca `expires_at` no futuro (recall já aplica ×0.7 baseado em `expires_at < NOW()`).
- Aplica limite macio de 50 fatos ativos por contato: LRU por `last_verified_at` → soft-delete excedente.
**`enterprise/app/jobs/captain/contact_memories/hard_delete_expired_job.rb`**
- Cron diário.
- `WHERE deleted_at < 30.days.ago``DELETE`.
- Loga count por account.
### 7.4 Integração no `AgentRunnerService` (arquivo existente — alteração)
**`enterprise/app/services/captain/assistant/agent_runner_service.rb`**
Alteração mínima, antes do `Agents::Runner.with_agents(...).run(message, ...)`:
```ruby
if account.captain_contact_memory_recall_enabled?
memories = Captain::ContactMemories::RecallService.new(
contact: @conversation.contact,
query_text: message,
unit_id: @conversation.captain_unit_id
).call
memory_block = Captain::ContactMemories::PromptInjectionService.new(
memories: memories
).call
system_prompt_with_memory = [system_prompt, memory_block].compact_blank.join("\n\n")
else
system_prompt_with_memory = system_prompt
end
```
System prompt do orchestrator (Jasmine) e de cada scenario (Daniela etc) passam a receber `system_prompt_with_memory`.
Instrução adicionada ao system prompt base: *"Se houver bloco <memoria_cliente>, use apenas se for natural na conversa. Nunca mencione 'memória', 'registro' ou 'lembrei'. Aja como quem já conhece o cliente."*
### 7.5 Controllers e UI
**`enterprise/app/controllers/api/v1/accounts/contacts/memories_controller.rb`**
```
GET /api/v1/accounts/:aid/contacts/:cid/memories # list
PATCH /api/v1/accounts/:aid/contacts/:cid/memories/:id # edit content
DELETE /api/v1/accounts/:aid/contacts/:cid/memories/:id # soft delete
DELETE /api/v1/accounts/:aid/contacts/:cid/memories # forget all (LGPD)
```
Autorização via Pundit: `ContactMemoryPolicy` — só agentes da account, com permissão de edição de contato.
**UI:** nova tab "Memória" no detalhe do contato. Componente Vue:
`app/javascript/dashboard/routes/dashboard/contact/components/ContactMemories.vue`
- Tabs: "Ativas" | "Histórico" (supersedidas/expiradas)
- Lista: type badge, content, confidence bar, idade, origem (link pra conversa)
- Ações por fato: "editar" (inline), "esquecer"
- Botão master: "Esquecer tudo deste contato" (modal de confirmação com texto livre)
**Configurações do Captain** (página existente): nova seção "Memória do Cliente" com 2 toggles:
- "Aprender com conversas (extraction)"
- "Usar memória nas respostas (recall)"
Cada toggle mostra: status, data da última mudança, quem mudou, custo acumulado do mês (se extraction).
### 7.6 Migrations
- `db/migrate/<ts>_create_captain_contact_memories.rb` — tabela + índices.
- Feature flags via `InstallationConfig` seed (padrão já existe no projeto) — NÃO é migration. Adiciona entries `captain_contact_memory_extraction_enabled` (default false) e `captain_contact_memory_recall_enabled` (default false).
## 8. Data flows
### 8.1 Fluxo de aprendizado (assíncrono)
```
conversation.resolved (Wisper event) OU silence_detector_job (cron 10min)
ExtractFromConversationJob (Sidekiq, fila captain_heavy)
├── feature flag extraction_enabled OFF? → early return
ExtractionService
│ prompt estruturado + histórico
│ LLM gpt-4o-mini, JSON mode, temp 0
│ valida evidence + confidence + tipo
│ retorna até 5 fatos válidos
Captain::ContactMemory.create! (N vezes)
▼ after_commit
UpdateEmbeddingJob (Sidekiq, fila captain_embeddings)
│ text-embedding-3-small
│ retry 3x backoff exp
▼ after embedding salvo
ContradictionCheckerJob (Sidekiq, fila captain_heavy)
│ busca candidatos cosine ≥ 0.6 mesmo tipo/contato
│ LLM binário por candidato
│ supersede os contraditórios
```
### 8.2 Fluxo de resposta (síncrono, caminho crítico)
```
message recebida → ResponseBuilderJob (já existe)
AgentRunnerService.call (alterado)
├── feature flag recall_enabled OFF? → pula recall
RecallService.call
│ embedding da query
│ nearest_neighbors top-5 com filtro scope
│ timeout 500ms
PromptInjectionService.call
│ monta bloco XML
system_prompt += memory_block
Agents::Runner.run (fluxo existente, inalterado)
```
## 9. Error handling
| Falha | Comportamento | Impacto no cliente |
|---|---|---|
| LLM extraction falha (timeout/rate limit) | Retry Sidekiq padrão (3x backoff), depois desiste, loga error | Zero — aprende da próxima conversa |
| LLM retorna JSON inválido | Parse tenta repair (regex simples), se falhar descarta conversa, loga warning | Zero |
| Embedding API falha | Retry 3x, depois deixa `embedding = NULL` | Fato existe mas invisível no recall até próximo update |
| Recall service excede 500ms | Retorna `[]`, loga warning com p99 | Agente responde sem memória dessa vez |
| Recall service lança erro | `rescue StandardError => e; log; return []` | Zero — degradação graciosa |
| Contradição ambígua (>1 candidato alto) | Mantém novo, marca antigos como superseded | Zero |
| Feature flag OFF em runtime | Short-circuit no início | Zero |
| Conversation sem unit_id | `source_unit_id = NULL` — fato fica global | Zero |
| Contact tem 50+ fatos ativos | Aging job aplica LRU na próxima execução semanal | Zero |
**Princípio invariante:** Camada 3 NUNCA bloqueia resposta do agente. Qualquer falha degrada para o comportamento atual (Camadas 1+2+4).
## 10. LGPD e privacidade
- **Soft-delete** por 30 dias permite desfazer exclusão acidental.
- **Hard-delete** automático após 30 dias via cron.
- **Botão "Esquecer tudo"** faz soft-delete em massa de todos os fatos do contato.
- **Transparência:** cliente pode pedir ao agente *"o que vocês sabem sobre mim?"* → tool nova `list_my_memories` (fora do escopo deste épico, mas arquitetura permite).
- **Auditoria:** toda mudança manual (edit, delete) loga `changed_by_user_id` + `changed_at` em `metadata`.
- **Retenção:** fatos sem interação em 365+ dias expiram conforme TTL da tabela.
## 11. Testes
### 11.1 Model specs
- Validações: memory_type enum, evidence presence, confidence range, content length.
- Scopes: active, by_type, scope_compatible.
- Soft-delete e supersede methods.
- has_neighbors busca funcional com fixtures.
### 11.2 Service specs
- **ExtractionService** com 10+ fixtures de conversas reais (sanitizadas) cobrindo: conversa curta, longa, sem fatos, multi-tipo, com alucinação induzida (verificar que descarta).
- **RecallService** com base pre-populada: ordering por cosine correto, scope filter correto, timeout respeitado.
- **ContradictionCheckerService** com pares: idênticos, contraditórios, relacionados mas não contraditórios.
- **PromptInjectionService** formato XML, vazio quando sem fatos.
### 11.3 Job specs
- Idempotência: rodar 2x a mesma `conversation_id` não duplica fatos.
- SilenceDetector detecta corretamente conversas elegíveis e ignora não-elegíveis.
- AgingJob aplica TTL correto por tipo.
- HardDeleteExpired só apaga > 30d.
### 11.4 Integration specs
- `ResponseBuilderJob` com recall ON vs OFF → apenas o ON injeta bloco `<memoria_cliente>` no prompt.
- End-to-end: conversa resolved → fato aprendido → próxima mensagem do mesmo contato usa fato no recall.
- Feature flags: extraction OFF não chama LLM (asserção de mock).
### 11.5 LGPD specs
- `forget_all_memories!` faz soft-delete em todos os fatos do contato.
- Cron hard-delete remove registros > 30d.
## 12. Feature flags e rollout
**Flags (InstallationConfig):**
- `captain_contact_memory_extraction_enabled` — default `false`
- `captain_contact_memory_recall_enabled` — default `false`
**Rollout sugerido:**
1. Deploy com flags OFF — nada muda em produção.
2. Account piloto (Prime Águas Lindas): liga `extraction`. Observa 1 semana: custo real, qualidade dos fatos, erros.
3. Se ok, liga `recall` na mesma piloto. Observa 1 semana: latência P99 agente, reação dos clientes.
4. Expande pras outras 3 marcas em ondas de 48h cada.
5. Se qualquer sinal ruim (custo > previsto, reclamação, latência) → desliga só a flag problemática.
## 13. Observabilidade
**Métricas (Prometheus/OTEL, padrão do projeto):**
- `captain_memory_extraction_count` (counter, por account, por tipo)
- `captain_memory_extraction_cost_brl` (counter, por account)
- `captain_memory_extraction_latency_seconds` (histogram)
- `captain_memory_recall_hit_rate` (gauge — % de mensagens onde recall retornou ≥1 fato)
- `captain_memory_recall_latency_seconds` (histogram, p50/p95/p99)
- `captain_memory_active_facts_count` (gauge, por account)
- `captain_memory_supersedence_count` (counter, por tipo)
**Logs estruturados:**
- Cada extração: conversation_id, fatos extraídos (tipo + confidence), descartados (com razão).
- Cada recall: contact_id, query length, top-5 hits com score, tempo total.
- Cada supersedência: old_id, new_id, reason.
**Dashboard admin:** uma view no Chatwoot admin com custo mensal por account, fatos por tipo, latência P99, taxa de supersedência.
## 14. Custos estimados
Baseado em ~3.000 conversas resolvidas/mês no grupo todo:
| Item | Volume | Custo unit | Total/mês |
|---|---|---|---|
| Extração LLM (gpt-4o-mini) | ~3.000 | R$ 0,015 | **R$ 45,00** |
| Embedding (text-embedding-3-small) | ~15.000 | R$ 0,00005 | **R$ 0,75** |
| Recall embedding (1/msg) | ~20.000 | R$ 0,00005 | **R$ 1,00** |
| Checagem contradição | ~200 | R$ 0,001 | **R$ 0,20** |
| Storage pgvector | 150k fatos em 1 ano | — | R$ 0 |
| **Total estimado** | | | **~R$ 47/mês** |
Feature flag `extraction_enabled` OFF zera 95% do custo. `recall_enabled` OFF zera ~4%.
## 15. Roadmap de features derivadas (documentado, fora deste épico)
**Prioridade alta — atacar após este épico estável ≥ 30 dias em produção:**
- **Outbound proativo por `data_comemorativa`** — cron diário varre fatos com data nos próximos 7 dias, dispara template de proposta personalizada cruzando `preferencia`. Vira canal de vendas passivo.
- **Relatórios por unidade** — página admin com filtros de `source_unit_id` + `memory_type`. Top reclamações, top elogios, perfil do cliente por unidade.
- **Tool `list_my_memories`** no agente pra atender direito de transparência LGPD via WhatsApp.
**Prioridade média — avaliar a partir de dados do épico A:**
- **Épico B — Evolução do orchestrator:** LangGraph via microserviço Python com checkpoints persistidos, retomada exata de conversas longas, paralelismo de tools. Será spec separado. Pode ou não acontecer — depende se a memória já resolve a dor percebida.
**Prioridade baixa — observar antes:**
- Reclassificação manual de fatos (operador reclassifica tipo).
- Import de fatos de outras fontes (CRM, reservas Plug-Play).
- Multi-idioma (hoje assume pt-BR).
## 16. Riscos e mitigações
| Risco | Probabilidade | Impacto | Mitigação |
|---|---|---|---|
| LLM alucinar fatos (inventar coisas) | Média | Alto | Evidence obrigatória + confidence ≥ 0.5 + alternativas B/C/D prontas pra ativar |
| Custo explodir | Baixa | Médio | Flag extraction_enabled desliga 95% do custo |
| Latência do recall piorar resposta | Baixa | Médio | Timeout 500ms com fallback vazio |
| Fatos obsoletos/conflitantes poluírem | Média | Médio | Supersedência automática + aging + limite 50 |
| Vazamento de info entre unidades | Baixa | Alto (LGPD/reputação) | Scope field per-fato pra isolar quando sensível |
| Operador confuso pela nova UI | Baixa | Baixo | Docs + toggle começa OFF, rollout gradual |
| Regressão nas Camadas 1/2/4 | Muito baixa | Alto | Princípio: Camada 3 nunca bloqueia. Todas as alterações são aditivas. |
## 17. Próximos passos
1. **Revisão do spec** (user) — ajustes inline se necessário.
2. **Invocar `writing-plans`** — gera plano executável multi-step com tarefas, ordem, owners, critérios de aceitação.
3. **Execução** — via `executing-plans` ou subagent-driven-development, conforme preferência.
4. **Deploy com flags OFF****piloto gradual****rollout completo**.
5. **Observar 30 dias** em produção com memória ativa em todas as contas.
6. **Abrir Épico B** com dados reais como input do brainstorming.