# Roadmap — Motor scan 2026-10-11

> Source: `RELATORIO.md` (sha `cca54076…`, 208 KB). Sections 6 (Roadmap priorizado em ondas) and 7 (Perguntas de negócio e decisões do dono).
>
> Generated on 2026-10-11. This is a planning document. Each item links to its origin in the scan and lists what's needed to close it. Code comes later.

## Estado geral (read-only)

| Métrica | Valor |
|---|---|
| Achados mantidos (crítica + alta) | 41 |
| Achados refutados | 11 |
| Perguntas abertas (decisão do dono) | 17 |
| Banco: `max_connections` (lido no `postgresql.conf` de produção) | 100 |
| Pool de conexões por processo | 1000 |
| Steps default por turno | 999 |
| Timeout default por turno (ms) | 0 |
| Onda 1 (dias) — itens | 13 |
| Onda 2 (semanas) — itens | 11 |
| Onda 3 (meses) — itens | 7 |

Notas sobre a tabela:

- O `max_connections` foi lido no `postgresql.conf`; o container do Postgres pode mudar isso na linha de comando, daí a ação 1.11 pedir `SHOW max_connections` em produção antes de qualquer mudança.
- As ondas 1/2/3 listadas no scan (seção 6) somam 13 + 11 + 7 = 31 ações; este ROADMAP desdobra cada uma na forma `1.1` … `3.7` para que o dono marque item por item.
- A seção 7 do scan lista 17 "decisões necessárias" e 9 "perguntas abertas que dependem de dados que o scan não leu". As 9 perguntas abertas precisam de leitura do `.env` de produção, métricas Prometheus, logs e entrevistas com o time antes de virar plano.

---

## Onda 1 — Rápido (dias): estancar custo, segurança e travamentos

> Onda sem dependências cruzadas; 11 das 13 ações são totalmente independentes e podem ser paralelizadas. Exceções: 1.11 (espera confirmação do `SHOW max_connections` em prod) e 1.6 (pré-requisito da 2ª réplica).
>
> Métricas alvo depois da Onda 1: nenhum turno sem teto de steps/tokens, nenhum turno sem timeout padrão, nenhum webhook sem guard SSRF, nenhum snapshot com segredos em claro, abort consistente entre réplicas (mesmo 1), reaper ligado, polling barato, `/info` enxuto.


### 1.1 — Teto de steps por turno no chat (ex.: 50, configurável por setup), teto de tokens por turno e alerta

| Campo | Valor |
|---|---|
| Achados origem | `loop-sdk-maxsteps-999`, `tools-prompt-maxsteps-999` |
| Onda | 1 (rápido, dias) |
| Esforço | P |
| Risco | baixo |
| Depende de | — |
| Quem toca | BE (runtime do turno) + FE (mostrar tetos na UI) |
| Decisões do dono pendentes | nenhuma |
| O que precisa pra fechar | Default novo (sugestão: 50 steps, configurável por setup); teto de tokens por turno com leitura cumulativa; alerta quando passar de X% do teto; revisão de testes que assumem o default alto |
| Testes obrigatórios | Teste L4 (loop): turno que ultrapassa o teto aborta com classe de erro esperada; configuração por setup sobrescreve o default; alerta não vira ruído |
| Bloqueios externos | nenhum |
| Estimativa (P/M/G) | P |
| Status | não iniciado |
| Dono | orchestrator + dev BE |

Plano curto: hoje qualquer chat cai em 999 steps por turno — um turno real documentado no scan chegou a ~14 milhões de tokens antes do `overloaded_error`. Reduzir o default para algo usável (50 é sugestão do scan, valor exato fica com o dev) e expor um teto de tokens por turno que é somado em cada step. Tarefas legítimas longas ganham override por setup. O alerta entra para o dev ver a frequência de tetos batidos antes de decidir se o default sobe ou se há setup mal-configurado.

---

### 1.2 — Timeout padrão do turno ou detector de stall

| Campo | Valor |
|---|---|
| Achados origem | `loop-sdk-timeout-zero` |
| Onda | 1 |
| Esforço | P |
| Risco | baixo |
| Depende de | — |
| Quem toca | BE (engine do turno) |
| Decisões do dono pendentes | nenhuma |
| O que precisa pra fechar | `turnTimeoutMs` default diferente de 0; detector de stall (sem progressão por N segundos); decisão sobre timeout duro vs watchdog suave; configurável por setup |
| Testes obrigatórios | Teste de turno pendurado (stream sem events) — deve abortar dentro do limite; lock e slot liberados no fim |
| Bloqueios externos | nenhum |
| Estimativa (P/M/G) | P |
| Status | não iniciado |
| Dono | orchestrator + dev BE |

Plano curto: sem timeout padrão, um stream pendurado prende o lock de thread e o slot de concorrência da org até alguém reiniciar. Adicionar timeout (valor exato fica com o dev — algo entre 10 e 30 min parece razoável para turnos interativos) e detector de stall para o caso do stream não falhar mas também não avançar. Configurável por setup para não quebrar workflows longos.

---

### 1.3 — `sideEffectWrite` com retry real; notificação no meio do turno só depois de persistir, com reenfileiramento

| Campo | Valor |
|---|---|
| Achados origem | `loop-sdk-sideeffect-retry`, `loop-sdk-p07-injeta-sem-persistir` |
| Onda | 1 |
| Esforço | P a M |
| Risco | baixo |
| Depende de | — |
| Quem toca | BE (engine + persistence) |
| Decisões do dono pendentes | nenhuma |
| O que precisa pra fechar | Retry idempotente real no `sideEffectWrite` (3 tentativas com backoff); notificação do filho só entra no turno depois do `INSERT` da notificação; se o insert falhar, reenfileirar (BullMQ ou mailbox) e seguir |
| Testes obrigatórios | Teste de falha transitória no banco durante o insert da notificação (mock); teste de 3 retries no `sideEffectWrite`; teste de idempotência (não duplicar a notificação se o insert sucede no retry) |
| Bloqueios externos | nenhum |
| Estimativa (P/M/G) | P a M |
| Status | não iniciado |
| Dono | orchestrator + dev BE |

Plano curto: hoje o helper `sideEffectWrite` promete 3 tentativas e executa 1. A notificação de subagente pode ser injetada no turno mesmo quando a gravação no banco falhou — o pai recebe o evento, mas nada fica persistido. Corrigir as duas pontas: retry real (com backoff e log) e a regra "injetar só depois de persistir, com reenfileiramento na falha".

---

### 1.4 — Fechar SSRF: webhooks via `safeRequest` (só `https`); `resolveAndValidate` em `connectMcp`, refresh de credencial e `test-connection`; `/api/mcp/test` só para admin, com rate limit

| Campo | Valor |
|---|---|
| Achados origem | `seguranca-ssrf-01` a `04`, `tools-prompt-ssrf-mcp-test`, varredura de lacunas |
| Onda | 1 |
| Esforço | P a M |
| Risco | baixo (com exceção explícita por env para MCP interno legítimo) |
| Depende de | — |
| Quem toca | BE (net + mcp + workflows + auth) |
| Decisões do dono pendentes | nenhuma (a exceção por env é técnica) |
| O que precisa pra fechar | Webhook: usar `safeRequest` (só `https`), cobrir redirect; `connectMcp`, refresh de credencial e `test-connection`: passar pelo guard; `/api/mcp/test`: exigir role admin + rate limit; TLS opcional nos MCPs internos: virar obrigatório |
| Testes obrigatórios | Teste de webhook com `http://` (deve recusar); teste de `redirect` para host interno (deve recusar); teste de MCP apontando para `127.0.0.1` (deve recusar); teste de rate limit em `/api/mcp/test`; teste do guard em `refresh-credential` |
| Bloqueios externos | nenhum (a exceção para MCP interno legítimo é via `env`, sem mudar contrato) |
| Estimativa (P/M/G) | P a M |
| Status | não iniciado |
| Dono | orchestrator + dev BE |

Plano curto: o guard SSRF (`be/src/net/ssrf.ts`) é bom mas só cobre `safeRequest` (passo `http` do workflow) e a checagem inicial dos webhooks. Quatro superfícies hoje aceitam URL arbitrária sem trava: webhook (incluindo redirect), `/api/mcp/test` (aberto a qualquer membro), `test-connection` (faz `fetch` cru na `baseURL` do MCP) e os MCPs configurados na thread. Fechar todas com o mesmo guard, exigir admin no `/api/mcp/test` com rate limit, e tornar TLS obrigatório para MCP interno (a topologia atual roda tudo no mesmo host, mas o aviso de boot já cobre mudança).

---

### 1.5 — Redigir o `configSnapshot` antes de gravar (hash calculado antes); headers no escopo da thread; o front não reenviar `•••`

| Campo | Valor |
|---|---|
| Achados origem | `tools-prompt-snapshot-com-segredos`, `seguranca-segredo-01` |
| Onda | 1 |
| Esforço | P |
| Risco | baixo |
| Depende de | — |
| Quem toca | BE (config snapshot) + FE (UI dos `•••`) |
| Decisões do dono pendentes | nenhuma |
| O que precisa pra fechar | Substituir o snapshot por uma versão redigida (placeholders para tokens, headers de auth) ANTES de gravar; calcular hash do conteúdo redigido, não do original; garantir que o `•••` ecoado pela UI nunca vire header real enviado ao MCP |
| Testes obrigatórios | Snapshot do banco/Redis não contém a string de um token de teste; o `•••` que volta da UI não vira header no `connectMcp`; hash bate entre conteúdo redigido e conteúdo original (uma das duas representações) |
| Bloqueios externos | nenhum |
| Estimativa (P/M/G) | P |
| Status | não iniciado |
| Dono | orchestrator + dev BE (FE para o `•••`) |

Plano curto: o `configSnapshot` é gravado a cada turno com segredos em claro (vai pro banco E pro Redis). O `•••` que a UI exibe para "esconder" um token vaza porque algum caminho no `connectMcp` reenvia o header real. Corrigir nos dois lados: BE redige antes de gravar (mantém o hash estável), FE garante que o placeholder nunca vire valor.

---

### 1.6 — Abort entre réplicas: assinante + `AbortReason` serializado; `abortChild` só marca `aborted` se confirmou

| Campo | Valor |
|---|---|
| Achados origem | tema "Parar/abort" (3.0) — `loop-sdk-abort-cross-replica`, `qualidade-abort-sem-assinante`, `subagentes-abort-cross-replica-morto`, `tempo-real-stop-abort-multi`, `dados-abort-sem-subscriber` |
| Onda | 1 |
| Esforço | P |
| Risco | baixo |
| Depende de | — (pré-requisito da 2ª réplica; latente hoje) |
| Quem toca | BE (engine, registry-redis, subagentes) |
| Decisões do dono pendentes | nenhuma |
| O que precisa pra fechar | Assinante do canal de abort; `AbortReason` serializado de forma estável; `abortChild` marca `aborted` só depois de confirmado pelo filho (ou por timeout) |
| Testes obrigatórios | Teste com 1 réplica (cancelar localmente funciona); teste com 2 réplicas simuladas (assinante pega o evento da réplica A vindo da réplica B); teste de `abortChild` que falha: o pai NÃO marca `aborted` |
| Bloqueios externos | nenhum |
| Estimativa (P/M/G) | P |
| Status | não iniciado |
| Dono | orchestrator + dev BE |

Plano curto: hoje o "Parar" publica num canal Redis (`registry-redis.ts:164`) com `{threadId, message}` que ninguém assina. Latente porque só roda 1 réplica. Antes de subir a 2ª, garantir que (a) há assinante, (b) o `AbortReason` é serializado de forma estável, e (c) `abortChild` só marca o filho como `aborted` quando o filho realmente confirmou — se a confirmação nunca chega, o pai continua esperando (e o reaper, item 1.7, pega o caso).

---

### 1.7 — Ligar `REAPER_ENABLED=1` em exatamente uma réplica, com alerta no boot

| Campo | Valor |
|---|---|
| Achados origem | `subagentes-reaper-desligado`, `dados-reaper-desligado` |
| Onda | 1 |
| Esforço | P |
| Risco | médio (reaper usa `isBusy` local, seguro com 1 réplica; antes da 2ª, ver 2.3) |
| Depende de | — (mas só "completo" depois de 2.3) |
| Quem toca | BE (engine + infra) |
| Decisões do dono pendentes | nenhuma |
| O que precisa pra fechar | Setar `REAPER_ENABLED=1` em exatamente uma réplica (a do worker, hoje); alerta no boot se a flag estiver sem replica de liderança; documentar a ordem antes da 2ª (ver 2.3) |
| Testes obrigatórios | Teste de boot com flag ligada (sem crash); teste de boot com flag desligada em todas as réplicas (alerta esperado); teste de crash entre `startTurn` e `finishTurn` com reaper ligado (turno é reaped dentro do tempo limite) |
| Bloqueios externos | nenhum |
| Estimativa (P/M/G) | P |
| Status | não iniciado |
| Dono | orchestrator + dev BE |

Plano curto: `REAPER_ENABLED` não está setado em nenhum serviço de produção. Crash entre `startTurn` e `finishTurn` deixa o turno `running` para sempre e o pai de subagente esperando. Hoje, com 1 réplica, o reaper pode usar o `isBusy` local (a flag foi desenhada assim). Ligar como está; alertar no boot se ninguém estiver com a flag. Antes da 2ª réplica, o reaper precisa de liderança (ver 2.3) — caso contrário duas réplicas competem.

---

### 1.8 — Cortar o `finalText` da notificação (4–8 mil chars) e o resultado de tool (20–50 mil), com marcador; notificação delimitada como dado não confiável

| Campo | Valor |
|---|---|
| Achados origem | `subagentes-notificacao-sem-teto`, `tools-prompt-sem-limite-resultado`, `tools-prompt-notificacao-role-user` |
| Onda | 1 |
| Esforço | P a M |
| Risco | baixo |
| Depende de | — |
| Quem toca | BE (engine + tools + subagentes) |
| Decisões do dono pendentes | nenhuma |
| O que precisa pra fechar | Teto no `finalText` da notificação do filho (4–8 mil chars, valor exato fica com o dev) + referência ao `threadId` para puxar o resto sob demanda; teto no resultado de tool (20–50 mil chars) com marcador `[resultado truncado]`; notificação do filho entra com moldura explícita "dado não confiável" e não com moldura de autoridade |
| Testes obrigatórios | Teste de `finalText` > limite (deve ser truncado com marcador); teste de resultado de tool > limite (idem); teste de prompt do pai: o texto truncado aparece com moldura correta, não como instrução |
| Bloqueios externos | nenhum |
| Estimativa (P/M/G) | P a M |
| Status | não iniciado |
| Dono | orchestrator + dev BE |

Plano curto: `finalText` do filho entra inteiro no histórico do pai até a compactação; resultado de tool também sem teto. Workflow já corta em 6 mil chars — alinhar os três lugares. A notificação do filho hoje entra com moldura de autoridade (`role: user`), o que confunde o modelo em prompts longos. Marcar como dado não confiável resolve.

---

### 1.9 — Validação de argumentos nas tools nativas

| Campo | Valor |
|---|---|
| Achados origem | `tools-prompt-args-sem-validacao` |
| Onda | 1 |
| Esforço | M |
| Risco | baixo |
| Depende de | — |
| Quem toca | BE (engine + tools nativas) |
| Decisões do dono pendentes | nenhuma |
| O que precisa pra fechar | Schema de validação em cada tool nativa (zod ou similar); quando o JSON é válido mas falta um campo obrigatório, o `execute` não roda — devolve erro estruturado; remover o `parseToolArgs` que devolve `{}` quando o JSON quebra (hoje ele só alimenta a UI, mas dá uma falsa sensação de "ok") |
| Testes obrigatórios | Para cada tool nativa: input válido (executa); input com campo obrigatório faltando (erro estruturado, sem executar); input com JSON quebrado (erro estruturado, sem executar) |
| Bloqueios externos | nenhum |
| Estimativa (P/M/G) | M |
| Status | não iniciado |
| Dono | orchestrator + dev BE |

Plano curto: `parseToolArgs` alimenta a UI, não a execução — JSON quebrado o SDK recusa. Mas JSON válido com campo faltando (`agent_spawn` recebendo `tarefa: undefined`) chega ao `execute` e roda. Adicionar validação real em cada tool nativa, com schema declarado e teste por tool.

---

### 1.10 — Tempo real barato: keepalive do servidor (ou watchdog pausado com aba oculta), jitter na reconexão, badge "rodando" reconciliada, modal de subagente a 4s

| Campo | Valor |
|---|---|
| Achados origem | `tempo-real-watchdog-reconecta-ocioso`, `tempo-real-backoff-sem-jitter`, `tempo-real-busy-travado-desconexao`, `tempo-real-dialog-estoura-ratelimit` |
| Onda | 1 |
| Esforço | P |
| Risco | baixo |
| Depende de | — |
| Quem toca | FE (maior parte) + BE (keepalive) |
| Decisões do dono pendentes | nenhuma |
| O que precisa pra fechar | Server-side keepalive (ou watchdog do FE que pausa quando a aba está oculta); jitter no backoff de reconexão para evitar "todas as abas voltam juntas no deploy"; reconciliação da badge "rodando" quando a conexão volta (hoje pode ficar acesa após queda); modal de subagente: polling com TTL maior (4s em vez de 2s) ou virar push |
| Testes obrigatórios | Teste de carga com 100 abas abertas: reconexão não dispara thundering herd; aba ociosa por 5 min: não reconecta a cada minuto; queda do WS: badge "rodando" reconcilia no próximo handshake |
| Bloqueios externos | nenhum |
| Estimativa (P/M/G) | P |
| Status | não iniciado |
| Dono | orchestrator + dev FE |

Plano curto: hoje, com 100 abas no pico, são ~3 000 req/min só de polling, sem cache, e o rate limit de 30 req/min por IP estoura com uma única aba no modal de subagente. Mudanças FE-only na maior parte: jitter no backoff, modal de subagente menos agressivo, badge que se reconcilia. Keepalive do servidor é uma linha no BE.

---

### 1.11 — Confirmar `SHOW max_connections` em produção e alinhar ao runbook (≥ 2100) ou ajustar `DB_CONNECTION_LIMIT`

| Campo | Valor |
|---|---|
| Achados origem | `dados-pool-1000` × lacuna (revisor rebaixou para baixa assumindo runbook ≥ 2100, mas o `postgresql.conf` mostra 100) |
| Onda | 1 |
| Esforço | P |
| Risco | médio (mudar o Postgres exige restart) |
| Depende de | decisão do dono (decisão 2 abaixo — manter "nunca reduzir o pool" e subir o Postgres, ou adotar PgBouncer) |
| Quem toca | Infra (Postgres) |
| Decisões do dono pendentes | "Banco: confirmar `max_connections` em produção; manter 'nunca reduzir o pool' e subir o Postgres para ≥ 2100, ou adotar PgBouncer?" (decisão 2) |
| O que precisa pra fechar | Rodar `SHOW max_connections` no Postgres de produção; comparar com o runbook (≥ 2100); se ≤ 200, decidir entre (a) subir `max_connections` no `postgresql.conf` e reiniciar, (b) ajustar `DB_CONNECTION_LIMIT` do Motor, (c) colocar PgBouncer na frente; registrar a decisão no `docs/operacao.md` |
| Testes obrigatórios | Validação com `SHOW max_connections` no banco de prod; teste de carga L7-16 repetido com a config nova; comparação do pico de conexões observadas |
| Bloqueios externos | nenhum (só leitura do banco) |
| Estimativa (P/M/G) | P |
| Status | não iniciado |
| Dono | orchestrator + Infra |

Plano curto: o `postgresql.conf` de produção mostra `max_connections = 100`, contra um pool pretendido de 1000 por processo (2000 com API e worker; o runbook pede ≥ 2100). Mas o container pode sobrescrever na linha de comando — daí o `SHOW max_connections` ser a verdade. Decisão do dono: subir o Postgres (preferência do scan, caminho "nunca reduzir o pool") ou colocar PgBouncer. Sem essa decisão, a ação fica em "ler e propor".

---

### 1.12 — Correções de texto e doc: aviso de colisão, header 16→100, cabeçalho `generateText`, `warpgrep`, "sem MCP externo", `docs/operacao.md`; definir `WF_AGENT_MAX_TOOL_STEPS` e `WF_DEFAULT_MAX_*`

| Campo | Valor |
|---|---|
| Achados origem | vários (3.1–3.5) — incluindo `tools-prompt-colisao-mantem-ultimo-mas-avisa-primeiro`, `subagentes-doc-concurrency-divergente`, `dados-default-divergente`, header `generateText` × `streamText`, append da thread "soma" × "substitui", prompt "sem MCP externo", "token na chave do pool", `docs/operacao.md` defasado |
| Onda | 1 |
| Esforço | P |
| Risco | baixo |
| Depende de | valores: decisão do dono (WF_DEFAULT_MAX_* e WF_AGENT_MAX_TOOL_STEPS) |
| Quem toca | BE + DevOps |
| Decisões do dono pendentes | "Política de custo: tetos padrão por turno (steps e tokens), por run (`WF_DEFAULT_MAX_*`) e por árvore de subagentes" (decisão 4) |
| O que precisa pra fechar | Corrigir o aviso de colisão de tools ("manteve o último" no código, "manteve o primeiro" no aviso — alinhar); header 16 × 100 (JSDoc `org-concurrency.ts:14`); cabeçalho `generateText` × código `streamText`; `docs/operacao.md` (atualizar junta); aviso "sem MCP externo"; chave do pool ("token na chave do pool"); definir e aplicar `WF_AGENT_MAX_TOOL_STEPS` e `WF_DEFAULT_MAX_*` |
| Testes obrigatórios | Teste de colisão de tools: o aviso bate com o código; lint/grep no repositório pelos termos antigos para garantir que não voltam |
| Bloqueios externos | nenhum |
| Estimativa (P/M/G) | P |
| Status | não iniciado |
| Dono | orchestrator + dev BE |

Plano curto: o scan achou várias divergências doc × código. Algumas são correções puras (avisos, headers JSDoc), outras exigem um valor numérico que o dono decide (decisão 4). Sem o valor do dono, marcar os tetos como `pending` no `.env` e prosseguir com as correções puras.

---

### 1.13 — `/info` protegido ou enxuto; parar de ecoar a mensagem do provider; remover (ou reaproveitar) o publish do canal `:stream`

| Campo | Valor |
|---|---|
| Achados origem | `seguranca-info-01`, `-info-02`, `tempo-real-events-canal-sem-ouvinte` |
| Onda | 1 |
| Esforço | P |
| Risco | baixo |
| Depende de | — |
| Quem toca | BE (rotas + engine) |
| Decisões do dono pendentes | nenhuma |
| O que precisa pra fechar | `/info`: ou exigir auth ou enxugar para o mínimo necessário; remover o eco da mensagem do provider (a string crua do SDK que vaza config); remover o publish no canal Redis `:stream` (que ninguém assina) OU reaproveitar como transporte do broadcast distribuído (que é parte da 2.3 — então, remover por enquanto e ressuscitar em 2.3) |
| Testes obrigatórios | `/info` sem auth retorna mínimo (sem config, sem versão detalhada); eco da mensagem do provider não aparece no response; nada depende do canal `:stream` no caminho típico |
| Bloqueios externos | nenhum |
| Estimativa (P/M/G) | P |
| Status | não iniciado |
| Dono | orchestrator + dev BE |

Plano curto: três ações rápidas de higiene. `/info` vaza config; o eco da mensagem do provider vaza o que o SDK mandou; o canal `:stream` recebe publishes sem assinante (custo puro). Como 2.3 quer justamente um broadcast distribuído em Redis, o canal pode ressuscitar nessa fase — mas em 1.13 é só remover.

---

## Onda 2 — Médio (semanas): custo real e várias réplicas

> Pré-requisito da 2ª réplica da API: 1.6 (abort entre réplicas) + 2.3 (estado distribuído) + 2.6 (banco e Redis dimensionados) + 3.2 ou a alternativa `ws` + pub/sub (broadcast distribuído).
>
> Itens de produto que dependem do dono antes de começar: 2.5 (cross-tenant) precisa vir ANTES de liberar troca de org na UI; 2.6 (PgBouncer vs subir o Postgres) precisa da decisão 2; 2.10 (namespace de tools) precisa de avaliação antes/depois; 2.11 (retenção) precisa da decisão 14.

---

### 2.1 — Fallback do convo (memória → Redis → banco) — vale já com 1 réplica

| Campo | Valor |
|---|---|
| Achados origem | `dados-convo-redis-writeonly` |
| Onda | 2 |
| Esforço | M |
| Risco | médio |
| Depende de | — |
| Quem toca | BE (engine + persistence) |
| Decisões do dono pendentes | nenhuma |
| O que precisa pra fechar | Implementar a cadeia: tentar memória (já tem); tentar Redis (já escreve, falta ler); tentar banco (consultar `messages`/`events`); medir o hit ratio de cada nível e o custo de uma queda completa |
| Testes obrigatórios | Teste com Redis vazio e memória fria (turno seguinte deve reconstruir do banco); teste com Redis cheio (leitura ignora o banco); teste de restart entre `startTurn` e `finishTurn` |
| Bloqueios externos | nenhum |
| Estimativa (P/M/G) | M |
| Status | não iniciado |
| Dono | orchestrator + dev BE |

Plano curto: o convo no Redis é write-only — escrito a cada turno, nunca lido. Um turno sem o convo em memória (restart seguido de um workflow que acorda a conversa, ou outra réplica) manda ao LLM só as mensagens novas, sem o histórico. Implementar a leitura na ordem memória → Redis → banco. Vale começar já, antes da 2ª réplica.

---

### 2.2 — `cache_creation` no contexto, custo e orçamento; preço por rota ou setup, ou custo vindo do LB

| Campo | Valor |
|---|---|
| Achados origem | `loop-sdk-usage-cache-creation`, `loop-sdk-preco-fixo` |
| Onda | 2 |
| Esforço | M |
| Risco | baixo |
| Depende de | coordenação com o LB (decisão 16) |
| Quem toca | BE (loop + custo) + LB (decisão sobre quem calcula) |
| Decisões do dono pendentes | "Cobrança: o custo exibido precisa ser exato para cobrar clientes?" (decisão 5); "Load Balance: quem coordena com o dono do LB as mudanças de contrato?" (decisão 16) |
| O que precisa pra fechar | Acordar com o dono do LB: o LB passa `cache_creation_input_tokens` no usage? (a resposta define a tecnologia); se sim, somar no contexto, no custo e no orçamento; se não, aceitar preço fixo de Sonnet para qualquer rota como teto aproximado |
| Testes obrigatórios | Comparação de usage reportado pelo Motor vs usage retornado pelo LB em um turno real (com cache hit e cache miss); teste de cobrança: o custo exibido bate com o cálculo esperado |
| Bloqueios externos | LB (decisões 5 + 16) |
| Estimativa (P/M/G) | M |
| Status | não iniciado |
| Dono | orchestrator + dev BE + dono do LB |

Plano curto: `cache_creation` fica fora do usage, do contexto e do orçamento. O preço é fixo de Sonnet para qualquer rota — isso é o teto aproximado. Antes de qualquer linha de código, decidir com o LB se ele passa `cache_creation` no usage (resposta define se 2.2 cabe no Motor ou se o LB calcula o custo e o Motor só exibe).

---

### 2.3 — Estado distribuído: `isBusy` pelo lock no Redis; fim de sessão por pub/sub; unique `(threadId, clientMessageId)`; mapas com TTL/LRU; rate limit, gate por org e circuit breaker no Redis; negação do gate como "ocupado" retentável

| Campo | Valor |
|---|---|
| Achados origem | tema "Estado local" (3.0) e afins — `tempo-real-broadcast-local`, `dados-broadcaster-local`, `tempo-real-busy-local` × `dados-isbusy-local`, `tempo-real-sessao-outra-replica`, `tempo-real-dedupe-so-local`, `tempo-real-mapas-sem-teto`, `dados-mapas-sem-eviccao`, `loop-sdk-live-sem-teto`, `seguranca-tools-01`, `loop-sdk-rate-limit-local`, `dados-rate-limit-local`, `dados-org-concurrency-local`, `loop-sdk-gate-denial-vira-erro` |
| Onda | 2 |
| Esforço | M |
| Risco | médio |
| Depende de | 1.6 |
| Quem toca | BE (engine + persistence + tempo real + auth) |
| Decisões do dono pendentes | nenhuma (decisão técnica) |
| O que precisa pra fechar | Mover `isBusy`, broadcaster, fim de sessão e dedupe de envio para o Redis (lock + pub/sub); unique constraint em `(threadId, clientMessageId)` na mailbox; TTL/LRU nos mapas em memória; rate limit, gate por org e circuit breaker também no Redis; quando o gate negar, devolver "ocupado" retentável em vez de "Erro do provider" |
| Testes obrigatórios | Teste com 2 réplicas simuladas: `isBusy` visto pelas duas; fim de sessão chegando nas duas; dedupe de envio entre réplicas; rate limit contado globalmente; gate por org com → retentável |
| Bloqueios externos | nenhum (a topologia Redis é decisão 3, mas 2.3 só precisa de Redis, não da separação fila × cache) |
| Estimativa (P/M/G) | M |
| Status | não iniciado |
| Dono | orchestrator + dev BE |

Plano curto: hoje broadcaster, `isBusy`, fim de sessão e dedupe vivem em `Map` de processo — funcionam com 1 réplica porque tudo está no mesmo processo. Antes da 2ª réplica, todos precisam ir para o Redis: `isBusy` via lock, fim de sessão via pub/sub, dedupe via unique `(threadId, clientMessageId)`, rate limit/rate-limit/gate/circuit breaker também em Redis. A negação do gate virando "ocupado" retentável (em vez de "Erro do provider") também entra aqui.

---

### 2.4 — Tetos por árvore de subagentes, abort em cascata, filtro de tools também nas nativas, persona e escopo de escrita explícitos

| Campo | Valor |
|---|---|
| Achados origem | `subagentes-fanout-ilimitado`, `subagentes-sem-cascata`, `tools-prompt-tudo-para-o-modelo`, `subagentes-persona-herdada-sumida`, `tools-prompt-thread-append-apaga-persona`, `tools-prompt-persona-herdada-perdida` |
| Onda | 2 |
| Esforço | M |
| Risco | médio (muda o comportamento do agente: medir) |
| Depende de | — |
| Quem toca | BE (engine + subagentes + tools) |
| Decisões do dono pendentes | "Subagentes: rever D4/D6 (largura e profundidade ilimitadas) e adotar tetos por árvore? Isolar a escrita por filho (worktree)?" (decisão 11); "System prompt: a camada da thread soma ou substitui a da org e a persona? O filho que herda o setup deve herdar a persona?" (decisão 10) |
| O que precisa pra fechar | Tetos por árvore (largura, profundidade, orçamento); abort em cascata (cancelar o pai desce para filhos e netos); filtro de tools também nas nativas (não só as MCPs); persona do setup herdada pelo filho; semântica explícita soma/substitui da camada da thread |
| Testes obrigatórios | Avaliação antes/depois em um conjunto curto de turnos que abrem subagentes (medir taxa de fan-out, custo, latência); teste de abort em cascata (cancelar pai derruba filhos); teste de persona: o filho recebe a persona do setup, não perde |
| Bloqueios externos | nenhum |
| Estimativa (P/M/G) | M |
| Status | não iniciado |
| Dono | orchestrator + dev BE |

Plano curto: hoje largura e profundidade de subagentes são ilimitadas (D4/D6), abort não desce em cascata, todas as tools vão para o filho mesmo quando o setup restringe, e a persona do setup se perde na herança. Mudar essas quatro coisas juntas (são relacionadas) — e medir antes/depois, porque é o tipo de mudança que parece pequena mas afeta o comportamento do agente.

---

### 2.5 — Cross-tenant: `loadEffectiveConfig` com a `OrgConfig` da org do turno

| Campo | Valor |
|---|---|
| Achados origem | `seguranca-cross-tenant-01` |
| Onda | 2 |
| Esforço | G |
| Risco | **alto** (toda a resolução de config; testar por org) |
| Depende de | — (mas **antes** de liberar troca de org na UI — ver decisão 8) |
| Quem toca | BE (config resolution) |
| Decisões do dono pendentes | "Multi-org: quando a UI terá troca de org? O conserto cross-tenant precisa vir antes." (decisão 8) |
| O que precisa pra fechar | Reescrever `loadEffectiveConfig` para resolver `OrgConfig` da org do turno (não da org default); testar por org (não só por setup); revisar todos os pontos que leem config para garantir que passam pela nova resolução |
| Testes obrigatórios | Teste de org A vs org B com mesmo setup: cada uma vê sua config (não a default); teste de org default: comportamento idêntico ao atual; teste de cache da config: warm-up também respeita a org |
| Bloqueios externos | nenhum (a UI não toca) |
| Estimativa (P/M/G) | G |
| Status | não iniciado |
| Dono | orchestrator + dev BE (com revisão de segurança) |

Plano curto: hoje toda org herda a `OrgConfig` da org default no turno — latente porque a UI não tem troca de org, mas crítico quando entrar. Reescrever `loadEffectiveConfig` para usar a org do turno. Alto risco porque toda a resolução de config passa por aqui.

---

### 2.6 — Infra: separar `REDIS_QUEUE_URL`; PgBouncer ou `max_connections` dimensionado; reaper com liderança

| Campo | Valor |
|---|---|
| Achados origem | `dados-pool-1000`; risco aceito do Redis (seção 3.9) |
| Onda | 2 |
| Esforço | M |
| Risco | médio |
| Depende de | decisões do dono (decisões 2 + 3) |
| Quem toca | Infra (Postgres + Redis) + BE (reaper com lock de liderança) |
| Decisões do dono pendentes | "Banco: confirmar `max_connections` em produção; manter 'nunca reduzir o pool' e subir o Postgres para ≥ 2100, ou adotar PgBouncer?" (decisão 2); "Redis: separar fila e cache (`REDIS_QUEUE_URL`), hoje juntos numa instância de 512 MB sem teto de memória?" (decisão 3) |
| O que precisa pra fechar | Após `SHOW max_connections` (1.11): subir Postgres para ≥ 2100 OU colocar PgBouncer; separar fila e cache em instâncias Redis diferentes (fila `noeviction`, cache com `allkeys-lru` e teto); reaper com lock de liderança (uma réplica só com a flag) |
| Testes obrigatórios | Teste de carga L7-16 repetido com a config nova; teste de perda de cache (fila continua, cache reconstrói); teste do reaper: só uma réplica reapa mesmo se a flag estiver em duas |
| Bloqueios externos | Postgres (restart) + Redis (provisionar nova instância) |
| Estimativa (P/M/G) | M |
| Status | não iniciado |
| Dono | orchestrator + Infra + dev BE (reaper) |

Plano curto: banco e Redis dimensionados para a 2ª réplica. Hoje fila e cache dividem 512 MB sem teto de memória. Banco em 100 conexões com pool de 1000 por processo. Após 1.11 dar a verdade sobre `max_connections`, decidir o caminho e implementar.

---

### 2.7 — Workflows: lista de passos sem `output`; log por cursor; lista de runs numa query; corridas de pause/resume/start; prazo de pausa

| Campo | Valor |
|---|---|
| Achados origem | `workflows-outputs-in-list-payload`, e afins (3.5) |
| Onda | 2 |
| Esforço | M |
| Risco | baixo |
| Depende de | — |
| Quem toca | BE (workflows) |
| Decisões do dono pendentes | nenhuma |
| O que precisa pra fechar | Lista de passos: omitir `output` do payload (puxar sob demanda); log paginado por cursor em vez de full pull; lista de runs consolidada em uma query; revisar corridas de pause/resume/start (sincronização); prazo para pausa (timeout se ninguém responde) |
| Testes obrigatórios | Teste de run com N passos: primeira página da lista não tem `output`; teste de log paginado (cursor anda corretamente); teste de pause sem resposta (entra em timeout) |
| Bloqueios externos | nenhum |
| Estimativa (P/M/G) | M |
| Status | não iniciado |
| Dono | orchestrator + dev BE |

Plano curto: a lista de passos e o handshake do WS puxam o `output` inteiro de cada passo; a lista de runs dispara 11 queries a cada 5s por aba (ver também 1.10). Reduzir o payload, paginar log, consolidar queries. Coisas pequenas mas que destravam volume.

---

### 2.8 — Compactação: overflow → compactar + retry; respeitar o Parar; compactador no slot e no custo; checagem por step

| Campo | Valor |
|---|---|
| Achados origem | `loop-sdk-*` de compactação (3.1) |
| Onda | 2 |
| Esforço | M |
| Risco | médio |
| Depende de | — |
| Quem toca | BE (engine + custo + concorrência) |
| Decisões do dono pendentes | nenhuma |
| O que precisa pra fechar | Quando o turno estoura o contexto: compactar o histórico e tentar de novo (não abortar); o compactador respeita o Parar; o compactador adquire slot e entra no custo (não é operação invisível); checagem por step (não acumular buffer até o fim) |
| Testes obrigatórios | Teste de turno que estoura o contexto: deve compactar e continuar; teste de Parar durante compactação: parar funciona; teste de custo: a compactação aparece no usage; teste de concorrência: compactação não fura o gate da org |
| Bloqueios externos | nenhum |
| Estimativa (P/M/G) | M |
| Status | não iniciado |
| Dono | orchestrator + dev BE |

Plano curto: compactação hoje não é confiável — estouro de contexto aborta em vez de compactar; o compactador não respeita Parar; não está no slot nem no custo; o buffer acumula step a step. Quatro correções relacionadas, todas com teste.

---

### 2.9 — Protocolo WS versionado (`v`, catálogo único) e métricas do tempo real

| Campo | Valor |
|---|---|
| Achados origem | `tempo-real-protocolo-sem-versao`, `-metricas-cegas` |
| Onda | 2 |
| Esforço | M |
| Risco | baixo |
| Depende de | — (base da onda 3) |
| Quem toca | FE + BE (tempo real) |
| Decisões do dono pendentes | nenhuma |
| O que precisa pra fechar | Adicionar campo `v` no envelope WS; catálogo único de eventos (BE exporta o tipo, FE consome); métricas do tempo real (não só contagem de sockets — TTFT, latência, taxa de perda, ack) |
| Testes obrigatórios | FE ignora evento sem `v` conhecida (com warn); evento novo entra no catálogo e o FE passa a consumir; métricas aparecem no Prometheus com nomes estáveis |
| Bloqueios externos | nenhum |
| Estimativa (P/M/G) | M |
| Status | não iniciado |
| Dono | orchestrator + dev FE/BE |

Plano curto: o protocolo WS não tem versão — deploy BE/FE acoplado, evento novo invisível. Métricas só contam sockets. Adicionar versão, catálogo único (de onde o FE importa o tipo), métricas ricas. É a base da Onda 3 (socket.io e broadcast distribuído).

---

### 2.10 — Namespace de tools e `toolPolicy` por `servidor__tool`; seção de tools gerada; fonte única de instruções

| Campo | Valor |
|---|---|
| Achados origem | `tools-prompt-tudo-para-o-modelo`, `tools-prompt-descricao-mcp-verbatim`, `tools-prompt-estimativa-ignora-tools`, `tools-prompt-colisao-mantem-ultimo-mas-avisa-primeiro` |
| Onda | 2 |
| Esforço | M |
| Risco | médio (muda nomes vistos pelo modelo; quebra o cache uma vez) |
| Depende de | avaliação antes e depois |
| Quem toca | BE (engine + tools + cache) |
| Decisões do dono pendentes | "Tools: 'nega por padrão' com curadoria por setup? Quais MCPs ficam no núcleo? Só admin cadastra MCP? Allowlist de hosts?" (decisão 9) |
| O que precisa pra fechar | Namespace `servidor__tool` em todo registro de tool; `toolPolicy` por nome com namespace; seção de tools gerada a partir do registro (fonte única); estimativa de contexto inclui o bloco de tools |
| Testes obrigatórios | Avaliação antes/depois em um conjunto fixo de turnos (medir custo de tools, taxa de erro, tempo até primeira tool útil); cache continua funcionando após a renomeação (uma vez); colisão detectada pelo novo namespace |
| Bloqueios externos | nenhum (a curadoria por setup é decisão do dono, mas a infraestrutura de namespace roda sem a decisão) |
| Estimativa (P/M/G) | M |
| Status | não iniciado |
| Dono | orchestrator + dev BE |

Plano curto: hoje as 362 tools vão todas para o modelo, em ordem alfabética byte-idêntica para o cache. Renomear para `servidor__tool` quebra o cache uma vez (e melhora o cache daí em diante, com mais prefixos estáveis). Curadoria por setup (decisão 9) entra aqui.

---

### 2.11 — Retenção de events só de exibição; índices compostos; cache de autenticação; cache da lista de webhooks

| Campo | Valor |
|---|---|
| Achados origem | `dados-events-sem-retencao` e afins (3.6) |
| Onda | 2 |
| Esforço | M |
| Risco | baixo |
| Depende de | decisão de retenção (decisão 14) |
| Quem toca | BE (persistence + auth + webhooks) |
| Decisões do dono pendentes | "Retenção: política para events de threads ativas (hoje mantidos para sempre, por decisão)?" (decisão 14) |
| O que precisa pra fechar | Definir TTL para `events` de exibição (separar dos que são fonte de verdade do turno); índices compostos nas queries quentes; cache da autenticação (hoje 2 leituras por request); cache da lista de webhooks |
| Testes obrigatórios | Query lenta fica rápida após o índice composto; cache de autenticação tem hit ratio > X%; após TTL, `events` antigos caem sem afetar a UI; teste de carga L7-16 com a config nova |
| Bloqueios externos | nenhum |
| Estimativa (P/M/G) | M |
| Status | não iniciado |
| Dono | orchestrator + dev BE |

Plano curto: quatro otimizações pequenas que destravam volume. Dependem de uma decisão de produto (decisão 14) para o TTL dos events.

---

## Onda 3 — Estrutural (meses)

> Itens grandes, com risco contratual. Cada um tem um desenho próprio na seção 5 do scan e exige alinhamento com pelo menos um outro app (LB, SSO, MCP, webhooks).
>
> A ordem importa: 3.1 (loop nativo) depende do LB; 3.2 (socket.io) depende de 2.3 + 2.9 + decisão sobre `ws` vs socket.io; 3.3 (tools sob demanda) depende de 2.10 e do LB repassar server tools; 3.7 (persistência por step) depende de 3.1.

---

### 3.1 — Loop nativo `@anthropic-ai/sdk` (tradutor, `AgentTransport`, shadow, flag por setup, persistência por step)

| Campo | Valor |
|---|---|
| Achados origem | seção 5.1 do scan (4.1 + loop.sdk * confronto com práticas) |
| Onda | 3 |
| Esforço | G |
| Risco | alto (contrato com o LB) |
| Depende de | snapshot do wire e acordo com o dono do LB; decisão sobre replay de thinking |
| Quem toca | BE (engine + persistence) + LB (contrato) |
| Decisões do dono pendentes | "SDK nativo: aprovar a migração incremental, abrindo mão da abstração multiprovider" (decisão 6); "Thinking: assumir a poda (e documentar) ou investir em preservar o thinking assinado?" (decisão 15); "Load Balance: quem coordena com o dono do LB as mudanças de contrato?" (decisão 16) |
| O que precisa pra fechar | Tradutor de histórico (formato AI SDK → Anthropic Messages); `AgentTransport` que isola o cliente; flag por setup (motor roda nativo ou AI SDK); shadow do wire (comparar dois caminhos por N turnos); persistência por step (ver 3.7); rollback por flag se o nativo regredir |
| Testes obrigatórios | Shadow por 1 semana em prod (sem flag, só log); avaliação de qualidade (juiz LLM, ver 3.5) com IA SDK vs nativo no mesmo conjunto; teste de replay de thinking (se a decisão for preservar) |
| Bloqueios externos | LB (decisão 16); opcionalmente Conta se o thinking assinado for requisito do produto |
| Estimativa (P/M/G) | G |
| Status | não iniciado |
| Dono | orchestrator + dev BE + dono do LB |

Plano curto: o Motor fala só Anthropic Messages via LB — a abstração do `ai` v5 cobra pedágio sem entregar valor (abort por nome mágico, `cache_creation` perdido, breakpoints montados por interceptor). Migrar para o SDK nativo incrementalmente, atrás de flag por setup, com shadow comparando os dois caminhos. Risco contratual com o LB exige snapshot do wire e acordo prévio.

---

### 3.2 — socket.io F0–F6 com Redis Streams adapter, salas, `seq`, ack e recuperação

| Campo | Valor |
|---|---|
| Achados origem | seção 5.2 do scan |
| Onda | 3 |
| Esforço | G |
| Risco | médio a alto (corte por cliente) |
| Depende de | decisão socket.io × `ws`; 2.3 e 2.9; config do proxy |
| Quem toca | BE (tempo real) + FE (cliente) + Infra (proxy) |
| Decisões do dono pendentes | "Tempo real: socket.io (como pedido) ou evoluir o `ws` nativo com pub/sub? Streaming token a token no chat é desejado? O corte por cliente é aceitável?" (decisão 7) |
| O que precisa pra fechar | 7 fases (F0–F6) descritas no scan, cada uma desligando um polling: F0 catálogo versionado (já em 2.9); F1 socket.io em paralelo com o `ws` para um cliente (opt-in); F2 salas por thread; F3 salas por run; F5 ack do cliente; F6 desligar o `ws` legado; REST como fallback. Redis Streams adapter para recuperação nativa |
| Testes obrigatórios | Por fase: smoke test no cliente; migração de um cliente (1% → 10% → 50% → 100%); teste de corte (cliente cai, eventos chegam pelo recovery); teste de carga com 1000 sockets |
| Bloqueios externos | proxy do painel (decisão de config: precisa suportar `ws` + socket.io); nenhum |
| Estimativa (P/M/G) | G |
| Status | não iniciado |
| Dono | orchestrator + dev BE/FE + Infra |

Plano curto: o desenho-alvo do scan é socket.io com Redis adapter (Streams para recovery). Migração em 7 passos, cada um desligando um polling. Risco de "corte por cliente" (quem não migrou fica sem evento) — daí a abordagem por fase.

---

### 3.3 — Tools sob demanda, curadoria por setup e painel de inspeção

| Campo | Valor |
|---|---|
| Achados origem | seção 5.3 do scan |
| Onda | 3 |
| Esforço | G |
| Risco | médio |
| Depende de | LB repassar server tools (para tool search); 2.10 |
| Quem toca | BE (engine + tools) + FE (painel) + LB (contrato) |
| Decisões do dono pendentes | "Tools: 'nega por padrão' com curadoria por setup? Quais MCPs ficam no núcleo? Só admin cadastra MCP? Allowlist de hosts?" (decisão 9) |
| O que precisa pra fechar | Núcleo pequeno de tools (carga fixa); tools sob demanda via tool search (LB precisa repassar `defer_loading`) OU `activeTools` por step; curadoria por setup; painel de inspeção por turno (hash de cada camada do prompt, tools finais com origem e tamanho, breakpoints de cache, tokens) |
| Testes obrigatórios | Turno com tool search: o modelo descobre e usa só as tools relevantes; custo cai X% em relação ao "tudo no prompt"; painel mostra os mesmos números que o log; curadoria por setup funciona (setup A não vê tools do setup B) |
| Bloqueios externos | LB (repassar server tools — decisão 16) |
| Estimativa (P/M/G) | G |
| Status | não iniciado |
| Dono | orchestrator + dev BE/FE + dono do LB |

Plano curto: hoje 362 tools vão para o modelo a cada turno, com 80–214 mil tokens só no bloco de tools. Carregar sob demanda (tool search ou `activeTools`) reduz custo drasticamente. Curadoria por setup dá controle ao dono. Painel de inspeção dá observabilidade (hoje só contagens no log e a config crua).

---

### 3.4 — Contratos versionados (LB, SSO, MCP, webhooks)

| Campo | Valor |
|---|---|
| Achados origem | seção 5.6 do scan |
| Onda | 3 |
| Esforço | M |
| Risco | baixo |
| Depende de | acordo com os donos dos outros apps |
| Quem toca | BE + LB + SSO + Conta (se MCP envolver Conta) |
| Decisões do dono pendentes | nenhuma diretamente (mas é pré-requisito para muitos outros itens: 3.1, 3.3, 2.2) |
| O que precisa pra fechar | Versionar contrato Motor↔LB (campos `v`, deprecation windows); Motor↔SSO; Motor↔MCP; Motor↔webhooks; documentar cada contrato em `docs/contratos/`; testes de conformidade |
| Testes obrigatórios | Suite de conformidade roda em CI (verifica que LB e Motor falam a mesma versão); teste de deprecação: versão antiga para de funcionar depois do window |
| Bloqueios externos | LB, SSO, Conta (MCP e webhooks) |
| Estimativa (P/M/G) | M |
| Status | não iniciado |
| Dono | orchestrator + dev BE (com cada dono de app) |

Plano curto: contratos hoje vivem no código e nos docs sem versão explícita. Versionar e testar. É trabalho de Glue entre apps — não é código Motor puro.

---

### 3.5 — Avaliações reais (~20 casos por uso, juiz LLM e revisão humana)

| Campo | Valor |
|---|---|
| Achados origem | seção 4.1 do scan |
| Onda | 3 |
| Esforço | M |
| Risco | baixo |
| Depende de | — |
| Quem toca | dev BE + revisor humano |
| Decisões do dono pendentes | nenhuma |
| O que precisa pra fechar | Definir 5–10 usos do Motor (chat, workflow, subagente, …); ~20 casos por uso; juiz LLM (modelo forte) + revisão humana; suite roda como gate antes do merge de ondas 1/2/3 |
| Testes obrigatórios | Cada onda 1/2/3 roda a suite antes de ser marcada como pronta; regressão detectada (qualidade caiu) trava o merge |
| Bloqueios externos | nenhum (precisa de acesso a um juiz LLM — pode ser o próprio LB) |
| Estimativa (P/M/G) | M |
| Status | não iniciado |
| Dono | orchestrator + dev BE |

Plano curto: o scan é uma fotografia — sem eval suite, qualquer mudança (especialmente 3.1 e 2.10) pode regredir qualidade sem ninguém ver. Construir a suite primeiro ou em paralelo com 3.1, e rodar antes de cada merge das ondas.

---

### 3.6 — Workflows: cache declarativo, alias de modelo por passo, agendamento, sinal para sub-run

| Campo | Valor |
|---|---|
| Achados origem | seção 5.5 do scan |
| Onda | 3 |
| Esforço | M a G |
| Risco | baixo |
| Depende de | — |
| Quem toca | BE (workflows) |
| Decisões do dono pendentes | "Workflows: exigir `budget`? Implementar o agendamento (`schedule`) ou retirá-lo?" (decisão 13) |
| O que precisa pra fechar | TTL de 1h de cache declarativo por setup (campo na receita); alias de modelo por passo (workflow aponta para um alias, alias aponta para modelo real); agendamento nativo (`schedule`) ou retirá-lo do schema; sinal explícito para sub-run (hoje implícito) |
| Testes obrigatórios | Cache declarativo: dois runs no mesmo setup dentro de 1h usam o cache; agendamento dispara um run no horário esperado; alias de modelo: trocar o alias não exige trocar a receita |
| Bloqueios externos | nenhum (decisão de produto sobre `schedule` é com o dono) |
| Estimativa (P/M/G) | M a G |
| Status | não iniciado |
| Dono | orchestrator + dev BE |

Plano curto: workflows já cobrem chaining, routing, paralelização, orchestrator-workers e evaluator-optimizer. Falta cache declarativo (hoje é só implícito), alias de modelo, agendamento nativo e sinalização explícita para sub-runs.

---

### 3.7 — Persistência por step; particionamento ou arquivamento de events

| Campo | Valor |
|---|---|
| Achados origem | seções 3.1 e 3.6 do scan |
| Onda | 3 |
| Esforço | G |
| Risco | médio |
| Depende de | 3.1 |
| Quem toca | BE (persistence + engine) |
| Decisões do dono pendentes | "Retenção: política para events de threads ativas (hoje mantidos para sempre, por decisão)?" (decisão 14) |
| O que precisa pra fechar | Persistir cada step do turno (não só o final), para suportar replay do thinking, observabilidade e auditoria; particionar `events` por mês OU arquivar `events` antigos para storage frio |
| Testes obrigatórios | Step persistido: dá pra replayar o turno do banco sem o SDK; particionamento: query por mês continua rápida; arquivamento: dado antigo sai do banco sem perder a auditoria |
| Bloqueios externos | nenhum (escolha de storage frio é com infra) |
| Estimativa (P/M/G) | G |
| Status | não iniciado |
| Dono | orchestrator + dev BE + Infra |

Plano curto: depende de 3.1 (que define o que conta como step). Persistir por step destrava replay do thinking (decisão 15) e observabilidade. `events` cresce sem teto — particionar ou arquivar.

---

## Decisões pendentes (17 do scan)

> Cada decisão abaixo é "do dono" — o scan não pode resolver sozinho. As decisões bloqueiam itens das ondas; uma decisão sem dono trava a onda inteira correspondente. Quem responde, o que precisa e qual item bloqueia estão em cada linha.

| # | Pergunta | Quem responde | Dado que falta | Bloqueia (item) |
|---|---|---|---|---|
| 1 | "Escala: quando subir a 2ª réplica da API e qual a meta de simultaneidade (usuários e turnos)? Define a urgência da onda 2." | Owner | meta de simultaneidade (usuários e turnos simultâneos); backlog de demanda | Onda 2 inteira (define urgência) |
| 2 | "Banco: confirmar `max_connections` em produção; manter 'nunca reduzir o pool' e subir o Postgres para ≥ 2100, ou adotar PgBouncer?" | Owner + Infra | `SHOW max_connections` em prod (ver 1.11); projeção de pico de conexões | 1.11, 2.6 |
| 3 | "Redis: separar fila e cache (`REDIS_QUEUE_URL`), hoje juntos numa instância de 512 MB sem teto de memória?" | Owner + Infra | projeção de uso de fila vs cache; budget | 2.6 |
| 4 | "Política de custo: tetos padrão por turno (steps e tokens), por run (`WF_DEFAULT_MAX_*`) e por árvore de subagentes; orçamento fail-open (hoje) ou fail-closed quando o Redis cai?" | Owner | tetos numéricos; decisão sobre fail-open vs fail-closed | 1.1, 1.2, 1.12, 2.4, 2.8 |
| 5 | "Cobrança: o custo exibido precisa ser exato para cobrar clientes? Isso decide entre preço por rota no Motor e custo informado pelo LB." | Owner (produto/finanças) | política comercial; cálculo do TB | 2.2 |
| 6 | "SDK nativo: aprovar a migração incremental, abrindo mão da abstração multiprovider (hoje o Motor só fala Anthropic via LB)?" | Owner + dono do LB | snapshot do wire LB; risco calculado | 3.1 |
| 7 | "Tempo real: socket.io (como pedido) ou evoluir o `ws` nativo com pub/sub? Streaming token a token no chat é desejado? O corte por cliente é aceitável?" | Owner + dev FE | caso de uso de streaming token a token; janela de migração aceitável | 3.2 |
| 8 | "Multi-org: quando a UI terá troca de org? O conserto cross-tenant precisa vir antes." | Owner (produto) | roadmap da UI; data prevista | 2.5 (deve vir antes de liberar a UI) |
| 9 | "Tools: 'nega por padrão' com curadoria por setup? Quais MCPs ficam no núcleo? Só admin cadastra MCP? Allowlist de hosts?" | Owner (produto/segurança) | lista de MCPs essenciais; quem cadastra | 2.10, 3.3 |
| 10 | "System prompt: a camada da thread soma ou substitui a da org e a persona? O filho que herda o setup deve herdar a persona?" | Owner (produto) | semântica desejada para thread; decisão sobre herança | 2.4 |
| 11 | "Subagentes: rever D4/D6 (largura e profundidade ilimitadas) e adotar tetos por árvore? Isolar a escrita por filho (worktree)?" | Owner (produto) | tetos numéricos; decisão sobre isolamento de escrita | 2.4 |
| 12 | "Slot de concorrência: manter por turno (desenho RATE-003) ou passar a por request?" | Owner (produto) | desenho de produto para subagentes | (sem bloqueio direto; o scan recomenda manter) |
| 13 | "Workflows: exigir `budget`? Implementar o agendamento (`schedule`) ou retirá-lo?" | Owner (produto) | casos de uso do agendamento | 3.6 |
| 14 | "Retenção: política para events de threads ativas (hoje mantidos para sempre, por decisão)?" | Owner + Infra | janela de retenção aceitável; destino (arquivamento?) | 2.11, 3.7 |
| 15 | "Thinking: assumir a poda (e documentar) ou investir em preservar o thinking assinado?" | Owner (produto) | requisito de auditoria; custo de preservar | 3.1, 3.7 |
| 16 | "Load Balance: quem coordena com o dono do LB as mudanças de contrato (`cache_creation`, modelo e custo, betas, versão)?" | Owner + dono do LB | canal formal (reunião quinzenal? slack dedicado?) | 2.2, 3.1, 3.3 |
| 17 | "Publicação: este relatório deve ir para o AgentPack de produção atual (`agentpack-v0`)?" | Owner | OK de publicação; instalação correta do MCP | (nenhum item — decisão editorial) |

---

## Perguntas abertas (9 do scan)

> Estas perguntas precisam de dados que o scan não leu (`.env` de prod mascarado no painel, métricas Prometheus, topologia real, configuração do LB). Quem responde, o que precisa e qual item bloqueia (ou só informa) estão em cada linha.

| # | Pergunta | Quem responde | Dado que falta | Bloqueia (item) |
|---|---|---|---|---|
| A | `.env` efetivo de produção (mascarado no painel): `DB_CONNECTION_LIMIT`, `ORG_CONCURRENCY_MAX`, `WF_*`, `CACHE_*`, `CHAT_TRUST_PROXY`, `METRICS_TOKEN`; `REAPER_ENABLED` não aparece na lista de variáveis. | Owner / Painel | acesso de leitura ao `.env` mascarado | 1.7, 1.11, 2.3, 2.8, 2.11 |
| B | Topologia e borda: réplicas, sticky no proxy do painel, API e WS na mesma origem no build do front. | Infra | mapa de réplicas e proxy | 1.10, 2.9, 3.2 |
| C | Tráfego real: turnos por minuto no pico, duração p50/p95 do turno, maior thread, taxa de 429/503 do proxy, taxa de acerto do cache (métricas Prometheus de tokens). | Infra / observador | Prometheus; logs do proxy | 2.1, 2.6, 2.11 |
| D | Tamanho real das definições dos MCPs de produção; filhos por pai (p99); tamanho típico do `finalText`; existência de netos. | dev BE | log do `appendEvent` ou query direta | 1.1, 1.8, 2.4 |
| E | Lado do LB: limpeza de sampling e thinking por modelo, `cache_creation` no usage, modelo real no SSE, repasse de betas e server tools. | dono do LB | conversa técnica; log do LB | 2.2, 3.1, 3.3 |
| F | Nomes exatos das opções do SDK oficial para endpoint e autenticação customizados. | dev BE (docs do SDK) | documentação atualizada do `@anthropic-ai/sdk` | 3.1 |
| G | O pool MCP é compartilhado entre orgs quando URL e headers coincidem, e é seguro no transporte SSE? (um revisor citou risco de credencial entre orgs no `bn-mcp.html`; não verificado) | dev BE + segurança | verificação no `be/src/mcp/*`; teste | (segurança — item de backlog ainda a criar) |
| H | O `runAgentStep` adquire slot de concorrência da org? A ordenação alfabética das tools é estável? `pruneConsumedMailbox` é chamada? | dev BE | grep + leitura | 1.9, 1.10 (refinamento) |
| I | Há threads com `pruneOldThinking: false` e histórico legado com reasoning assinado (define a urgência do replay)? | dev BE + DBA | query no banco de prod | 3.1, 3.7 |

> Observação: o brief listou "6 perguntas abertas" mas a seção 7 do scan tem 9 itens com bullet. Este ROADMAP usa a contagem real (9) e dá a cada uma uma letra (A–I) para não confundir com as decisões numeradas (1–17).

---

## Sequenciamento sugerido

> O scan já tem o grafo de dependências (Mermaid, abaixo). Este ROADMAP adiciona a leitura prática: **abrir pela Onda 1**, que não tem dependências cruzadas e cujos itens resolvem três problemas que travam tudo (custo descontrolado, vazamento de segredo/SSRF, abort sem assinante). Itens da Onda 1 que demandam decisão prévia do dono: 1.11 (decisão 2 — `max_connections`) e 1.12 (decisão 4 — `WF_DEFAULT_MAX_*`). Os outros 11 itens de Onda 1 podem começar em paralelo.
>
> **A 2ª réplica só deve subir depois de 1.6 + 2.3 + 2.6 + 3.2** (ou alternativa `ws` + pub/sub). A troca de org na UI só depois de 2.5. Tool search (3.3) e custo exato (2.2) dependem do LB. Loop nativo (3.1) depende de acordo com o LB E decisão 15 sobre thinking.
>
> **Decisões que mais bloqueiam:** decisão 4 (tetos numéricos — bloqueia 5 itens), decisão 16 (canal com o LB — bloqueia 3 itens grandes), decisão 14 (retenção — bloqueia 2 itens), decisão 8 (multi-org — bloqueia 2.5 e libera a UI). Resolver essas 4 destrava > 50% da onda 2.
>
> **Gating events (itens travados):** 1.11 → 2.6 (banco); 1.6 → 2.3 (estado distribuído); 2.3 + 2.6 + 2.9 → 3.2 (socket.io); 2.5 → troca de org na UI; 3.1 → 3.7 (persistência por step); 2.10 → 3.3 (tools sob demanda).

```mermaid
flowchart LR
    A16[1.6 abort entre réplicas] --> B23[2.3 estado distribuído]
    A17[1.7 reaper ligado] --> B23
    A111[1.11 max_connections] --> B26[2.6 banco e Redis]
    B23 --> R2[2ª réplica da API]
    B26 --> R2
    B29[2.9 protocolo versionado] --> C32[3.2 socket.io]
    B23 --> C32
    C32 --> R2
    B25[2.5 cross-tenant] --> ORG[troca de org na UI]
    B210[2.10 namespace de tools] --> C33[3.3 tools sob demanda]
    LB[acordo com o dono do LB] --> C31[3.1 loop nativo]
    LB --> B22[2.2 custo real]
    LB --> C33
```

(Mermaid copiado verbatim da seção 6 do scan. Não é código — é diagrama para marcar item por item.)

---

## Out of scope (explicit)

- Este ROADMAP é **planning overlay**, não reescrita do scan. Cada item aponta de volta para o scan pelos IDs (`1.1`, `1.2`, …, `3.7`) e pelas tags de achado. O scan é o source-of-truth para o "como"; este ROADMAP é o "quem, quando, o que bloqueia".
- Nenhum item deste ROADMAP toca o repositório `motor`. Nenhum `npm run gates`, nenhum `deployments_trigger`. Próximo passo (subagente separado) é virar este markdown em HTML para o dono ler no navegador.
- Itens do scan que **não viraram ações no roadmap** (refutados em 3.10 ou marcados "não verificados" sem severidade alta/crítica) ficam no scan e não precisam de item aqui. Ver 3.9 do scan para os refutados e 3.x para os de severidade média/baixa (que dependem de validação adicional antes de virarem ação).

---

## Apêndice — mapa scan ↔ ROADMAP

| Wave | Item scan | Item ROADMAP | Achados origem |
|---|---|---|---|
| 1 | 1.1 | 1.1 | `loop-sdk-maxsteps-999`, `tools-prompt-maxsteps-999` |
| 1 | 1.2 | 1.2 | `loop-sdk-timeout-zero` |
| 1 | 1.3 | 1.3 | `loop-sdk-sideeffect-retry`, `loop-sdk-p07-injeta-sem-persistir` |
| 1 | 1.4 | 1.4 | `seguranca-ssrf-01` a `04`, `tools-prompt-ssrf-mcp-test` |
| 1 | 1.5 | 1.5 | `tools-prompt-snapshot-com-segredos`, `seguranca-segredo-01` |
| 1 | 1.6 | 1.6 | tema "Parar/abort" (3.0) |
| 1 | 1.7 | 1.7 | `subagentes-reaper-desligado`, `dados-reaper-desligado` |
| 1 | 1.8 | 1.8 | `subagentes-notificacao-sem-teto`, `tools-prompt-sem-limite-resultado`, `tools-prompt-notificacao-role-user` |
| 1 | 1.9 | 1.9 | `tools-prompt-args-sem-validacao` |
| 1 | 1.10 | 1.10 | `tempo-real-watchdog-reconecta-ocioso`, `-backoff-sem-jitter`, `-busy-travado-desconexao`, `-dialog-estoura-ratelimit` |
| 1 | 1.11 | 1.11 | `dados-pool-1000` × lacuna |
| 1 | 1.12 | 1.12 | vários (3.1–3.5) |
| 1 | 1.13 | 1.13 | `seguranca-info-01`, `-info-02`, `tempo-real-events-canal-sem-ouvinte` |
| 2 | 2.1 | 2.1 | `dados-convo-redis-writeonly` |
| 2 | 2.2 | 2.2 | `loop-sdk-usage-cache-creation`, `loop-sdk-preco-fixo` |
| 2 | 2.3 | 2.3 | tema "Estado local" (3.0) |
| 2 | 2.4 | 2.4 | `subagentes-*` |
| 2 | 2.5 | 2.5 | `seguranca-cross-tenant-01` |
| 2 | 2.6 | 2.6 | `dados-pool-1000`; risco aceito do Redis |
| 2 | 2.7 | 2.7 | `workflows-*` |
| 2 | 2.8 | 2.8 | `loop-sdk-*` de compactação |
| 2 | 2.9 | 2.9 | `tempo-real-protocolo-sem-versao`, `-metricas-cegas` |
| 2 | 2.10 | 2.10 | `tools-prompt-*` |
| 2 | 2.11 | 2.11 | `dados-*` |
| 3 | 3.1 | 3.1 | seção 5.1 do scan |
| 3 | 3.2 | 3.2 | seção 5.2 do scan |
| 3 | 3.3 | 3.3 | seção 5.3 do scan |
| 3 | 3.4 | 3.4 | seção 5.6 do scan |
| 3 | 3.5 | 3.5 | seção 4.1 do scan |
| 3 | 3.6 | 3.6 | seção 5.5 do scan |
| 3 | 3.7 | 3.7 | seções 3.1 e 3.6 do scan |

---

## Apêndice — itens do scan que ficaram fora do ROADMAP (e por quê)

| Achado | Severidade | Por que ficou de fora |
|---|---|---|
| `loop-sdk-slot-por-turno` | refutado (3.9) | Slot por turno é desenho intencional (RATE-003). Só sobra correção de JSDoc — pequeno demais pra entrar como item; cabe em 1.12 se o dono quiser. |
| `loop-sdk-fanout-ilimitado` | refutado (3.9) | Defesas RATE-001/003/P-23 existem; o que sobra virou `subagentes-fanout-ilimitado` (Onda 2, item 2.4). |
| `loop-sdk-lockfile-divergente` | refutado (3.9) | Erro factual — produção instala pelo `be/package-lock.json` (5.0.253). Resta `check-lock:be` (risco baixo), não cabe em onda estruturada. |
| `loop-sdk-parse-args` | refutado (3.9) | `parseToolArgs` só alimenta UI; JSON quebrado o SDK recusa. O que sobra virou `tools-prompt-args-sem-validacao` (item 1.9). |
| `tempo-real-fila-descarta-stop` | refutado (3.9) | Só o silêncio na UI quando o descarte acontece (raro). Não cabe como item. |
| `tempo-real-resync-descarta-paginas` | refutado (3.9) | Nada. |
| `dados-events-rebuild-full` | refutado (3.9) | O desperdício no servidor foi confirmado em `tempo-real-handshake-repete-historico` (3.2 do scan). Quem cuida é 2.9 (protocolo versionado) + 3.2 (socket.io). |
| `dados-isbusy-local` | refutado como alta (3.9); confirmado como alta em `tempo-real-busy-local` | O que sobra virou `tempo-real-busy-local`, que mora em 2.3 (estado distribuído). |
| `dados-abort-sem-subscriber` | refutado (3.9) | O que sobra virou tema transversal "Parar/abort" (3.0), que virou o item 1.6. |
| `workflows-shared-redis-prod` | refutado (3.9) | Decisão registrada (instância em `noeviction`). Separar `REDIS_QUEUE_URL` virou decisão 3 + item 2.6. |
| `workflows-tls-db-prod-info` | refutado (3.9) | `DATABASE_TLS_POLICY=allow-insecure` é decisão registrada; revisar se Postgres sair do host — não cabe em item sem mudança de topologia. |
