# Coexistência — Fase 4b (histórico + contatos): análise e decisão

**Decisão (07/08/2026, do dono): NÃO importar histórico nem agenda.**
Documento existe para fechar a questão — e para que ninguém a reabra por
intuição daqui a seis meses sem saber o que já foi apurado.

---

## 1. A pergunta que abriu a análise

> "isso tem de ser trazido, não pode ficar na nuvem da Meta e consumirmos?"

**Tem de ser trazido. Não existe leitura sob demanda.**

O Cloud API **não tem endpoint para consultar mensagens**. O fluxo é o inverso
do intuitivo: nós chamamos **uma vez**

```
POST /{PHONE_NUMBER_ID}/smb_app_data   { "messaging_product":"whatsapp", "sync_type":"history" }
```

e a Meta **despeja** o histórico inteiro em cima do nosso webhook. Da doc
oficial ("Onboard WhatsApp Business app users"):

- *"a single webhook could potentially describe thousands of messages, so
  capture its contents first, then process the contents asynchronously"*;
- *"You can only perform this step once. If you need to perform it again, the
  customer must first offboard, then complete the Embedded Signup flow again."*

Ou seja: **tiro único**. Se falharmos em capturar, o dado só volta se o cliente
desconectar e refizer o Embedded Signup inteiro.

O mesmo vale para a agenda, por chamada separada
(`sync_type: "smb_app_state_sync"` → webhooks com `full_name`, `first_name`,
`phone_number` e `action`; mudanças futuras na agenda continuam chegando).

## 2. O meio-termo pedido já é o estado atual

> "talvez seja melhor não importar os dados, apenas atualizar o que vem a
> partir do momento da conexão"

É exatamente o que está no ar. **O histórico só chega se pedirmos, e nunca
pedimos.** A Fase 4a (viva desde 05/08) captura só o que acontece da conexão em
diante, via `smb_message_echoes`. Não há nada a construir para ter esse
comportamento — ele é o padrão.

## 3. Como o histórico chegaria (se fosse pedido)

- **Fases**: 0 = dia 0→1, 1 = dia 1→90, 2 = dia 90→180.
- **Pedaços fora de ordem**: `chunk_order` para remontar, `progress` (100 =
  completo) para saber que acabou.
- **Grupos não entram** (limitação da Meta).
- **Mídia antiga não vem**: mensagem com arquivo chega como
  `media_placeholder`, sem conteúdo. Os IDs de mídia só são enviados para
  mensagens dos **últimos 14 dias**. Um "histórico de 180 dias" seria, na
  prática, **texto com buracos**.
- Se o dono desligou o compartilhamento no app, vem um único webhook com o
  erro `2593109`.

## 4. Os cinco bloqueios do nosso lado

Levantados no código, não estimados:

1. **Limite de corpo** — `express.json({ limit: "10mb" })`
   (`server/index.ts:154`). Um webhook com milhares de mensagens estoura e
   volta **413 antes da validação do HMAC**. Combinado com o tiro único: o
   histórico se perderia **para sempre, em silêncio**.
2. **Fila não fatia** — `sliceWebhookIntoQueueRows`
   (`server/metaCloudRoutes.ts:64`) grava **1 linha por `change`**, com o
   `value` inteiro em `jsonb`. Um history vira **uma linha gigante**.
3. **Drain sequencial** — `CLAIM_BATCH = 50` a cada 5 s
   (`metaWebhookWorker.ts:25`, `index.ts:966`), laço `for` com `await`,
   processo único. Um item de history bloquearia os outros 49 e o event loop;
   passando de `STALE_PROCESSING_MINUTES = 10` o item volta para `pending` e é
   **reprocessado até 5×**.
4. **Sem insert em lote** — `appendAiConversationMessage`
   (`storage.ts:7297`) é **1 INSERT por mensagem**, mais 1 SELECT extra por
   mensagem quando há assinante SSE. Não existe caminho bulk para mensagens.
5. **Consultas da Central não aguentam** — o `ORDER BY greatest(...)` do inbox
   (`storage.ts:7553`) não é indexável por construção; `countCentralInbox` faz
   4 `count(*)` sem limite; o LATERAL do SLA filtra `role = 'AGENT'` sem índice
   de apoio — numa conversa com dezenas de milhares de mensagens sem resposta
   humana, varre tudo.

## 5. O que fizemos em vez disso

**Capturar o nome de perfil que já chega de graça.** O webhook `messages`
sempre traz `contacts[{ wa_id, profile: { name } }]` — e nós descartávamos.
Agora ele preenche a ficha do contato: nome de verdade na Central em vez de
número cru, **sem importar nada**, sem expor gente que nunca falou com a
empresa, sem volume.

Regra: **nome de perfil nunca sobrescreve nome curado** (importação, CRM,
edição do cliente). Só preenche quando o contato ainda não tem nome.

Por que isso é melhor que importar a agenda: a agenda do celular do dono é
cheia de gente que **nunca contatou a empresa** — importá-la para uma
plataforma de marketing é exposição de LGPD sem contrapartida. E, no nosso
código, todo contato novo nasce no segmento `nunca_engajou`
(`schema.ts` default), que é **acionável por rotinas** — 10 mil contatos
importados poderiam virar público de campanha.

## 6. Se um dia for reaberto

Pré-requisitos, na ordem: subir o limite de corpo com folga; **fatiar o
payload já na entrada** (uma linha de fila por thread, não por change); fila e
worker **separados** do drain principal, para não competir com a MO ao vivo;
insert em lote de mensagens; e reescrever as três consultas do §5. Só então
faz sentido chamar a API de sync — lembrando que é tiro único e que a mídia
antiga não vem.

## 7. "E se o cliente conectar um número cheio de conversas?" — a IA responde algo antigo?

**Não. Quatro camadas independentes:**

0. **"Genuinamente nova" é garantido por construção**: o webhook `messages` só
   entrega o que chega DEPOIS do subscribe da WABA. Mensagem anterior à
   conexão não tem caminho técnico para aparecer — só viria pelo sync de
   histórico, que não pedimos.

1. **Nada é importado.** O histórico só vem se NÓS pedirmos (`smb_app_data`),
   e essa chamada não existe no código. O `runConnectionSetup`
   (`metaCloudRoutes.ts`) só faz subscribe + descoberta do número + sync de
   templates. No painel do app, o campo `history` está sem assinatura; por
   WABA declaramos só `["messages","smb_message_echoes"]` (`metaGraph.ts:194`).
2. **Webhook `history` que chegue mesmo assim morre no worker.** Cai no
   `default` do switch (`metaWebhookWorker.ts`): loga "field não tratado",
   marca `done`, nunca encosta no aiAgent. Travado por teste
   (`tests/metaWebhookWorker.test.ts`, "field history — nunca vira resposta").
3. **MO velha que entre pela trilha normal é barrada pelo gate 4.9**
   (`aiAgent.ts`, `inbound_too_old`): mensagem com mais de **24h** de idade
   real é REGISTRADA no Chat (o operador vê; opt-out roda — registro é
   incondicional, regra de 24/07/2026) mas a IA fica calada e nenhum
   transporte é chamado. Travado por teste (`tests/waOnsmsReceptivo.test.ts`,
   incluindo o caso "'sair' antigo ainda entra na black-list").

4. **QUARENTENA de conexão nova (gate 4.95, decisão do dono 08/08/2026)**:
   número recém-conectado fica **mudo por 60 min** (env
   `WA_CLOUD_AI_QUARANTINE_MINUTES`; 0 desliga) — nada automático sai, nem IA
   nem Duas Etapas/handoff; a MO é registrada e o opt-out roda. Cobre o caso
   que as outras camadas não cobrem: cliente com IA **já ligada** conectando
   um número ADICIONAL (coexistência com conversas quentes no celular) — o
   takeover por echo só blinda a conversa depois que o dono responde pelo app;
   a quarentena é o amortecedor dessa primeira hora. Relógio = `createdAt` da
   conexão: retry-setup não reseta; reconectar do zero reseta.

E os **echoes** (mensagens que o dono envia pelo app) **nunca** acionam a IA —
viram role AGENT + takeover permanente (decisão da 4a).

Premissas anotadas (sem ação): MO sem `timestamp` da Meta assume `new Date()`
e passa no gate 4.9 — teórico, a Meta sempre manda timestamp; o teto de 24h é
fixo, coerente com a janela de serviço da própria Meta. Observação não
relacionada a importação: não há rate-limit de LLM por conta para rajada de
contatos DISTINTOS (o debounce agrupa por conversa; os tetos são por contato e
por mês).

## 8. Nota para auditoria

A **Política de Privacidade v3** (publicada 06/08) descreve o sync de 180 dias
e dos contatos, porque veio do texto validado do painel. Como **não exercemos**
essa permissão, o texto descreve algo que a plataforma pode fazer mas não faz.
Não é falso — é permissivo. Quem auditar deve saber que, na prática, **nenhum
histórico e nenhuma agenda são importados**; o único dado de coexistência que
entra é a mensagem que o dono envia pelo app **depois** de conectar
(`smb_message_echoes`), mais o nome de perfil de quem escreve para a empresa.
