docs(spec): Captain semantic memory (episodic contact layer)

Spec do Epico A - adiciona Camada 3 (memoria semantica episodica do contato)
ao Captain AI, mantendo as 3 camadas existentes inalteradas.

Decisoes fechadas no brainstorming:
- Extracao ao resolver conversa OU silencio > 30min (100% automatico)
- Validacao: evidence obrigatoria, confidence >= 0.5 (alternativas B/C/D
  documentadas como fallback)
- Scope global no recall, atribuicao por source_unit_id pra relatorios
- 9 tipos iniciais, limite 5 fatos/conversa, 50 ativos/contato
- TTL por tipo + supersedencia automatica por contradicao
- LGPD soft-30d -> hard-delete via cron
- 2 feature flags independentes, default OFF
- Epico B (LangGraph/inteligencia) sera spec separado pos-producao

Custo estimado: ~R$ 47/mes no grupo todo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rodribm10 2026-04-18 21:37:32 -03:00
parent 6a5ba17bfc
commit 719448120a

View File

@ -0,0 +1,499 @@
# 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 REFERENCES conversations(id),
source_unit_id BIGINT REFERENCES captain_units(id),
source_inbox_id BIGINT REFERENCES inboxes(id),
expires_at TIMESTAMP,
last_verified_at TIMESTAMP NOT NULL,
superseded_at TIMESTAMP,
superseded_by_id BIGINT REFERENCES captain_contact_memories(id),
deleted_at TIMESTAMP,
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
```
### 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.