# Relatório final — Scan do Motor

**Data:** 2026-10-11 · **Repositório:** `PedroPaduelo/motor` (sandbox `motor`, `/workspace`) · **HEAD auditado:** `69e0b28`

**Produção:** `motor-be-ws` e `motor-worker-ws` rodam `6fc7d65`. Conferido com `git` nesta revisão: o HEAD está só 2 commits à frente, ambos de frontend (markdown do chat). **O backend auditado é idêntico ao de produção**, e o fix do Parar (`020aa42`) já está em produção. Uma métrica de scanner fala em "4 merges à frente"; vale a conferência acima.

## 0. Metodologia e como ler

O scan cobriu **8 dimensões**: loop do agente e SDK; tempo real; tools e system prompt; agente e subagentes; workflows; banco, cache, threads e escala; segurança; qualidade e modularidade. Cada dimensão descreveu como o Motor funciona, respondeu às perguntas do dono com evidência `arquivo:linha`, mediu o que dava para medir e listou perguntas abertas. Depois vieram uma pesquisa de boas práticas (Anthropic como referência, mais mercado e tempo real) e uma investigação das lacunas (SDK nativo e socket.io; capacidade e topologia de produção; modularidade de tools e subagentes; workflows frente ao mercado; webhooks, rotas e versionamento de contratos).

**Revisão cética.** Todos os achados que os scanners classificaram como **crítica ou alta** passaram por revisores adversários, que procuraram contra-evidência no código, nos docs e nos logs de produção. Os achados média, baixa e info **não** passaram por essa rodada.

Status usados:

- **confirmado** — fato e severidade mantidos pelo revisor;
- **confirmado; sev. contestada (X→Y)** — fato confirmado, severidade revista pelo revisor; o relatório usa Y;
- **contestado** — revisores de dimensões diferentes divergiram sobre o mesmo fato; as duas leituras aparecem;
- **não verificado** — sem rodada adversária (média, baixa, info); hipótese do scanner com evidência `arquivo:linha`, a confirmar antes de agir;
- **refutado** — derrubado por contra-evidência; listado em 3.9 e fora do roadmap.

Severidade: crítica, alta, média, baixa, info. Esforço: **P** (horas a 1 dia), **M** (dias), **G** (semana ou mais). Dentro de cada área, a ordem é do mais grave ao mais leve, pela severidade revisada. As linhas de código citadas referem-se ao HEAD `69e0b28`.

**Limites.** Produção estava ociosa nas janelas de log lidas (2,66 req/min numa janela; 115 requests, todos 200, noutra). Não houve teste de carga: números de capacidade são estimativas por leitura de código e configuração. Segredos e `.env` de produção não foram lidos (aparecem mascarados no painel). Tudo que é estimativa ou incerto está marcado.

## 1. Sumário executivo — resposta direta às 9 perguntas

**P1. O loop atual é o melhor? Vale trocar para o SDK nativo da Anthropic (`@anthropic-ai/sdk`, API Messages), sem Agent SDK nem Claude Code? Como ficaria?**
O loop atual é bom e cheio de cicatrizes pagas: usage somado à mão (existe mesmo em falha), parcial gravado antes de classificar o erro, aborto com nome `AbortError` mais guarda de 5s, reparo de tool-calls. Seria o melhor se o Motor precisasse de abstração multiprovider. Mas o Motor fala **um único formato**: Anthropic Messages via Load Balance, com `model: "proxy-managed"` sempre e até 362 tools por turno. Nesse cenário o pacote `ai` v5 cobra pedágio sem entregar valor: aborto por nome mágico, retry escondido, `cache_creation` perdido, breakpoints de cache montados por interceptor que reparseia o body a cada step, buffer acumulado manual no `prepareStep`, replay de thinking inoperante. **Recomendação: migrar para o SDK nativo de forma incremental** (tradutor de histórico, runner nativo atrás de flag por setup, shadow do wire, rollback por flag), nunca big-bang: o contrato byte a byte com o LB precisa ser preservado e o histórico persistido está em formato do AI SDK. Desenho na seção 5.1.

**P2. O Motor aguenta alto volume, com muitas pessoas usando ao mesmo tempo?**
**Não, como está.** O núcleo foi desenhado para várias réplicas (lock distribuído, mailbox durável, drain atômico), mas hoje roda 1 réplica de API e 1 de worker, e há travas medidas:
(1) **banco:** 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). Falta confirmar com `SHOW max_connections`, porque o container pode sobrescrever na linha de comando;
(2) **custo e escala:** 999 steps por turno por padrão (um turno real queimou cerca de 14 milhões de tokens até `overloaded_error`), nenhum timeout padrão (`turnTimeoutMs = 0`), fan-out de subagentes sem teto por árvore;
(3) **gates por processo:** rate limit, concorrência por org e circuit breaker multiplicam por réplica; e a 2ª réplica hoje quebraria Parar, "rodando", eventos ao vivo e fim de sessão;
(4) **polling:** cerca de 3.000 req/min só de polling com 100 abas no pico, sem cache, e um rate limit de 30 req/min por IP que uma única aba com o modal de subagente já estoura.
Estimativa sem teste de carga: ~10–30 conversas simultâneas se o banco estiver mesmo em 100 conexões; ~50–80 turnos simultâneos por réplica, com teto duro de 100 turnos por org e por processo, se o banco seguir o runbook. **Recomendação:** ondas 1 e 2 do roadmap (tetos, timeout, preço real, abort, `isBusy` e broadcast distribuídos, banco e Redis dimensionados) e um teste de carga 10→50→100 turnos antes de prometer volume.

**P3. Como funciona o tempo real hoje (WebSocket, polling, SSE)? Como ficaria tudo via WebSocket com socket.io, sem polling e sem SSE?**
Hoje há **dois canais WebSocket nativos** (thread e run) em `GET /ws`, com handshake e replay; **9 pontos de polling REST** (lista de threads 8s; árvore e graph 3–5s; modal de subagente 2s + 3s; lista de runs 5s; log 4s; passo 3s; saúde 30s); e **zero SSE na camada do app** — o único SSE é interno, entre o SDK e o proxy. O chat não faz streaming token a token: manda o parcial uma vez por step. Com 2 réplicas o tempo real quebra, porque broadcaster, `isBusy`, abort e fim de sessão vivem na memória de cada processo. **Recomendação:** gateway socket.io com Redis adapter (Redis Streams adapter se quiser recuperação nativa de conexão), salas por thread, run, usuário e org montadas pelo servidor a partir da identidade autenticada, `seq` por sala com replay pelo banco, acks do cliente para o servidor e catálogo de eventos versionado. Migração em 7 fases (F0–F6), cada uma desligando um polling, com REST como fallback. Desenho na seção 5.2. Decisão do dono: socket.io (pedido) ou evoluir o `ws` nativo com Redis pub/sub (seção 7).

**P4. O código está modular e bem escrito? As integrações estão bem feitas? Há contradições, vazamento de informação ou problemas com as tools?**
**Em grande parte sim, acima da média para o tamanho** (backend com cerca de 57 mil linhas, frontend com cerca de 73 mil). Separação clara entre transporte (ws), execução (engine), estado (persistence) e tools; os cabeçalhos explicam o porquê; o contrato Motor↔LB é o melhor documento do repositório; MCP usa o SDK oficial v2 com pool consciente; o frontend tem zero `as any` (o backend tem 60).
**Contradições doc×código.** Confirmadas pelos revisores: o aviso de colisão de tools diz "manteve o primeiro" e o código mantém o último; o abort entre réplicas é documentado ("a dona escuta e aborta") e não tem assinante. Apontadas sem revisão: cabeçalho `generateText` × código `streamText`; append da thread "soma" × "substitui"; defaults de cache do schema × contrato do LB; prompt "sem MCP externo"; "token na chave do pool"; header 16 × código 100; `docs/operacao.md` defasado.
**Vazamentos.** Snapshot da config com segredos em claro gravado a cada turno no Postgres e no Redis; SSRF em 4 superfícies (webhooks, `/api/mcp/test`, `test-connection`, MCPs por thread); config da org default herdada por qualquer outra org (latente hoje, crítico com multi-org); `•••` ecoado pela UI vira header real enviado ao MCP.
**Tools.** Resultado sem teto; `required` decorativo para JSON válido com campo faltando; descrição de MCP entra verbatim no prompt; notificação de subagente entra com moldura de autoridade; 362 tools por turno no padrão.

**P5. Como as tools são montadas, interpoladas e enviadas ao modelo, e como o system prompt é gerado? Como modularizar e ter domínio?**
Por turno: MCPs conectados em paralelo via pool (`listTools`) → filtro opcional `mcps[].tools` → merge com as 23 nativas em **ordem alfabética** (prefixo byte-idêntico para o cache) → recorte pela `toolPolicy` do setup. O Record inteiro vai no `tools` de **cada step**, sem limite de tamanho. O system prompt é concatenação de 3 camadas: base (`system-prompt.md`, 25,9 mil chars) + `systemPromptAppend` efetivo (org + persona do setup, ou override que substitui) + bloco de voz. Interpolação só em 3 pontos: `${VAR}` no `chat-config.json` em disco, `token` de MCP virando `Authorization: Bearer`, e expressões `${...}` de workflow; não há motor de template no prompt. Custo medido: nativas ≈ 9,4 mil tokens + system ≈ 6,5 mil = ~15,8 mil tokens por request antes de qualquer MCP; com os MCPs de produção, estimativa de 80–214 mil tokens só no bloco de tools (premissa de 150–400 tokens por tool; não medido em produção), numa janela de 230 mil. Domínio hoje: só contagens no log e a config crua. **Recomendação:** (1) builder de prompt por camadas versionadas (nome, hash, tamanho) com semântica explícita de soma ou substituição; (2) registro com namespace `servidor__tool`; (3) núcleo pequeno com carregamento sob demanda (tool search ou `activeTools` por step); (4) painel de inspeção por turno (hash de cada camada, tools finais com origem e tamanho, breakpoints, tokens de cache); (5) tetos de descrição, schema, resultado e steps; (6) validação de argumentos. Desenho na seção 5.3.

**P6. A comunicação agente↔subagentes, que é assíncrona, é eficiente, sem overhead? Está claro para a IA como abrir subagente, escolher o setup e conduzir o processo?**
**O desenho é bom e assíncrono de verdade:** `agent_spawn` cria a thread filha e retorna na hora; o filho roda com lock próprio, em paralelo real; o resultado volta por push na mailbox do pai com exactly-once (`notifiedParentAt`); N notificações viram 1 turno; com o pai rodando, entram no meio do turno; o spawn é idempotente por `toolCallId`. **Overhead real:** o `finalText` do filho volta inteiro e fica no histórico do pai até a compactação; largura e profundidade são ilimitadas (D4/D6) sem orçamento por árvore; cancelar o pai não para filhos e netos; o reaper está desligado em produção, então um crash deixa o pai esperando para sempre. **Clareza para a IA:** descrições e seção do system prompt são boas (quando paralelizar, tarefa autocontida, `setup_list` antes de `setupId`, não esperar porque a notificação chega sozinha), mas faltam o limite de tamanho do retorno e o aviso de que filhos paralelos escrevem no mesmo workspace; e "o filho herda o setup" não traz a persona. **Recomendação:** contrato de delegação com retorno capado (4–8 mil chars + referência ao `threadId`), tetos por árvore, abort em cascata, reaper ligado em uma réplica, persona e escopo de escrita explícitos (seção 5.4).

**P7. Está eficiente em banco de dados, em threads (concorrência) e em cache?**
**Parcialmente.** Banco: transações curtas, bons índices, mailbox com `FOR UPDATE SKIP LOCKED`; mas um turno simples custa ~20–25 statements, a autenticação faz 2 leituras por request sem cache, cada `appendEvent` publica o JSON num canal Redis sem consumidor, `events` de thread ativa cresce sem purga e o pool (1000 por processo) não casa com o `max_connections = 100` lido em produção. Concorrência: lock por thread correto (Redis `SET NX` + heartbeat); mas rate limit, gate por org, circuit breaker, `isBusy`, abort e broadcaster são locais ao processo, e mapas em memória crescem sem expiração. Cache: janela quente de events bem desenhada; porém o convo no Redis é **write-only** (escrito a cada turno, nunca lido) — e 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. Fila e cache dividem a mesma instância Redis de 512 MB (decisão registrada). **Recomendação:** fallback do convo (memória → Redis → banco), remover ou aproveitar o publish, cache de autenticação, retenção de events, gates em Redis, confirmar e dimensionar `max_connections` (ou PgBouncer) antes da 2ª réplica.

**P8. O que é o workflow no Motor e como ele funciona?**
Uma **receita JSON imutável por versão** (`motor.workflow/v1`) com **11 tipos de passo**: 5 expansores, que materializam filhos (`map`, `verify`, `tournament`, `loop`, `switch`), e 6 executáveis no worker (`agent`, `transform`, `http`, `wait`, `human`, `workflow`). No arranque, o run congela a versão, a base técnica da org e um snapshot por setup citado (D002). O run tem 3 tetos: `usd` (mole), `maxSteps` (1–10.000, contando filhos) e `wallClockMs` (pausas e esperas humanas descontadas). O **reconciliador** é o cérebro: único que enfileira passo, com lease no Postgres; o Postgres é a verdade e o BullMQ (5 filas de trabalho + `wf-ping`) é transporte; 3 barreiras impedem execução dupla. O passo `agent` reusa o `runTurn` com saída validada; pausar é gracioso em 3 estados; cancelar é cooperativo, com fechamento imediato se ninguém está vivo; tudo se propaga a sub-runs. Eventos saem em 3 trilhas (log append-only com `seq`, filas, `runbus` efêmero para a tela) e a UI mostra 1 cartão por fan-out. Fronteira deliberada: `agent_spawn` é quando o **LLM** decide paralelizar; workflow é quando a **definição** decide. Dentro de um passo, tools de subagente e de governança são negadas.

**P9. Quais são as melhores práticas de mercado, com a Anthropic como referência, para multiagentes e runs de workflow, e onde o Motor está?**
O Motor **cobre as 5 categorias da Anthropic** — chaining, routing, paralelização (seções e votação), orchestrator-workers e evaluator-optimizer — e **está à frente da média** em reconciliação pelo banco, pause/cancel cooperativo com escape, custo de sub-run consolidado no pai e no modo assíncrono com notificação, que a Anthropic ainda lista como evolução futura. **Falta:** carregar tools sob demanda (tool search, `defer_loading`) — todo passo paga o catálogo inteiro, e um passo "trivial" custou US$ 0,25 e ~83 mil tokens; TTL de 1h de cache declarativo por setup; regras de cardinalidade e um "amortecedor" de delegação para a orquestradora; contrato de delegação completo (fronteiras, esforço, parada); evals reais com juiz LLM; tetos determinísticos por árvore; e observabilidade de cache e latência (TTFT, taxa de acerto). Tabela completa na seção 4.

**Recomendação geral.** O Motor é um runtime bem arquitetado, com dívidas de escala e segurança mapeadas e, nas mais graves, confirmadas. Ordem sugerida: **(i)** estancar custo e segurança (tetos, timeout, preço real, SSRF, segredos, reaper); **(ii)** destravar várias réplicas (abort, `isBusy`, broadcast e sessão distribuídos, banco e Redis dimensionados, cross-tenant antes de liberar troca de org); **(iii)** migrar o estrutural (SDK nativo, socket.io, tools sob demanda, painel de inspeção) com flags e shadow, sem big-bang. Nada exige reescrever o Motor: exige completar o que o desenho já prevê.

## 2. Como o Motor funciona hoje

### 2.1 O turno e o loop

Um turno é um **ator por thread**: cada thread tem lock próprio (uma conversa nunca espera outra), uma **mailbox** que agrupa tudo o que chegou num turno só e um loop de agente que chama o LLM em steps, via streaming SSE, **sempre contra o proxy Load Balance em formato Anthropic Messages**. O Motor nunca sabe provider, modelo real ou credencial: envia `model: "proxy-managed"` para `POST {customUrl}/v1/messages`, e o LB transpila, autentica e devolve SSE canônico (`be/docs/contrato-load-balancer.md`).

Passo a passo no caminho do chat:

1. **Recebimento e fila.** O WS recebe a mensagem e o dispatcher faz `submit` (`be/src/engine/dispatcher.ts:265`): rate limit por (org, usuário) só para submits externos (60/min por processo), push na mailbox (ilimitada por desenho, D4) e `pump` sem esperar. O `drain` é atômico: tudo o que estava pendente vira **um** turno (`dispatcher.ts:143-151`).
2. **Lock e preparo.** O `pump` pega o lock da thread no registry (memória ou Redis com TTL de 30s e heartbeat de 10s) e chama `runTurn` (`be/src/engine/turn-runner.ts:427`). O `prepareTurn` lê a thread e a config efetiva (global < setup < thread) e congela tudo numa linha `Turn`, para auditoria e replay.
3. **Compactação na fronteira, antes do LLM** (`turn-runner.ts:524-548`). O gatilho olha a ocupação medida no **fim do turno anterior** (`thread.metadata.contextTokens`). Se passou de `window × compactAt` (padrão 230 mil × 0,9), o compactador resume o histórico antigo, persiste um evento `compaction` e o LLM passa a ver [resumo] + eventos novos. Falha aqui nunca derruba o turno; após 3 falhas seguidas, escala alerta.
4. **Bootstrap do agente** (`turn-runner.ts:650` e `be/src/agent/bootstrap.ts`). Monta o model (placeholder `proxy-managed`), o system prompt (base + apêndice do setup + voz), conecta os MCPs em paralelo (falha de um não trava os outros) e soma as tools nativas (subagentes, workflows, setups). A `toolPolicy` do setup recorta as tools. Um `metadata.user_id` estável por conversa maximiza o cache no proxy.
5. **Persistência da entrada** (`turn-runner.ts:704-798`). Primeiro as notificações de subagentes (uma mensagem sintética combinada), depois cada mensagem do usuário, cada uma como evento append-only. Pré-cheque do orçamento mensal da org (Redis, em USD e/ou tokens): estourou, aborta antes de chamar o LLM.
6. **O loop** (`be/src/agent/loop.ts:402`, `runAgent`). Monta `messages` com breakpoints de cache (system + pontos do histórico, até 4 por request; tools via interceptor), poda ou reanexa thinking antigo, reserva 1 slot no gate da org e chama `streamText` do pacote `ai` v5 com `stopWhen: stepCountIs(maxSteps ?? 999)`, `maxRetries ?? 3` e o `abortSignal` do turno. A cada step: o modelo gera texto e/ou tool calls, as tools executam, `onStepFinish` soma o usage à mão e emite `onStep` para a timeline ao vivo. Entre steps, o `prepareStep` checa o orçamento e drena notificações novas da mailbox, reenviando um buffer acumulado. O aborto tem rede dupla: `reason` com nome `AbortError` (o único que o SDK reconhece) e uma guarda de 5s que encerra à força.
7. **Persistência da resposta e fechamento** (`turn-runner.ts:1091-1271`). `gravarRespostas` planeja os eventos por passo (`reasoning` só para exibição → mensagens limpas com tool-calls reparadas → `step_done`), grava, registra o usage no bucket de custo da org, mede a ocupação pelo usage do **último** step e fecha o Turn (`done`, ou `errored` se um turno humano saiu vazio). Em falha no meio, o parcial dos steps fechados é gravado antes de classificar o erro (orçamento, cancelamento, timeout, provider 4xx/5xx, MCP, interno).

Caminho dos workflows: `runAgentStep` (`be/src/engine/agent-step.ts:534`) é uma casca sobre `runTurn` sem mailbox: cria ou continua uma thread headless, segura o lock durante as rodadas (original + reparos de schema, padrão 1), acumula deltas token a token, soma usage e custo e devolve saída validada ou falha tipada (inclui `timeout` próprio e `mcp_indisponivel` antes de gastar).

```mermaid
flowchart LR
    WS[WS: user_message] --> SUB[dispatcher.submit<br/>rate limit + mailbox.push]
    SUB --> LOCK[pump: lock da thread<br/>Redis TTL 30s + heartbeat]
    LOCK --> PREP[prepareTurn<br/>config global, setup, thread<br/>congelada no Turn]
    PREP --> COMPACT{ocupação acima de<br/>window x compactAt?}
    COMPACT -->|sim| SUM[compactador resume<br/>evento compaction]
    COMPACT -->|não| BOOT[bootstrap<br/>model + system + MCPs + nativas<br/>toolPolicy]
    SUM --> BOOT
    BOOT --> PERSIST[persiste notificações e<br/>user_messages + pré-cheque de orçamento]
    PERSIST --> LOOP[runAgent: streamText v5<br/>até 999 steps · prepareStep<br/>orçamento + drain no meio do turno]
    LOOP --> SAVE[gravarRespostas por passo<br/>usage + contextTokens]
    SAVE --> DONE[finishTurn done ou errored<br/>evento final + limpa o ao vivo]
```

Arquivos-chave: `be/src/agent/loop.ts`, `be/src/engine/turn-runner.ts`, `be/src/engine/dispatcher.ts`, `be/src/engine/agent-step.ts`, `be/src/agent/bootstrap.ts`, `be/src/provider/anthropic.ts`, `be/src/agent/messages.ts`, `be/src/agent/compaction.ts`, `be/src/engine/response-events.ts`, `be/src/engine/org-concurrency.ts`, `be/src/engine/cost-tracker.ts`, `be/src/engine/registry.ts`, `be/src/engine/registry-redis.ts`, `be/docs/contrato-load-balancer.md`.

### 2.2 Tools e system prompt

O `prepareTurn` (`be/src/engine/turn-runner.ts:346`) calcula a config efetiva com `loadEffectiveConfig` (`be/src/setups/service.ts:763`): global da org (`OrgConfig` no Postgres) < setup da conversa (`Agent.configOverride` + persona + `toolPolicy`) < override legado da thread (`metadata.config`), fundidos por `resolveEffectiveConfig` (`be/src/config/thread-config.ts:473`). O `bootstrapTurn` monta o que o modelo vai ver:

1. `buildNativeTools`: **23 tools in-process** (4 de subagente, 4 de setup, 15 de workflow);
2. `bootstrapAgent` (`be/src/agent/bootstrap.ts:51`): conecta os MCPs da config em paralelo via pool (`acquireMcp`, `be/src/tools/mcp-pool.ts:136`) e filtra cada um pela allowlist `mcps[].tools` (`filterMcpTools`, `be/src/tools/registry.ts:28`);
3. `buildMcpTools` (`registry.ts:67`): junta tudo num Record único com as nativas, em **ordem alfabética**, para o prefixo ficar byte-idêntico e o cache render;
4. `applyToolPolicy` (`registry.ts:125`): recorta pela `toolPolicy` do setup.

O Record final vai inteiro, **a cada step**, no `tools` do `streamText` (`be/src/agent/loop.ts:499`); o SDK serializa cada tool como `{name, description, input_schema}` sem validar nem limitar o tamanho.

O **system prompt** é montado no `bootstrapAgent` (linhas 66-95) em camadas: base (`cfg.systemPrompt` inline ou o arquivo `be/system-prompt.md`, 25,9 mil chars, lido do disco a cada turno) + `systemPromptAppend` efetivo (append da org + persona do setup concatenados, ou um override da thread ou do setup que substitui tudo) + bloco de voz (só no modo voz, `be/src/agent/voice-prompt.ts:88`). Entra como primeira message de role `system` (`buildMessages`, `be/src/agent/messages.ts:72`) com `cache_control`; o interceptor do provider (`be/src/provider/anthropic.ts:178`) pode marcar a última tool com breakpoint de cache. Durante o turno, o `prepareStep` (`turn-runner.ts:956`) injeta notificações de subagentes e workflows como message sintética de role `user` (`[NOTIFICAÇÃO DO SISTEMA]`, `be/src/engine/mailbox.ts:232`). No fim, o usage do último step mede a janela (`turn-runner.ts:1180`) e pode disparar a compactação no turno seguinte.

```mermaid
flowchart TD
    CFG[loadEffectiveConfig<br/>org, setup, thread] --> NAT[buildNativeTools: 23 nativas]
    CFG --> MCP[acquireMcp em paralelo<br/>listTools por servidor]
    MCP --> FIL[filterMcpTools<br/>allowlist mcps.tools opcional]
    FIL --> MERGE[buildMcpTools<br/>Record único em ordem alfabética]
    NAT --> MERGE
    MERGE --> POL[applyToolPolicy do setup]
    CFG --> SYS[system: base + append efetivo + voz]
    POL --> REQ[streamText a cada step<br/>system + messages + tools inteiras]
    SYS --> REQ
```

**Interpolação** existe em três lugares distintos: `${VAR}` de ambiente só no `chat-config.json` em disco (`be/src/config/load.ts:57`; não vale para config no banco); `token` de MCP virando `Authorization: Bearer` no resolver (`thread-config.ts:329-342`); e `${...}` das expressões de workflow (interpretador próprio, fora do chat).

### 2.3 Agente e subagentes

O pai chama `agent_spawn` (title, task e, opcionalmente, systemPrompt, tools e setupId). O orchestrator cria a thread filha (`kind: subagent`) num único INSERT (`be/src/engine/orchestrator.ts:234-249`), herdando `metadata.config` do pai — mesma sandbox, mesmos MCPs, mesma rota —, mas **nunca o system prompt do pai** (`orchestrator.ts:175-184`). Com `setupId` explícito, valida o setup (ativo, não arquivado, visível) e injeta a persona uma vez no `systemPromptAppend` do filho; sem `setupId`, o filho só herda o `agentId` e roda **sem a persona** (`orchestrator.ts:186-223`). O spawn retorna logo após `dispatcher.submit` com trigger `spawn`; a tarefa vira a primeira `user_message` do filho, que roda com thread e lock próprios, em paralelo real.

Quando o filho termina um turno com a mailbox vazia, `onTurnEnd` faz o claim exactly-once (`updateMany` onde `notifiedParentAt` é nulo, `orchestrator.ts:317-321`), monta o `SubagentResultItem` (status, `finalText` integral, steps, duração, tokens, irmãos ainda rodando) e empurra na mailbox do pai. Pai ocioso: abre turno com trigger `subagent_notification`. Pai rodando: o `prepareStep` drena só as notificações e injeta no meio do turno. N notificações juntas viram 1 turno (drain atômico com `FOR UPDATE SKIP LOCKED`, `be/src/engine/mailbox-db.ts:98-146`). Falha vira notificação `errored`; pai deletado entre claim e push dispara rollback com evento `lost_notification`; crash de processo depende do reaper, que é opt-in e está **desligado em produção**.

```mermaid
sequenceDiagram
    participant Pai
    participant Mail as Mailbox do pai
    participant Filha
    Pai->>Filha: agent_spawn: thread filha + primeira user_message
    Note over Pai: retorna na hora, sem esperar
    Filha->>Filha: turnos próprios, com lock próprio
    Filha->>Mail: onTurnEnd: claim exactly-once + subagent_result
    alt pai ocioso
        Mail->>Pai: turno novo, trigger subagent_notification
    else pai rodando
        Mail->>Pai: prepareStep drena e injeta no meio do turno
    end
```

Na UI, o filho aparece inline na timeline do pai. O desenho prevê fan-out dos eventos do filho para a sala da raiz (`be/src/ws/handler.ts:234-251`), mas na prática o **progresso** do filho não chega à sala do pai — só `spawn` e `result`, emitidos direto (achado confirmado em 3.2) —; o modal de subagente compensa com polling de 2s.

### 2.4 Workflows

Um workflow é uma **receita JSON imutável por versão** (`schema: "motor.workflow/v1"`; `be/src/workflows/definition.ts`, 1911 linhas, 11 tipos de passo). Cada versão publicada vira uma `WorkflowVersion`. O run congela `versionId`, a base técnica da org (`orgConfigSnapshot`), um snapshot por setup citado (`setupSnapshot.__porSetup[setupId]`) e o setup padrão (`__padrao`) quando algum passo de IA fica sem `setupId` (D002, `be/src/workflows/runs.ts:57-179, 252-267, 404-436`). O `input` é validado contra o schema da versão **antes** de qualquer cobrança, e a `idempotencyKey` é única por org (repetir o POST devolve o mesmo `runId`).

**Os 11 tipos.** Cinco expansores, que materializam filhos em vez de ir para a fila: `map`, `verify`, `tournament`, `loop`, `switch` (`be/src/workflows/expanders.ts`). Seis executáveis, que rodam no worker: `agent`, `transform`, `http`, `wait`, `human`, `workflow` (`be/src/workflows/executors.ts`). Só `agent` cobra dinheiro: usa a ponte `runAgentStep` com saída estruturada validada (1 reparo) e propaga usage e custo na mesma transação que fecha o passo. O `loop` roda uma iteração por vez; `tournament` tem bye e empate (A vence); `switch` decide no tick e marca os outros ramos como `skipped`.

**Orçamento por run:** `usd` (mole; 0 = tolerância zero), `maxSteps` (1–10.000 contando filhos; o expansor falha com `fanout_excede_maxsteps` antes de criar) e `wallClockMs` (espera humana e pausas descontadas por união de janelas).

**Ciclo de vida.** O **reconciliador** é o único que enfileira passo (`be/src/workflows/reconcile.ts`): cada tick segura o run por lease no Postgres (30s, claim atômico com a condição do run dentro do UPDATE), materializa o que desbloqueou, enfileira o que está pronto (em lote, respeitando a concorrência do fan-out, 1–200, padrão 100) e fecha o run. Quem executa é o `motor-worker-ws`, processo separado, com workers dedicados às filas `wf-reconcile`, `wf-step-ai`, `wf-step-det`, `wf-notify`, `wf-maintenance` e `wf-ping`. Três barreiras impedem execução dupla: `jobId` determinístico (`step:<stepId>:<attempt>`), claim condicional e `@@unique([stepId, attempt])`; e um passo com `output` gravado nunca reexecuta. A recuperação é por batimento: tentativa sem heartbeat por 45s vira `lost` e volta para `pending`; run sem progresso por 120s fecha como `run_travado`.

**Pausar, retomar, cancelar.** Pausar é gracioso: `running → pausing → paused` (a porta fecha antes de contar o que está em voo). Retomar restaura `waiting` se ainda há portão humano aberto. Cancelar é cooperativo: se há worker vivo, marca a bandeira e espera; se ninguém está vivo, fecha na hora. Tudo se propaga recursivamente a sub-runs; o custo do filho sobe ao pai uma vez só.

**Eventos e tela.** Três trilhas: `workflow_events` (log append-only, `seq` monotônico por `UPDATE … RETURNING`), filas BullMQ duráveis e `runbus:*` no Redis Pub/Sub (efêmero, para o WS). O canal WS do run coalesce `step_partial` a 2 Hz e libera até 8 passos em foco. A tela agrupa os filhos de um fan-out num cartão com histograma: "40 revisores" é uma linha só.

**Fronteira com subagentes (intencional).** Dentro de um passo, as tools de subagente são **negadas** (o filho sobreviveria ao run e furaria o orçamento; paralelismo dentro de workflow é `map`/fan-out orçado), e as de governança (`workflow_run`, `pause`, `resume`, `decide`, `cancel`) também (um passo não aprova o próprio portão).

```mermaid
flowchart TD
    AUT[autoria: create, draft, add_step, validate] --> PUB[publicação: versão imutável]
    PUB --> ADM[startRun: valida input<br/>congela setups e org<br/>idempotencyKey]
    ADM --> TICK[reconciliador: tick com lease de 30s<br/>materializa e enfileira]
    TICK --> AI[wf-step-ai: passo agent<br/>runAgentStep com saída validada]
    TICK --> DET[wf-step-det: transform, http, wait]
    TICK --> HUM[human: portão, run em waiting]
    AI --> BAR[barreira no banco<br/>expected e completed]
    DET --> BAR
    HUM --> BAR
    BAR --> TICK
    BAR --> AVISO[aviso para a conversa de origem]
```

### 2.5 Tempo real

Há **dois canais WebSocket nativos** (sem socket.io e sem SSE no app) numa rota só, `GET /ws` (`be/src/plugins/websocket.ts:94`): `?thread=` vai para o canal de chat (`be/src/ws/handler.ts`) e `?run=` para o canal de workflow (`be/src/ws/run-channel.ts`). A identidade nunca vem da query: o hook de auth (`be/src/http/auth.ts:7-27`) autentica no upgrade por bearer de máquina, cookie de sessão da Conta ou `?ticket=` de uso único, e cada socket ganha um `connId`.

**Canal de thread.** O front abre 1 socket por thread ativa (`fe/src/features/thread-runtime/model/use-thread-runtime.ts:90`) com o cliente `fe/src/api/ws.ts`: backoff exponencial de 1s a 15s, sem jitter; fila de envio de 100 itens/256 KB; watchdog que fecha o socket após 60s sem frame. Ao conectar, o servidor resolve a thread, entra na sala, manda `connected` e reemite tudo: replay do histórico persistido mais o turno em andamento, vindo do buffer vivo no Redis. Durante o turno, o engine emite para a sala da thread e para a da raiz. O chat **não** faz streaming token a token: `assistant_partial` sai uma vez por step com o texto inteiro; deltas existem só como opção usada pelo executor de workflow. O `thread_busy` (início e fim de turno, e batimento de 25s enquanto ocupado) vai só para os sockets do dono, e a sidebar mantém um **segundo** socket por aba só para ouvi-lo.

**Canal de run.** O passo roda no worker e o socket está na API, então os frames atravessam processos por Redis Pub/Sub (`be/src/workflows/bus.ts`): o worker publica em `<prefixo>:runbus:<runId>` e cada réplica da API faz um `psubscribe` global e filtra por run. O front recebe `run_connected` (estado completo), `run_status`, `step_status`, `step_output`, `run_done` e `step_partial`.

| Superfície | Mecanismo hoje | Intervalo | Onde |
|---|---|---|---|
| Chat da thread | WS `/ws?thread=` com replay | parcial 1x por step | `be/src/ws/handler.ts` |
| Run de workflow | WS `/ws?run=` + Redis Pub/Sub | `step_partial` até 2 Hz | `be/src/ws/run-channel.ts`, `be/src/workflows/bus.ts` |
| Badge "rodando" da sidebar | 2º WS por aba, só `thread_busy` | batimento de 25s | `fe/src/components/thread-sidebar/model/use-thread-busy.ts:82-88` |
| Lista de threads | polling REST | 8s | `fe/src/features/thread/model/queries.ts:39` |
| Árvore e graph de subagentes | polling REST | 3–5s com filho rodando | `fe/src/features/subagent/model/queries.ts:57-63, 82-87` |
| Modal de subagente | polling REST | 2s eventos + 3s status | `use-subagent-events.ts:30`, `subagent/model/queries.ts:111-117` |
| Lista de runs | polling REST | 5s, até 11 queries | `fe/src/features/run/model/queries.ts:60-66`, `RunsListPage.tsx:142-148` |
| Log do run | polling REST | 4s | `run/model/queries.ts:132` |
| Detalhe do passo | polling REST | 3s | `run/model/queries.ts:178` |
| Saúde do backend | polling REST | 30s | `fe/src/features/system/model/queries.ts:28` |
| Voz e TTS | REST + streaming do corpo | sem polling | `speech-player.ts:451` |
| SSE | só interno, SDK → proxy LB | — | `be/src/agent/loop.ts:450-451` |

Na reconexão do chat, o front refaz o histórico por REST e poda o buffer ao vivo por `turnId`; turnos fechados sem conteúdo disparam um catch-up por `seq`.

**Várias réplicas (ponto central).** O broadcaster é 100% em memória (`be/src/ws/broadcaster.ts:59-65`); o `isBusy` enxerga só a réplica local (`be/src/engine/registry-redis.ts:154`); o abort entre réplicas publica num canal que ninguém assina (`registry-redis.ts:156-167`); o fechamento de sockets no fim de sessão só alcança o processo atual (`be/src/auth/session.ts:104-113`). Já são distribuídos: o lock de turno (Redis com TTL e heartbeat), o buffer vivo (Redis com TTL), a mailbox (Postgres com drain atômico) e o barramento de runs.

### 2.6 Dados e cache

**Caminho do turno no banco.** Mensagem → `dispatcher.submit` → `mailbox.push` (1 leitura `thread.findUnique` + 1 escrita `mailboxItem.create`, `be/src/engine/mailbox-db.ts:75-96`) → `pump()` (`hasPending` → `tryAcquire` com `SET NX` e TTL de 30s → `drain` atômico com `FOR UPDATE SKIP LOCKED` → `runTurn` → `release` em Lua). Dentro do `runTurn`: `prepareTurn` (`getThread` com cache Redis + `loadEffectiveConfig` + `startTurn`, que faz `UPDATE threads.last_turn` + `INSERT turn` numa transação) → compactação → bootstrap → `registry.getConvo` (conversa em memória, `turn-runner.ts:705`) → 1 `appendEvent` por mensagem (apaga o cache, transação `UPDATE last_seq` + `INSERT event` e escrita pós-commit no Redis via outbox com retry) → pré-check de orçamento (Redis `HGETALL` com espelho local de 1,5s) → `runAgent` (1 slot de concorrência da org pelo turno inteiro) → a cada step, `prepareStep` com checagem de orçamento e `drainNotifications` (1 UPDATE) → respostas (1 `appendEvent` por mensagem + `reasoning` e `step_done`) → `finishTurn` → `onTurnEnd`.

**Cache de events.** Por thread, um ZSET (score = `seq`) + HASH de payloads + sentinela `synced`, com janela quente de 200 events (trim a cada push, TTL de 1800s). Se a janela não cobre o pedido, lê do Postgres sem re-hidratar. O histórico para o LLM é reconstruído por `eventsToLlmMessages` (respeita o último checkpoint de compactação, `be/src/agent/history.ts:102-120`), mas só no connect do WS e depois de compactar; durante o turno vale o `getConvo` em memória.

**Fila de workflows** (BullMQ): transporte, com a verdade no Postgres — lease do reconciliador, claim condicional, heartbeat de tentativa a cada ~15s, varredura de órfãos a cada 30s em lote de 200, purga horária de `workflow_events` e retenção diária (threads na lixeira 30 dias, mailbox consumida 7, entregas de webhook 90, runs 180).

**Topologia de produção medida** (investigação de lacunas):

| Peça | RAM | Observação |
|---|---|---|
| `motor-be-ws` (API + WS) | 2 GB | 1 réplica; uso 242 MB e 1,3% de CPU na leitura |
| `motor-worker-ws` (BullMQ) | 1 GB | 1 réplica; o boot loga `sharesCacheInstance: true` e purga ligada (30 dias) |
| `motor-prod-postgres` (postgres:16-alpine) | 4 GB | `postgresql.conf` com `max_connections = 100` e `shared_buffers = 128MB` (defaults); confirmar override com `SHOW` |
| `motor-prod-redis` (redis-stack 7.4) | 512 MB | sem `maxmemory` nem `maxmemory-policy` explícitos (padrão `noeviction`); fila e cache na mesma instância; 24 MB em uso |

Logs do `motor-be-ws`: 115 requests, todas com status 200, p50 de 0,8 ms, p99 de 1.250 ms, máximo de 2.188 ms; três avisos `[mcp-pool] LEAK detected` (browser, agentpack, mp).


## 3. Achados por área

Formato: os achados **revisados** (originalmente crítica ou alta) aparecem em blocos com evidência, impacto, recomendação, esforço e a nota do revisor. Os **não verificados** (média, baixa, info) aparecem em tabela, com evidência, impacto e recomendação curtos. Os refutados estão em 3.9.

### 3.0 Temas transversais (o mesmo fato em várias dimensões)

Vários problemas foram achados por mais de um scanner. Esta tabela junta as leituras, inclusive quando os revisores divergiram.

| Tema | Achados e status | Leitura consolidada |
|---|---|---|
| Parar/abort entre réplicas | `loop-sdk-abort-cross-replica` (confirmado, alta→crítica); `qualidade-abort-sem-assinante` (confirmado, alta→crítica); `subagentes-abort-cross-replica-morto` (confirmado, alta); `tempo-real-stop-abort-multi` (confirmado, alta→média); `dados-abort-sem-subscriber` (refutado como bug ativo) | **Contestado só na severidade; o fato é unânime:** o único publish (`be/src/engine/registry-redis.ts:164`) leva só `{threadId, message}` e nenhum código assina o canal. Latente hoje (1 réplica). Com 2 ou mais, Parar não para, a UI mostra "Erro do provider" e `abortChild` marca `aborted` no banco com o turno rodando. Tratado como **bloqueador da 2ª réplica**; esforço P. |
| Estado local ao processo | `tempo-real-broadcast-local` e `dados-broadcaster-local` (confirmados, alta); `tempo-real-busy-local` (confirmado, alta) × `dados-isbusy-local` (refutado como "alta"; revisor: média); `tempo-real-sessao-outra-replica` e `tempo-real-dedupe-so-local` (confirmados, alta) | Broadcaster, `isBusy`, fim de sessão e dedupe de envio vivem em `Map` de processo. **Contestado** apenas na severidade do `isBusy`. Todos latentes com 1 réplica; todos precisam ir para o Redis antes da 2ª. |
| Reaper desligado | `subagentes-reaper-desligado` (confirmado por 2 revisores, crítica→alta); `dados-reaper-desligado` (confirmado, alta) | `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; a rede do reconciler de notificações (BE-ERR-09) também fica desligada. |
| Turno sem teto | `loop-sdk-maxsteps-999` e `tools-prompt-maxsteps-999` (confirmados, alta); `loop-sdk-timeout-zero` (confirmado, alta) | O chat cai sempre no default de 999 steps; não há teto de tokens por turno nem timeout padrão. Turno real: ~14 milhões de tokens até `overloaded_error`. |
| Custo real | `loop-sdk-usage-cache-creation` (confirmado, alta); `tools-prompt-cache-creation-ignorado` (não verificado); `loop-sdk-preco-fixo` (confirmado, alta→média) | `cache_creation` fora do contexto, do custo e do orçamento; preço fixo de Sonnet para qualquer rota (o teto em tokens é a rede). |
| Escritas colaterais | `loop-sdk-sideeffect-retry` (confirmado, alta); `seguranca-tools-02` (não verificado, média); `loop-sdk-p07-injeta-sem-persistir` (confirmado, alta); `seguranca-tools-03` (não verificado) | O helper de retry não retenta; notificação injetada sem persistir; metadados de compactação gravados com snapshot velho. |
| Canal `:stream` sem assinante | `tempo-real-events-canal-sem-ouvinte` (confirmado, alta) × `dados-publish-stream-sem-subscriber` (não verificado, baixa) | Fato confirmado: cada `appendEvent` publica o JSON inteiro num canal que ninguém escuta. Remover ou usar como transporte do broadcast distribuído. |
| SSRF | `seguranca-ssrf-01` a `04` e `tools-prompt-ssrf-mcp-test` (confirmados, alta); investigação de lacunas (refresh de credencial MCP por HTTP sem guard; `allowPrivateHosts` morta; TLS opcional) | O guard existe e é bom (`be/src/net/ssrf.ts`), mas só cobre o passo `http` do workflow (`safeRequest`) e a checagem inicial dos webhooks. |
| Fan-out de subagentes | `loop-sdk-fanout-ilimitado` (refutado) × `subagentes-fanout-ilimitado` (confirmado, alta) | **Contestado, e as duas leituras convivem:** há defesas por org (rate limit externo, gate de 100 por org e circuit breaker), o que refutou a tese de "bypass"; mas não há teto por árvore (largura, profundidade, orçamento), o que foi confirmado. |
| Argumentos de tools | `loop-sdk-parse-args` (refutado) × `tools-prompt-args-sem-validacao` (confirmado, alta→média) | `parseToolArgs` só alimenta a UI e JSON quebrado o SDK recusa; mas JSON **válido com campo faltando** chega ao `execute` — `agent_spawn` cria filho com tarefa "undefined". |
| Concorrência e rate limit locais | `loop-sdk-slot-por-turno` (refutado: intencional, RATE-003); `dados-org-slot-por-turno`, `loop-sdk-rate-limit-local`, `dados-rate-limit-local`, `dados-org-concurrency-local`, `loop-sdk-gate-denial-vira-erro` (não verificados) | Slot por turno é decisão de produto. O que sobra: contadores locais (multiplicam por réplica) e negação do gate virando "Erro do provider" sem retry. |
| Conexões do Postgres | `dados-pool-1000` (confirmado, alta→baixa) × investigação de lacunas (`max_connections = 100` no `postgresql.conf` de produção) | **Contestado.** O revisor rebaixou assumindo o runbook (≥ 2100), 1 réplica e pico medido de 151 conexões no teste L7-16. Se os 100 se confirmarem, a carga do próprio L7-16 não caberia: vira **alta**. Ação: `SHOW max_connections`. |
| Handshake e replay | `tempo-real-handshake-repete-historico` (confirmado, alta) × `dados-events-rebuild-full` (refutado no impacto de UI) | O servidor relê e reenvia a thread inteira a cada (re)conexão; o front descarta. O custo é do servidor, não trava a UI; o watchdog de 60s multiplica (`tempo-real-watchdog-reconecta-ocioso`). |
| Mapas sem teto | `tempo-real-mapas-sem-teto` (confirmado, alta); `dados-mapas-sem-eviccao`, `loop-sdk-live-sem-teto`, `seguranca-tools-01` (não verificados) | Vazamento lento de memória por thread, org e conexão até o restart. |
| Retorno e resultados sem teto | `subagentes-notificacao-sem-teto` (confirmado, alta); `tools-prompt-sem-limite-resultado` (confirmado, alta→média); `tools-prompt-notificacao-role-user` (confirmado, alta) | `finalText` do filho e resultado de tool entram inteiros (workflow já corta em 6 mil chars); notificação do filho chega com moldura de autoridade. |
| Persona do setup | `tools-prompt-persona-herdada-perdida`, `subagentes-persona-herdada-sumida`, `tools-prompt-thread-append-apaga-persona` (não verificados) | Herança de setup sem persona; apêndice da thread apaga a persona inteira. |
| Versão do pacote `ai` | `loop-sdk-lockfile-divergente` (refutado) | Produção instala pelo `be/package-lock.json` (`ai` 5.0.253, `@ai-sdk/anthropic` 2.0.101); o 5.0.179 é do lock da raiz (dev/CI) e do `node_modules` da sandbox. Isso também explica o warning "System messages…" visto em produção. Resta: não existe `check-lock:be` para pegar drift. |
| Header 16 × 100 | `subagentes-doc-concurrency-divergente`, `dados-default-divergente` (não verificados) | O refutador do slot confirmou: só o JSDoc de `be/src/engine/org-concurrency.ts:14` está defasado; código, `.env` e docs operacionais estão em 100. |

### 3.1 Loop do agente e SDK

**[crítica · confirmado; sev. contestada alta→crítica] `loop-sdk-abort-cross-replica` — Aborto entre réplicas perde as flags e vira "Erro do provider"** · Esforço P
- Evidência: `be/src/engine/registry-redis.ts:163-164` (publish só com `{threadId, message}`); `be/src/engine/turn-runner.ts:1285-1319` (classifica o erro por `isBudgetExceeded`, `isUserCancel`, `isTurnTimeout`); `turn-runner.ts:1352` (`turnsFailed.inc()` para tudo que não é cancelamento do usuário); `be/src/engine/orchestrator.ts:457-458` e `be/src/engine/server/index.ts:346-348` (cascade e shutdown usam o mesmo caminho).
- Impacto: com 2 ou mais réplicas, Parar, timeout, orçamento ou cascade que caiam fora da réplica dona não abortam nada, porque não há assinante; e, mesmo com assinante, a causa tipada se perderia: a UI mostraria "Erro do provider", a métrica de falhas inflaria e o workflow retentaria o que não devia. Hoje latente (produção roda 1 réplica).
- Recomendação: assinar `<prefixo>:abort` em todas as réplicas, serializar o `AbortReason` com as flags e reconstruir o `AbortError` na dona; logar quando o publish não tiver efeito; teste de integração com 2 réplicas.
- Revisão: mais grave que o descrito — o canal nem tem assinante (`grep` em `be/src`: só `runbus:*` e `launcher-wake`), e os comentários de `registry-redis.ts:22-26` e `be/src/engine/registry.ts:37` descrevem um desenho não implementado.

**[alta · confirmado] `loop-sdk-usage-cache-creation` — `cache_creation_input_tokens` fica fora do usage, do contexto e do custo** · Esforço M
- Evidência: `be/src/agent/loop.ts:470-479` (soma só input, output, cached e reasoning); `be/src/engine/turn-runner.ts:1180` (`contextTokens = input + cached`); `be/src/engine/cost-tracker.ts:86-91, 141-148, 150-157` (sem campo de criação); `be/src/engine/agent-step.ts:887-893`; `be/docs/contrato-load-balancer.md:58` (total = input + leitura + criação de cache). `grep` por `cacheCreation|cache_creation|providerMetadata` em `be/src`: zero ocorrências.
- Impacto: contexto submedido (a compactação dispara tarde), custo subfaturado e orçamento furado justamente nas threads que mais escrevem cache. Pela documentação de prompt caching, a escrita custa mais que o input normal (1,25x no TTL de 5 min; 2x no de 1h).
- Recomendação: ler `providerMetadata.anthropic.cacheCreationInputTokens` por step e somar em `contextTokens`, custo e orçamento; no loop nativo, ler os três contadores direto do SSE.

**[alta · confirmado] `loop-sdk-timeout-zero` — Sem timeout padrão, um stream pendurado prende lock e slot para sempre** · Esforço P
- Evidência: `be/src/config/schemas.ts:386` (`turnTimeoutMs` padrão 0); `be/chat-config.json:32` (0 no config padrão); `be/src/engine/turn-runner.ts:460-485` (o relógio só existe se > 0); `be/src/provider/anthropic.ts:159-197` (fetch sem timeout); `be/src/engine/registry-redis.ts:103-128` (o heartbeat renova o lock); `be/src/engine/reaper.ts:153` (o reaper ignora turno vivo no próprio processo).
- Impacto: provider que não fecha o stream (socket meio aberto, proxy travado) sem ninguém clicar em Parar = thread travada, lock mantido e slot vazado até reiniciar. As tools têm timeout (60s); o LLM não. A guarda de 5s só age depois que alguém aborta.
- Recomendação: padrão maior que zero no chat (ex.: 30–60 min) ou detector de stall (N minutos sem evento SSE aborta o step); timeout explícito no fetch ou no SDK.
- Revisão: o audit interno do dono já registrava o cenário (TURN-07 como alto; RATE-004 e CONFIG-13 como médios).

**[alta · confirmado] `loop-sdk-maxsteps-999` — Teto de 999 steps permitiu um turno real de ~14 milhões de tokens** · Esforço P
- Evidência: `be/src/agent/loop.ts:508`; `be/src/engine/dispatcher.ts:158-170` (o chat chama `runTurn` sem `maxSteps`); `be/src/engine/turn-runner.ts:224-229, 940-952`; `be/src/engine/agent-step.ts:689` e `be/src/workflows/executors.ts:340-343` (o limite de workflow depende de `WF_AGENT_MAX_TOOL_STEPS`, ausente do `.env.example`); log de produção do `motor-be-ws`: 81 responseMessages (~41 rodadas de tool), input 2.633.801 + output 23.622 + cached 11.340.563, terminou em `overloaded_error`.
- Impacto: um laço de tool queima milhões de tokens num turno só; quem parou o turno real foi o proxy sobrecarregado, não o teto. O `costBudget` é mensal por org e opcional, não um teto por turno.
- Recomendação: padrão menor (ex.: 50) configurável por setup, teto de tokens por turno com encerramento gracioso e alerta ao passar de N steps.

**[alta · confirmado] `loop-sdk-sideeffect-retry` — `sideEffectWrite` promete 3 tentativas e executa uma** · Esforço P
- Evidência: `be/src/engine/turn-runner.ts:91-104` (recebe `promise: Promise<unknown>`, já iniciada); `turn-runner.ts:105-131` (`await promise` sobre a mesma promise a cada tentativa); chamadores nas linhas 562, 823, 1017, 1202 e 1365; nenhum teste do helper.
- Impacto: "3 tentativas com backoff de 200 e 600 ms" vira 1 tentativa e ~800 ms parado. Um soluço de banco ou Redis que um retry real salvaria vira aviso e divergência entre cache e banco em título automático, `contextTokens`, compactação, notificação no meio do turno e `finishTurn`.
- Recomendação: receber uma função (`() => Promise`) e chamá-la a cada tentativa, mantendo o respeito ao `abortSignal`; cobrir com teste.

**[alta · confirmado] `loop-sdk-p07-injeta-sem-persistir` — Notificação no meio do turno é injetada mesmo quando a gravação falha** · Esforço M
- Evidência: `be/src/engine/turn-runner.ts:1017-1030` (usa `sideEffectWrite`, que nunca lança); `turn-runner.ts:1031-1038` (push e emit incondicionais); `turn-runner.ts:1004-1011` (o comentário diz o contrário); `be/src/engine/mailbox-db.ts:132-145` (o drain já marcou `consumed_at`); não existe reenfileiramento.
- Impacto: falha de banco no meio do turno faz o LLM reagir a uma notificação que não está no banco e já saiu da mailbox; um restart a perde para sempre.
- Recomendação: `sideEffectWrite` devolver sucesso ou falha; só injetar e emitir se persistiu; em falha, reenfileirar na mailbox.

**[média · confirmado; sev. contestada alta→média] `loop-sdk-preco-fixo` — Preço fixo de Sonnet para qualquer rota deixa o teto em USD aproximado** · Esforço M
- Evidência: `be/src/engine/cost-tracker.ts:118-120` (3 / 15 / 0,3 USD por milhão, por env); `cost-tracker.ts:64-67` (a aproximação está documentada); `cost-tracker.ts:93-98, 372-377` (há também teto em tokens, independente de preço); `be/docs/contrato-load-balancer.md:14` (o Motor nunca sabe o modelo).
- Impacto: se o LB rotear para um modelo mais caro ou mais barato, o teto em USD erra para cima ou para baixo, e o custo exibido mente na mesma direção. Quem cobra por uso não pode confiar no número.
- Recomendação: preço por rota ou setup, ou o LB informar modelo e custo no SSE; até lá, tratar o teto em tokens como o autoritativo.
- Revisão: rebaixado porque o teto em tokens é uma rede independente de preço e os preços são ajustáveis por env sem deploy — aproximado, não fictício.

**Não verificados (média, baixa, info):**

| Achado | Sev. | Esf. | Evidência | Impacto | Recomendação |
|---|---|---|---|---|---|
| `loop-sdk-compactacao-proximo-turno` — compactação só enxerga a explosão no turno seguinte (TURN-12) | média | M | `be/src/engine/turn-runner.ts:516-525` | turno gigante paga o contexto cheio até o fim | checar o usage por step com teto de tokens; estourou, encerra gracioso, compacta e orienta a continuar |
| `loop-sdk-compactador-sem-custo` — compactador fora do slot, sem retry e sem custo | média | P | `be/src/agent/compaction.ts:44, 347-356`; `turn-runner.ts:535-548` | fura o gate sob carga; até ~75 mil tokens por compactação invisíveis ao orçamento | passar pelo slot, retry limitado, somar o usage no bucket da org |
| `loop-sdk-parar-compactacao` — Parar ou timeout durante a compactação é engolido | média | P | `turn-runner.ts:547, 587-644` | o turno segue para o LLM e gasta mesmo depois do Parar | no catch, relançar se o `abortSignal` estiver abortado |
| `loop-sdk-replay-morto` — replay de thinking assinado é inoperante | média | M | `be/src/engine/response-events.ts:51-62`; `turn-runner.ts:926-928`; `be/src/agent/loop.ts:159-186`; warnings "unsupported reasoning metadata" em produção | `pruneOldThinking: false` não funciona depois de persistir; doc, contrato e código divergem | assumir a poda sempre e documentar, ou guardar as assinaturas fora da conversa e reanexar |
| `loop-sdk-erro-sem-status` — erro de stream sem status vira erro genérico | média | P | `loop.ts:501-503`; `turn-runner.ts:1330-1334, 1495-1563` | overloaded e rate limit aparecem como "Erro do provider"; workflow não distingue o que é retentável | mapear o tipo do erro para `errorKind` com status 429/503 e expor `retryAfterMs` |
| `loop-sdk-overflow-sem-retry` — estouro de janela (400) não compacta nem retenta | média | M | `be/src/config/schemas.ts:156`; `turn-runner.ts:1330-1344` | o usuário precisa reenviar; com janela padrão de 230 mil e modelo de 200 mil é questão de tempo | detectar overflow, compactar e retentar 1 vez; calibrar a janela por rota |
| `loop-sdk-bootstrap-perde-itens` — falha no bootstrap descarta mensagens já drenadas | média | P | `turn-runner.ts:650-693, 704-798`; `be/src/engine/dispatcher.ts:150` | mensagem do usuário some (Turn `errored` sem o texto); o reenvio duplica | persistir os itens antes do bootstrap ou reenfileirar em falha antes do LLM |
| `loop-sdk-guard-partial-vazio` — a guarda de aborto devolve parcial vazio | média | P | `loop.ts:563-572`; `dispatcher.ts:158-170`; `agent-step.ts:632-637` | no chat, cancelar no meio joga fora o texto já gerado e pago | acumular deltas no `runAgent` e devolver na guarda |
| `loop-sdk-repair-apaga-args` — o reparo troca input corrompido por `{}` | média | P | `response-events.ts:71-90` | o histórico passa a mentir sobre os argumentos usados | preservar o texto cru truncado com marcador ou anotar no `tool_result` |
| `loop-sdk-persistencia-fim` — a resposta só é gravada no fim do turno | média | G | `turn-runner.ts:1092`; `agent-step.ts:52-54` | crash no meio de um turno longo: tools com efeito real e custo pago, zero eventos | persistir por step (o loop nativo deve nascer assim) |
| `loop-sdk-rate-limit-local` — rate limit por (org, usuário) local ao processo | média | M | `be/src/engine/rate-limit.ts:40-44`; `dispatcher.ts:269-274` | o teto efetivo vira 60 × réplicas | mover para Redis (INCR + EXPIRE) |
| `loop-sdk-retry-tempestade` — retries sincronizados contra o LB | média | P | `loop.ts:507`; `be/src/engine/org-concurrency.ts:58-60` | antes do circuit abrir, dezenas de turnos retentam juntos em 2s, 4s, 8s contra um proxy já sobrecarregado | jitter nos retries, ou menos retries e circuit mais rápido |
| `loop-sdk-gate-denial-vira-erro` — negação do gate vira "Erro do provider" | média | M | `loop.ts:434-448`; `turn-runner.ts:1330-1344` | sob carga o usuário vê erro genérico e reenvia (duplica a mensagem); `retryAfterMs` é ignorado | `errorKind` próprio e retentável; reenfileirar com atraso ou responder 429 honesto |
| `loop-sdk-injected-ordem` — ordem memória × banco das notificações diverge | baixa | M | `turn-runner.ts:932-935, 1017-1030` | depois de um restart, o histórico reconstruído difere do que o modelo viu | inserir na posição vista ou documentar a divergência |
| `loop-sdk-system-messages` — system dentro de `messages` gera warning a cada step | baixa | P | `be/src/agent/messages.ts:77-79`; log de produção | ruído de log que dilui sinais reais | usar a opção `system` ou `allowSystemInMessages` explícito |
| `loop-sdk-fetch-interceptor` — o interceptor reparseia o body a cada step | baixa | M | `be/src/provider/anthropic.ts:159-197` | CPU por step com 362 tools; em falha de parse envia sem `user_id` e quebra o roteamento de cache do LB | preservar o `user_id` no fallback; no loop nativo, montar o body direto |
| `loop-sdk-cache-5bps` — sem garantia do teto de 4 breakpoints | baixa | P | `messages.ts:4, 72-107`; `anthropic.ts:178` | a config pode plantar 5; a API ignora o excedente e o cache rende menos | impor o teto com prioridade e avisar ao descartar |
| `loop-sdk-live-sem-teto` — buffer ao vivo e convo crescem sem teto no turno | baixa | P | `be/src/engine/registry.ts:186-189`; `registry-redis.ts:172-177` | turno longo acumula MBs por thread em memória e no Redis | janela deslizante e limite defensivo |
| `loop-sdk-compactacao-memoria` — compactação carrega todos os events na memória | baixa | M | `compaction.ts:163-180, 208` | thread com mais de 10 mil events = dezenas de MB por compactação | projeção SQL enxuta e estimativa sem materializar tudo |
| `loop-sdk-extendedthinking-morto` — a opção `extendedThinking` nunca é passada | info | P | `loop.ts:250, 409-418`; `turn-runner.ts:940-955` | código morto com `budget_tokens`, rejeitado nos modelos atuais | remover ou ligar à config com tipo adaptativo |
| `loop-sdk-extras-ignorados` — `temperature`, `max_tokens` e `stream` em extras são ignorados | info | P | `anthropic.ts:31-38, 114-148` | o operador configura e nada acontece, sem aviso | documentar como não suportado ou avisar |
| `loop-sdk-custo-fail-open` — orçamento é fail-open por padrão | info | P | `cost-tracker.ts:50-53, 369-371` | Redis fora + carga = estouro sem alarme (decisão documentada) | manter, mas alertar quando o gate decidir pelo espelho local |
| `loop-sdk-preparestep-v7` — buffer acumulado manual exigido na v5 | info | M | `loop.ts:281-290`; `turn-runner.ts:1040-1047` | atualizar o `ai` para v7 muda a semântica e pode duplicar injeções | registrar como restrição; no loop nativo vira append direto |

**Métricas medidas (loop):**
- Tamanho em linhas: `turn-runner.ts` 1583; `agent-step.ts` 956; `loop.ts` 654; `compaction.ts` 473; `org-concurrency.ts` 263; `provider/anthropic.ts` 227; `bootstrap.ts` 187; `response-events.ts` 175; `messages.ts` 107; `contrato-load-balancer.md` 74.
- Versões: produção instala pelo `be/package-lock.json` (`ai` 5.0.253, `@ai-sdk/anthropic` 2.0.101); o lock da raiz (5.0.179 e 2.0.77) atende dev e CI e é o que está no `node_modules` da sandbox.
- Gate por org: até 100 chamadas em voo por processo; backoff de 30s, teto de 120s; circuit abre por 90s após 3 respostas 429/503 em 60s.
- Loop: `stopWhen` 999; `maxRetries` 3 (o padrão do SDK é 2); guarda de aborto de 5s. Retry instalado: 2s inicial, fator 2, respeita `retry-after`; só erro marcado como retentável retenta; abort nunca retenta.
- Turno: `turnTimeoutMs` 0; `sideEffectWrite` com 0, 200 e 600 ms; compactação em 230 mil × 0,9, `keepTurns` 4, saída do compactador de 8 mil tokens.
- Cache padrão: TTL de 5 min em system, tools e messages; `cacheTools` ligado; `markLastUser` e `markLastAssistant` desligados; checkpoint acima de 20 mensagens.
- Contrato com o LB: `proxy-managed`, `max_tokens` 64000, stream sempre, thinking adaptativo resumido, effort alto.
- Lock e registry: TTL de 30s com heartbeat de 10s; convo com TTL de 3600s; buffer ao vivo com 900s; reaper de órfãos em 30 min (desligado).
- Custo: 3 / 15 / 0,3 USD por milhão; espelho local de 1,5s; bucket com TTL de 70 dias. Compactador: transcript de até 300 mil chars.
- MCP: chamada de tool 60s; conexão e listagem 30s.
- Produção (`motor-be-ws`): turno com 81 responseMessages e ~14 milhões de tokens até `overloaded_error`; dezenas de warnings "unsupported reasoning metadata" e "System messages"; 362 tools num turno (76 + 70 + 193 de MCP + 23 nativas); 3 avisos `[mcp-pool] LEAK detected`. `motor-worker-ws`: só o boot, sem erros de turno na janela.


### 3.2 Tempo real (backend e frontend)

**[alta · confirmado] `tempo-real-broadcast-local` — Broadcaster 100% em memória: a 2ª réplica quebra o chat ao vivo** · Esforço G
- Evidência: `be/src/ws/broadcaster.ts:59-65` (clientes, salas e donos em `Map`/`Set` locais); `be/src/ws/handler.ts:234-239` (o emit só alcança as salas do processo); `be/src/engine/dispatcher.ts:265-303` (o turno roda na réplica que fez o pump); `be/src/ws/run-channel.ts:10-12` (o código admite que o canal de thread assume turno e socket no mesmo processo).
- Impacto: com 2 réplicas, quem está conectado na outra vê o turno "congelado" até um F5 ou o polling recuperar. Escalar a API, o caminho para alto volume, quebra o tempo real de forma intermitente.
- Recomendação: salas distribuídas (socket.io + Redis adapter) ou publicar os eventos de turno no Redis e assinar em todas as réplicas, como já faz o runbus, mantendo o gate de tenant DM-17.
- Revisão: latente hoje (`compose.prod.yml` sem réplicas); bloqueia a escala horizontal.

**[alta · confirmado; contestado entre dimensões] `tempo-real-busy-local` — `isBusy` enxerga só a réplica local, mas alimenta REST, handshake e árvore** · Esforço M
- Evidência: `be/src/engine/registry-redis.ts:154` (`isBusy: (threadId) => local.has(threadId)`); consumidores em `be/src/routes/threads.ts:114`, `be/src/routes/agents.ts:480, 541` e `be/src/ws/handler.ts:419, 467`; `be/src/engine/reaper.ts:23-26, 153, 181` (o reaper usa o mesmo `isBusy`, e o comentário admite que só vale com 1 processo).
- Impacto: com 2 ou mais réplicas, o "rodando" da sidebar, da lista e do handshake oscila ou some; o polling do subagente pode parar cedo; o reaper de uma réplica pode declarar órfão um turno vivo em outra.
- Recomendação: derivar o `busy` do lock distribuído (EXISTS na chave do Redis), com fallback local se o Redis falhar.
- Revisão: o revisor desta dimensão manteve alta; o da dimensão de dados rebaixou para média (o doc oficial classifica como médio) e não o tratou como bug ativo. Há mitigação parcial no front só para o rótulo da árvore.

**[alta · confirmado] `tempo-real-sessao-outra-replica` — Sessão encerrada fecha sockets só na réplica local** · Esforço P
- Evidência: `be/src/auth/session.ts:104-113` (o comentário admite "só neste processo"); `be/src/plugins/websocket.ts:74-92` (mapa local de sockets por sessão); `be/src/ws/handler.ts:452-460, 486-559` (identidade fixada no handshake e nunca revalidada); `fe/src/api/ws.ts:470`.
- Impacto: logout ou revogação com 2 ou mais réplicas deixa sockets abertos nas outras, recebendo eventos da conta; com tráfego ativo, por bem mais de 1 minuto.
- Recomendação: publicar o fim de sessão no Redis para cada réplica fechar os seus sockets; revalidar a sessão periodicamente no socket.

**[alta · confirmado] `tempo-real-watchdog-reconecta-ocioso` — Toda aba ociosa reconecta cerca de 1 vez por minuto** · Esforço P
- Evidência: `fe/src/api/ws.ts:469-498` (sem frame em 60s, fecha) e `527-536, 555, 575-593` (reconecta com o backoff zerado); `be/src/ws/busy-heartbeat.ts:14, 25-32` (o único batimento só existe com a thread ocupada); `be/src/ws/handler.ts:131-133` (o comentário admite o ciclo); o servidor não emite ping.
- Impacto: socket ocioso é derrubado e reaberto a cada ~61s, para sempre, em todas as abas, cada vez com handshake completo (events, config, replay). O servidor não distingue cliente caído de cliente ocioso.
- Recomendação: ping/pong real ou keepalive barato vindo do servidor; pausar o watchdog com a aba oculta; no socket.io, usar o heartbeat nativo.

**[alta · confirmado] `tempo-real-handshake-repete-historico` — Cada (re)conexão relê a thread inteira e a reenvia para o cliente descartar** · Esforço M
- Evidência: `be/src/ws/handler.ts:396` (`getEvents` sem limite a cada abertura), `302-304` e `429` (replay de tudo); `be/src/persistence/events.ts:247-250` ("por padrão, TUDO"); `fe/src/features/thread-runtime/model/runtime.ts:139-140` (o front ignora o replay não inflight); `use-thread-runtime.ts:11, 107-109` (o REST da reconexão é paginado em 100).
- Impacto: a cada reconexão — cerca de 1 por minuto por aba ociosa — o servidor relê e serializa a thread inteira para o cliente jogar fora; em threads longas (o código cita 15 mil+ events), megabytes por minuto por aba. O custo é do servidor; a UI não trava (ver o refutado em 3.9).
- Recomendação: handshake magro (`connected` + buffer vivo + `seq`); histórico 100% por REST; replay só do que vier depois do `seq` informado pelo cliente.

**[alta · confirmado] `tempo-real-dialog-estoura-ratelimit` — Modal de subagente e lista estouram o teto de 30 req/min por IP com uma aba** · Esforço P
- Evidência: `fe/src/components/subagent-sidebar/model/use-subagent-events.ts:30` (2s = 30/min); `fe/src/features/subagent/model/queries.ts:106-120` (status a cada 3s = 20/min); `fe/src/features/thread/model/queries.ts:39` (lista a cada 8s = 7,5/min); `be/src/routes/threads.ts:88-92, 102, 630, 783` (essas rotas têm 30 req/min por IP); `be/src/server/index.ts:181-206` (chave por IP); `be/src/routes/workflows.ts:198-202` (o código admite que o certo é limitar por token).
- Impacto: com uma aba e o modal aberto num subagente rodando, só as rotas com teto de 30/min recebem ~57 req/min; somando árvore e graph, ~90 req/min por aba. Parte dos polls toma 429 e o retry do cliente reinjeta a request. Atrás de NAT, poucas pessoas se derrubam umas às outras.
- Recomendação: curto prazo, espaçar o modal (2s → 4s) e/ou subir o teto dessas rotas; estrutural, push pelo WS (F3) e limite por token ou sessão em vez de IP.
- Revisão: o quadro é pior que o descrito no achado; o cliente usa `retry: 1` (não 3).

**[alta · confirmado] `tempo-real-runs-lista-explosao` — A tela de runs dispara até 11 queries a cada 5s por aba** · Esforço M
- Evidência: `fe/src/features/runs/components/RunsListPage.tsx:142-148` (até 11 hooks); `fe/src/features/run/model/queries.ts:60-66, 87-95` (5s com run vivo; 1 status = 1 request); `be/src/routes/workflows.ts:505-582` (cada listagem faz findMany + count + groupBy) e `109-123` (o backend já aceita vários status numa chamada); `workflows.ts:204` (600 req/min por IP).
- Impacto: no recorte "ativos", até 132 req/min por aba, cada uma com 2–3 consultas ao Postgres; poucas abas no mesmo IP chegam ao teto; com 100 operadores, ~13 mil req/min só nessa tela. Para quando não há run vivo.
- Recomendação: unificar contadores e lista numa query só (o backend já aceita lista de status); depois, push de `run.status` (F4).

**[alta · confirmado] `tempo-real-events-canal-sem-ouvinte` — Todo evento persistido é publicado num canal Redis que ninguém assina** · Esforço P
- Evidência: `be/src/persistence/events.ts:105-113` (`publish` do JSON inteiro dentro do MULTI); `be/src/persistence/keys.ts:60-62`; os únicos assinantes do backend são `runbus:*` (`be/src/workflows/bus.ts:142`) e `launcher-wake` (`be/src/ws/handler.ts:259`), além de um smoke de teste.
- Impacto: cada `appendEvent` paga serialização e publish sem consumidor, no caminho mais quente do motor.
- Recomendação: remover o publish até haver consumidor, ou usá-lo como base do broadcast distribuído (um assinante por réplica alimentando as salas).

**[alta · confirmado] `tempo-real-mapas-sem-teto` — Mapas em memória sem expiração crescem com o uso** · Esforço P
- Evidência: `be/src/ws/handler.ts:212` (`rootOf` nunca apaga); `be/src/ws/busy-owner.ts:34, 41, 46` (`owners` nunca apaga); `be/src/engine/registry-redis.ts:72, 178-180` (`convoCache` sem TTL; só sai quando fecha o último socket); `registry-redis.ts:216, 254, 257` (espelho do turno de outra réplica); `docs/motor-agent-prod-readiness/api.html:484-488` (o doc já recomenda LRU com TTL).
- Impacto: memória cresce por thread tocada até o restart, com dados velhos (dono e conversa de horas atrás).
- Recomendação: TTL ou LRU nesses mapas e limpeza do espelho quando o lock sumir.

**[alta · confirmado] `tempo-real-dedupe-so-local` — Dedupe de envio não vale entre réplicas; a mailbox não tem unique** · Esforço M
- Evidência: `be/src/ws/handler.ts:177-205` (`seenClientMessageIds` em memória) e `656-658` (esquecido no close); `be/src/engine/mailbox-db.ts:87-95` (insert sem checagem); `be/prisma/schema.prisma:329-356` (`MailboxItem` sem coluna nem unique de `clientMessageId`); `docs/api-terceiros.md:595-596` (problema já conhecido).
- Impacto: envio → queda → reconexão em outra réplica (ou depois de um restart) enfileira a mesma mensagem de novo: turno duplicado e o usuário paga duas vezes; o dedupe do front só esconde a duplicata na tela.
- Recomendação: coluna `clientMessageId` com unique `(threadId, clientMessageId)` e insert tolerante a conflito, ou checagem no Redis antes do push.

**[alta · confirmado] `tempo-real-protocolo-sem-versao` — Protocolo sem versão: deploy be/fe acoplado e eventos novos invisíveis** · Esforço M
- Evidência: `fe/src/api/ws.ts:242-259, 360-365` (tipo desconhecido vira `null` e uma métrica que ninguém lê); `fe/src/features/thread-runtime/model/runtime.ts:207-215` (switch exaustivo); `be/src/engine/turn-runner.ts:617, 919, 1068, 1157, 1165` (emite `compaction_failed`, `persistence_warning` e `turn_empty`, que o front não conhece); `fe/src/features/run/model/events.ts:139-150` (o parser do run só faz cast); nenhum campo de versão no envelope.
- Impacto: backend à frente do front = eventos descartados em silêncio (3 tipos já são); deploy independente de be e fe fica arriscado; frame malformado passa no canal de run.
- Recomendação: campo `v` nos frames, catálogo único be/fe com regra aditiva, validação no parser do run e `unknownType` levado à observabilidade.

**[alta · confirmado] `tempo-real-metricas-cegas` — O tempo real só conta sockets** · Esforço P
- Evidência: `be/src/observability/metrics.ts:106-121` (só o gauge `ws_clients`); `be/src/routes/metrics.ts:19-23`; `be/src/routes/health.ts:115-127`; nenhum contador em `ws/handler.ts`, `ws/broadcaster.ts` ou `http/ws-protocol.ts`.
- Impacto: não se veem salas ativas, eventos por tipo, reconexões, bytes de replay, descartes nem tipos desconhecidos — operar volume e provar a migração fica no escuro.
- Recomendação: contadores de frames por tipo e canal, conexões e reconexões, bytes de replay, descartes e tipos desconhecidos; gauges de salas, sockets por sala e listeners do runbus.

**[alta · confirmado] `tempo-real-busy-travado-desconexao` — A badge "rodando" pode ficar acesa depois de uma queda** · Esforço P
- Evidência: `fe/src/components/thread-sidebar/model/use-thread-busy.ts:36, 56-61, 84-87, 123-177` (snapshot singleton que só muda com evento novo); `fe/src/components/thread-sidebar/ui/ThreadList.tsx:222` (o overlay vence o valor do REST); `fe/src/api/ws.ts:575-593` (o close não limpa); `be/src/ws/busy-owner.ts:52` (um evento por thread, sem reenvio do estado na reconexão).
- Impacto: o turno que termina durante a queda nunca envia `busy=false`; a badge âmbar fica acesa até a thread rodar de novo.
- Recomendação: reconciliar o snapshot com o `busy` do REST a cada lista (o overlay só vence se for mais novo) e limpar no close.

**[média · confirmado; sev. contestada alta→média] `tempo-real-stop-abort-multi` — "Parar" entre réplicas não cancela e ainda marca `aborted` no banco** · Esforço P
- Evidência: `be/src/engine/registry-redis.ts:156-167` (publica e devolve `false`); `be/src/routes/agents.ts:575-592` e `be/src/engine/orchestrator.ts:456-464` (`abortChild` marca `aborted` mesmo com `ok=false`); `be/src/ws/handler.ts:599-612` (o stop sem efeito só gera um log); `orchestrator.ts:297-321`.
- Impacto: se o Parar cair em outra réplica, o turno não cancela e a API responde `ok:false`, mas a thread fica `aborted` no banco enquanto roda: a árvore mente e o gasto continua.
- Recomendação: assinar `<prefixo>:abort` em todas as réplicas e só marcar `aborted` depois que o abort confirmar; enquanto isso, documentar que o Parar exige sticky.
- Revisão: rebaixado por ser latente (1 `be` e 1 `worker` em produção); vira alta no scale-out. Ver o tema transversal em 3.0.

**[média · confirmado; sev. contestada alta→média] `tempo-real-fanout-filho-sem-raiz` — O progresso do filho nunca chega à sala do pai** · Esforço P
- Evidência: `be/src/ws/handler.ts:212, 234-240` (o fan-out só acontece se `rootOf` conhecer a thread) e `266, 377, 565` (só a thread do próprio socket entra no cache); `be/src/engine/orchestrator.ts:236-285` (o spawn não popula); `fe/src/features/thread-runtime/model/timeline-live.ts:232-260` e `runtime.ts:203-206`.
- Impacto: o desenho D3 (o pai enxerga o filho trabalhando) não acontece: parciais e tools do filho não chegam à sala do pai; só `spawn` e `result`. O modal paga polling de 2s.
- Recomendação: popular a raiz no spawn; na migração, o pai assina a sala da família.
- Revisão: rebaixado porque o fluxo funcional está intacto e o polling de 2–3s foi aceito no doc de decisão (bn-subagent, achado 18).

**[média · confirmado; sev. contestada alta→média] `tempo-real-polling-sem-cache` — O polling lê o Postgres direto** · Esforço M
- Evidência: `be/src/persistence/threads.ts:269-316` (lista e contagem direto no Prisma); `be/src/routes/workflows.ts:618-626, 667-671` (passo e eventos do run direto). Mitigações achadas pelo revisor: `threads.ts:165-172` (`getThread` com cache), `events.ts:251-330` (janela quente no Redis), polls condicionais e pausados com a aba oculta, rate limits e índices.
- Impacto: picos de operadores viram picos de queries no Postgres; o custo cresce com abas × telas.
- Recomendação: push pelo WS (F2–F4); paliativo com cache curto (2–5s) por (org, usuário, rota) no Redis.

**[média · confirmado; sev. contestada alta→média] `tempo-real-socket-busy-duplica` — O socket da sidebar recebe tudo para usar só o `thread_busy`** · Esforço M
- Evidência: `fe/src/components/thread-sidebar/model/use-thread-busy.ts:82-88` (abre socket na thread ativa) e `85-86` (só consome `thread_busy` e `connected`); `be/src/ws/handler.ts:385, 429` (todo socket entra na sala e recebe o replay); `be/src/ws/broadcaster.ts:129-151`; `fe/src/features/playground/components/PlaygroundPage.tsx:36, 193`.
- Impacto: cada aba do playground mantém 2 sockets na mesma thread, com frames ao vivo e handshake em dobro. O socket do runtime já recebe o `thread_busy` de todas as threads do dono; o segundo é redundante.
- Recomendação: um socket por aba servindo runtime e sidebar, ou sala `user:` sem entrar na thread.

**[média · confirmado; sev. contestada alta→média] `tempo-real-runbus-psubscribe` — Cada réplica recebe os frames de todos os runs** · Esforço M
- Evidência: `be/src/workflows/bus.ts:142` (`psubscribe` global), `18-26` (o comentário admite) e `124-133` (descarta antes do parse quando não há ouvinte local).
- Impacto: o tráfego Redis → API cresce com runs × réplicas; hoje, com 1 réplica, é 1x.
- Recomendação: subscribe por run com contagem de referência (o próprio comentário aponta) ou roteamento por sala no adapter do socket.io.

**[média · confirmado; sev. contestada alta→média] `tempo-real-sem-acks-perdas` — Sem ack nem sequência: o que se perde numa queda** · Esforço M
- Evidência: `be/src/persistence/types.ts:97-123` (não existe tipo persistido para `final` e `error`); `be/src/engine/turn-runner.ts:1232-1245` (limpa o ao vivo antes do `final`); `be/src/ws/handler.ts:302-368, 421-425`; `fe/src/features/thread-runtime/model/timeline-merge.ts:90-95, 144-158`; `be/src/engine/registry.ts:133-144`.
- Impacto: numa queda entre o último resultado e o `final`, o conteúdo se recupera, mas somem da timeline `elapsedMs`, `steps`, `usage` e `historyLength` do turno; a cauda do parcial do run some até o próximo frame. Não há perda de dado do usuário.
- Recomendação: `seq` por sala e replay de janela na reconexão; incluir `final` e `error` no buffer vivo ou persistir o resumo do `final`.

**[média · confirmado; sev. contestada alta→média] `tempo-real-backoff-sem-jitter` — Reconexão sem jitter: todas as abas voltam juntas no deploy** · Esforço P
- Evidência: `fe/src/api/ws.ts:527-536` (1s × 2^n, teto de 15s, sem aleatoriedade); `be/src/ws/handler.ts:392-443` (cada reconexão faz `getEvents`, config, replay e buffer vivo). Mitigações: cache quente de events; `loadEffectiveConfig` só vai ao banco quando há setup; rate limit global.
- Impacto: num restart, as abas reconectam nos mesmos instantes, em rajadas contra Redis e Postgres.
- Recomendação: jitter de ±50% no atraso; o handshake magro reduz o custo de cada tentativa.

**Não verificados (baixa e info):**

| Achado | Sev. | Esf. | Evidência | Impacto | Recomendação |
|---|---|---|---|---|---|
| `tempo-real-health-por-aba` — saúde a cada 30s por aba consulta Postgres e Redis | baixa | P | `fe/src/features/system/model/queries.ts:17-32`; `be/src/routes/health.ts:90-113` | carga evitável e mistura probe de infra com status de UI | F5: derivar "backend offline" do socket e dos erros REST; deixar `readyz` para o orquestrador |
| `tempo-real-chat-sem-streaming-token` — o chat emite parcial por step | info | M | `be/src/sinks/broadcast.ts:65-105`; `be/src/agent/loop.ts:218-227, 514-550`; `be/src/engine/agent-step.ts:621-687` | resposta longa de step único chega em blocos | na F6, ligar `onTextDelta` no chat com evento versionado e coalescência, como no run |
| `tempo-real-sse-so-vendor` — nenhum SSE na camada do app | info | P | busca em `fe/src` e `be/src`; `be/src/agent/loop.ts:450-451` | "sem SSE" já é verdade; a migração só precisa eliminar o polling | manter a proibição de SSE no app |
| `tempo-real-tts-voz-rest` — voz e TTS não têm polling próprio | info | P | `fe/src/features/voice-output/model/speech-player.ts:451`; `use-conversation.ts:198` | fora da migração de polling | nenhuma ação |
| `tempo-real-run-cauda-efemera` — a cauda do parcial do run some na reconexão | info | M | `be/src/workflows/bus.ts:12-16`; `fe/src/features/run/model/runtime-reducer.ts:63-81` | passo em andamento fica sem o texto parcial até o próximo frame (por desenho) | se quiser zero perda visível, guardar os últimos parciais por passo no Redis com TTL curto |
| `tempo-real-comentario-run-stale` — comentário diz que o backend "não serve `?run=`" | info | P | `fe/src/features/run/model/channel.ts:39-41`; `be/src/plugins/websocket.ts:122-130` | confunde o planejamento; o canal existe | atualizar o comentário |

**Métricas medidas (tempo real):**
- Produção (`motor-be-ws`, janela de 7,5 min): 20 requests — `/healthz` ×16, `/api/threads?limit=50` ×2 (intervalo de 8,38s = polling de 1 aba), `/tree` ×1, `/readyz` ×1; total de 2,66 req/min. Produção estava ociosa: não há base para medir polling sob carga.
- Polling por aba visível, pior caso: lista de threads 7,5/min; árvore 20/min + graph 20/min (com filho rodando); modal de subagente 30/min + 20/min; lista de runs no recorte "ativos" 132/min; log 15/min; passo 20/min; saúde 2/min.
- Projeção: 100 abas no playground com subagentes rodando ≈ 3.000 req/min só de polling, cada uma com 1–3 queries sem cache.
- Rate limits: global de 100/min por IP; rotas de thread com 30/min por IP; leituras de run com 600/min por IP.
- WS: frame de até 8 MiB e imagem de até 2 MiB; watchdog do cliente de 60s (checagem a cada 15s); batimento de busy de 25s; backoff de 1s × 2^n com teto de 15s, sem jitter; fila de 100 itens / 256 KB.
- Sockets por aba: playground com thread ativa = 2; +1 por tela de run aberta; dashboard = 0 (só polling).
- Run: parcial de passo fora de foco até 2 Hz, com coalescência; até 8 passos em foco; handshake = 1 run + N passos + 1 groupBy.
- Buffer vivo: guarda 9 dos ~18 tipos emitidos; TTL de 900s. Eventos que o backend emite e o front descarta: `compaction_failed`, `persistence_warning`, `turn_empty`; `subagent_progress` está no buffer, mas não tem emissor.


### 3.3 Tools e system prompt

**[alta · confirmado] `tools-prompt-snapshot-com-segredos` — Snapshot da config com segredos em claro vai para Postgres e Redis a cada turno** · Esforço P
- Evidência: `be/src/engine/turn-runner.ts:364-365` (`configSnapshot = effCfg`, sem redação); `be/src/persistence/turns.ts:120-127` (grava em `turns.config_snapshot`) e `19-36, 140-143` (copia o Turn para o Redis, TTL de 1800s); `be/src/config/thread-config.ts:304, 332, 388-394` (a config efetiva carrega `basic.apiKey`, token/`Authorization` de MCP e `compactor.apiKey`); `be/src/config/redact.ts:44-73` (a redação existe, mas só nas respostas HTTP e WS).
- Impacto: cada turno espalha chaves do proxy e tokens de MCP por tabela, cache, backups e réplicas; qualquer leitura ampla do banco ou do Redis expõe credenciais.
- Recomendação: persistir `redactConfig(effCfg)` e calcular o `configHash` sobre a config completa antes de redigir; segredos ficam só em `OrgConfig` e `Agent`.
- Revisão: a única rota que hoje serializa o Turn para o cliente não devolve o snapshot — isso atenua a exposição externa, não a persistência.

**[alta · confirmado] `tools-prompt-ssrf-mcp-test` — Teste de MCP e MCPs por thread aceitam URL arbitrária sem trava SSRF** · Esforço M
- Evidência: `be/src/routes/mcp.ts:11-12, 22, 35-47` (`POST /api/mcp/test` conecta em qualquer URL do corpo, sem checagem de papel, e devolve a mensagem de erro do destino); `be/src/tools/mcp-client.ts:220-233` (`connectMcp` não usa o guard); `be/src/config/thread-config.ts:34-49, 71-79` (`ExtraMcpSchema` aceita qualquer URL, inclusive `http://`); `be/src/routes/threads.ts:139-140` (o PATCH da thread não exige admin); `be/src/net/ssrf.ts:38-39` (o próprio módulo diz que MCP não passa por ele).
- Impacto: qualquer membro autenticado faz o backend conectar em hosts internos (metadata da nuvem, serviços da VPC), pelo endpoint de teste ou a cada turno via MCP da thread; e pode apontar um MCP malicioso que injeta instruções.
- Recomendação: aplicar a guarda de `be/src/net/ssrf.ts` no `connectMcp` e no `/api/mcp/test`; considerar exigir admin para cadastrar MCP novo.
- Revisão: o vetor fala protocolo MCP (handshake JSON-RPC), não HTTP cru; a exfiltração passa por respostas e mensagens de erro. Ver `seguranca-ssrf-02` e `04` em 3.7.

**[alta · confirmado] `tools-prompt-descricao-mcp-verbatim` — Descrição e schema de MCP entram verbatim no prompt, sem limite** · Esforço M
- Evidência: `be/src/tools/mcp-client.ts:280-284` (descrição e `inputSchema` entram sem truncar, sanitizar ou marcar a origem); `be/src/tools/registry.ts:74-84` (em colisão, o último MCP vence); `thread-config.ts:71-79` (qualquer thread aceita MCP por URL). Mitigações parciais: nativas vencem colisão (`registry.ts:87-96`); allowlist e `toolPolicy` opcionais.
- Impacto: qualquer MCP configurado — inclusive um que um membro adicionou à própria thread — tem um canal permanente de instrução ao modelo em todo step: exfiltração, sequestro de tools vizinhas e injeção persistente.
- Recomendação: teto de tamanho para descrição e schema; prefixo de origem (`[mcp:servidor]`) nas descrições; allowlist e pin de servidores por org, com revisão de tools novas.

**[alta · confirmado] `tools-prompt-notificacao-role-user` — Resultado de subagente ou workflow entra como `user`, com moldura de autoridade** · Esforço M
- Evidência: `be/src/engine/mailbox.ts:232-239` (a notificação vira `role: 'user'`); `mailbox.ts:245-290, 292-322` (o texto do filho ou do workflow entra dentro de `[NOTIFICAÇÃO DO SISTEMA]`, sem marca de dado não confiável); `be/src/engine/orchestrator.ts:340-343` (texto cru do filho, sem corte); `be/system-prompt.md:91-126` (nenhuma instrução de que notificação é dado, não ordem).
- Impacto: texto gerado por outro agente — possivelmente envenenado por uma tool comprometida no filho — chega ao pai com autoridade de "notificação do sistema": canal clássico de injeção indireta entre agentes, num pai com tools poderosas.
- Recomendação: delimitar o conteúdo do filho como não confiável; instruir no prompt nativo que conteúdo de notificação é dado; cortar o `finalText` com referência para o detalhe.
- Revisão: o caminho de workflow já corta em 6 mil chars (`be/src/workflows/notify-launcher.ts:57-61`); o de subagente, não.

**[alta · confirmado] `tools-prompt-maxsteps-999` — Default de 999 steps por turno permite custo descontrolado** · Esforço P
- Evidência: `be/src/agent/loop.ts:508`; `be/src/engine/dispatcher.ts:158-170`; `be/src/engine/agent-step.ts:179-180, 689`; `be/src/config/schemas.ts:289-301, 386`; `be/src/engine/cost-tracker.ts:361-365` (orçamento ausente = sem teto).
- Impacto e recomendação: os mesmos de `loop-sdk-maxsteps-999` (3.1). O revisor acrescenta que 999 steps também seguram fila e worker por horas.

**[alta · confirmado] `tools-prompt-tudo-para-o-modelo` — O padrão registra todas as tools de cada MCP** · Esforço G
- Evidência: `be/src/config/schemas.ts:252-253` (`tools` opcional; ausente = todas); `be/src/tools/registry.ts:32` (sem filtro, devolve tudo); `be/src/persistence/agents.ts:228-243` e `be/prisma/schema.prisma:100` (o setup padrão nasce sem `toolPolicy`); `be/src/engine/turn-runner.tool-policy.test.ts:92-101` (um teste protege o "tudo" como padrão); logs de produção: 76 + 70 + 193 de MCP + 23 nativas = 362 tools por turno, sem nenhuma linha de "tools selecionadas".
- Impacto: prefixo de dezenas de milhares de tokens em todo step, mais latência, mais custo de escrita de cache e mais chance de escolher a tool errada.
- Recomendação: curadoria por setup (`toolPolicy` preenchida no setup padrão) e núcleo mínimo com busca de tools sob demanda, negando por padrão.
- Revisão: o número exato varia com os MCPs conectados (o briefing citava 534 antes das colisões); a ordem de grandeza — centenas por turno — está confirmada.

**[média · confirmado; sev. contestada alta→média] `tools-prompt-colisao-mantem-ultimo-mas-avisa-primeiro` — O aviso de colisão diz o contrário do que o código faz** · Esforço M
- Evidência: `be/src/tools/registry.ts:76-82` (o aviso diz que manteve o primeiro e descartou o atual, mas `collected[name] = t` mantém o último); `registry.ts:6-7` (o cabeçalho diz, corretamente, que o segundo sobrescreve); `be/src/agent/bootstrap.ts:134-160` (a ordem é a de `cfg.mcps`).
- Impacto: com nomes repetidos entre MCPs, a tool que executa é a do último da lista e o log diz o contrário: depuração invertida, e um MCP pode tomar as tools de outro.
- Recomendação: namespace `servidor__tool` com alias curto; até lá, "o primeiro vence" (coerente com o aviso) ou recusar no boot quando os schemas diferirem.
- Revisão: rebaixado porque é determinístico pela ordem da config, há aviso a cada colisão (só o texto está invertido) e existe allowlist opcional.

**[média · confirmado; sev. contestada alta→média] `tools-prompt-sem-limite-resultado` — Resultado de tool sem teto** · Esforço M
- Evidência: `be/src/tools/mcp-client.ts:305-310` (junta todo o texto sem cortar); `be/src/engine/orchestrator.ts:340-343` (`finalText` sem corte). Mitigações: `be/src/workflows/notify-launcher.ts:57-61` (workflow corta em 6 mil chars); `be/src/agent/history.ts:102-119` e `be/src/agent/compaction.ts:128-129` (a compactação limita o acúmulo entre turnos).
- Impacto: um arquivo grande, um log verboso ou um filho prolixo entram inteiros no contexto de todos os steps seguintes do turno; a compactação só age no turno seguinte (TURN-12).
- Recomendação: teto configurável por resultado (ex.: 20–50 mil chars) com marcador `[TRUNCADO]` e uma tool nativa para ler o resultado completo em páginas.
- Revisão: rebaixado porque o acúmulo entre turnos é limitado; o estrago fica em um turno por conversa.

**[média · confirmado; sev. contestada alta→média] `tools-prompt-estimativa-ignora-tools` — A estimativa de contexto ignora o bloco de tools** · Esforço P
- Evidência: `be/src/engine/turn-runner.ts:1566-1583` (`estimateContextTokens` soma system e messages, sem tools) e `1180-1181` (fallback quando o proxy não devolve usage).
- Impacto: sem usage do provider, a ocupação medida erra dezenas de milhares de tokens para baixo e a compactação dispara tarde.
- Recomendação: somar o tamanho das tools (pré-calculado no bootstrap) e logar a divergência entre estimado e medido.
- Revisão: rebaixado porque o LB sempre devolve usage no caminho feliz; o fallback só roda com o proxy degradado.

**[média · confirmado; sev. contestada alta→média] `tools-prompt-args-sem-validacao` — Argumentos de tools nativas não são validados** · Esforço M
- Evidência: `be/src/tools/native/subagent.ts:175-176` (`String(args.title)` e `String(args.task)` viram "undefined" e o spawn acontece); `be/src/engine/orchestrator.ts:239-246` (sem validação); `@ai-sdk/provider-utils` (`jsonSchema()` sem `validate` aprova tudo). Contraexemplo bom: `be/src/tools/native/workflow.ts:300-330` (as tools `workflow_*` recusam argumento ruim).
- Impacto: o `required` dos schemas é decorativo para JSON válido com campo faltando: `agent_spawn` cria filho com tarefa "undefined", gastando modelo e poluindo a árvore.
- Recomendação: validar `required` e tipos no início de cada `execute` nativo (helper zod compartilhado) e recusar com erro legível.
- Revisão: o mecanismo original estava errado (`parseToolArgs` só alimenta a UI e JSON quebrado o SDK já recusa — ver `loop-sdk-parse-args` em 3.9), mas o problema de fundo se confirmou por este caminho.

**Não verificados (média, baixa, info):**

| Achado | Sev. | Esf. | Evidência | Impacto | Recomendação |
|---|---|---|---|---|---|
| `tools-prompt-system-via-messages` — system dentro de `messages` gera warning do SDK | média | P | `be/src/agent/messages.ts:77-79`; `be/src/agent/loop.ts:499-505`; log de produção | hoje é ruído (o SDK converte), mas o SDK indica que o caminho pode endurecer | usar a opção `system` com o mesmo cache, ou `allowSystemInMessages` explícito |
| `tools-prompt-system-mente-builtin` — o prompt diz que as tools são "built-in, sem MCP externo" | média | M | `be/system-prompt.md:19`; `be/src/agent/bootstrap.ts:120-173` | o modelo aprende um mapa falso e gasta steps em tools ausentes quando um MCP cai ou a política restringe | gerar a seção de ferramentas a partir das tools montadas no turno |
| `tools-prompt-duplicacao-prompt-tools` — mesmas regras no prompt, nas descrições e nos resultados | média | M | `be/system-prompt.md:89-274`; `be/src/tools/native/subagent.ts:94-105`; `setup.ts:249-266`; `workflow.ts:603-635, 1270-1277` | cada palavra duplicada é paga em todo request e há três lugares para manter sincronizados | fonte única: detalhe na descrição da tool, prompt como índice curto, resultados só com fatos |
| `tools-prompt-append-concat-vs-substitui` — a thread soma ou substitui o apêndice da org? | média | P | `be/src/config/schemas.ts:333-335` (soma) × `be/src/config/thread-config.ts:208-210, 417-420` (substitui) | quem configura a thread acha que preserva as regras da org e as apaga | decidir a semântica (sugestão: org + setup + thread concatenados e rotulados) e alinhar comentários e UI |
| `tools-prompt-cache-defaults-divergem` — defaults de cache do schema × contrato do LB | média | P | `be/src/config/schemas.ts:27-28, 356-357` × `be/docs/contrato-load-balancer.md:41-45, 67`; `be/chat-config.json:22-31` | orgs criadas fora do seed plantam breakpoints inúteis | alinhar os defaults ao contrato e corrigir o comentário de `markLastAssistant` |
| `tools-prompt-toolpolicy-colisao` — `toolPolicy` por servidor pode liberar a tool do servidor errado | média | M | `be/src/tools/registry.ts:74-83, 139-145` | a política não garante a origem que o admin imagina | resolver com namespace: a política cita `servidor` ou `servidor__tool` |
| `tools-prompt-cache-creation-ignorado` — escrita de cache fora do custo | média | M | `@ai-sdk/anthropic` expõe `cacheCreationInputTokens`; `loop.ts:307-313`; `be/src/engine/response-events.ts:98-110`; `be/src/engine/cost-tracker.ts:86-91` | custo real subestimado no orçamento | igual a `loop-sdk-usage-cache-creation`, confirmado em 3.1 |
| `tools-prompt-sem-painel-inspecao` — não dá para ver o que foi enviado em cada step | média | G | `be/src/provider/anthropic.ts:221-227` (só contagens); `turn-runner.ts:364`; `be/src/routes/threads.ts:605-627` | todo debug de "por que o modelo fez X" vira reconstrução mental; divergências de cache ficam invisíveis | persistir por turno o hash e o tamanho de cada camada, as tools finais e os breakpoints; expor em endpoint e aba de debug (5.3) |
| `tools-prompt-persona-herdada-perdida` — subagente com setup herdado perde a persona | média | P | `thread-config.ts:500`; `orchestrator.ts:202-223` | o filho responde fora do papel esperado, em silêncio | injetar a persona herdada ou documentar e avisar no `setup_list` |
| `tools-prompt-thread-append-apaga-persona` — apêndice na thread apaga a persona inteira | média | M | `thread-config.ts:444-447, 512` | um ajuste pequeno remove toda a persona do setup | concatenar rotulado em vez de tudo-ou-nada; até lá, avisar na UI |
| `tools-prompt-setup-update-reenvio` — `setup_update` exige reenviar o override inteiro | média | M | `be/src/tools/native/setup.ts:208-215, 421-424` | caro em tokens e propenso a apagar campos ou segredos (`•••`) | semântica de patch no serviço, com `clearSecrets` explícito |
| `tools-prompt-warpgrep-fantasma` — o prompt cita a tool `warpgrep`, que não existe | baixa | P | `be/system-prompt.md:85` | o modelo tenta chamar tool inexistente justo quando está travado | remover ou mapear para a tool real |
| `tools-prompt-pool-key-comentario` — comentário diz que o token renovado entra na chave do pool | baixa | P | `mcp-client.ts:223-227`; `schemas.ts:239-242`; `be/src/tools/mcp-pool.ts:65-70` | doc enganosa (a rotação funciona por invalidação no 401) | corrigir os comentários ou pôr o hash do token na chave |
| `tools-prompt-readfile-por-turno` — o system prompt é lido do disco a cada turno | baixa | P | `be/src/agent/bootstrap.ts:66-70` | I/O de ~26 KB por turno e por subagente | cache em memória invalidado por mtime |
| `tools-prompt-midturn-ordem` — notificações vistas intercaladas e persistidas no fim | baixa | M | `turn-runner.ts:932-935, 1040-1047` | depois de um restart, a ordem reconstruída difere da vista pelo modelo | persistir a posição ou documentar a divergência |
| `tools-prompt-parsetoolargs-sem-teto` — parser de argumentos quadrático e sem teto | baixa | P | `loop.ts:56-64` | entrada patológica pode prender a CPU do event loop | teto de tamanho (ex.: 1 MB) antes do parse |
| `tools-prompt-voice-cache` — trocar o modo de voz invalida o cache do system | info | P | `bootstrap.ts:93-95`; `be/src/agent/voice-prompt.ts:88-120` | comportamento esperado | só documentar |
| `tools-prompt-unsupported-reasoning` — warnings de reasoning sem assinatura | info | P | `loop.ts:159-186`; log de produção | ruído de log; o SDK descarta os blocos | filtrar reasoning sem metadata em todos os caminhos ou silenciar depois de confirmar |
| `tools-prompt-activetools-nao-usado` — o SDK aceita `activeTools` por step e o Motor não usa | info | G | pacote `ai` (`PrepareStepResult`); `loop.ts:291-297` | não é problema: é a porta para carregar tools sob demanda | estender o `prepareStep` para devolver `activeTools` quando houver busca de tools |

**Métricas medidas (tools e prompt):**
- 23 tools nativas (4 de subagente, 4 de setup, 15 de workflow): 37.467 chars de JSON ≈ 9.367 tokens (chars/4; faixa de 7.494 a 12.489 entre chars/5 e chars/3).
- `workflow_validate` sozinha: 15.399 chars de JSON, com descrição de 14.665 chars (`workflow.ts:603-635`).
- `system-prompt.md`: 25.903 chars ≈ 6.476 tokens. Prefixo fixo sem MCP: ~63 mil chars ≈ 15,8 mil tokens por request.
- MCPs de produção citados no briefing: 195 + 193 + 76 + 70 = 534 tools antes das colisões; estimativa do bloco de MCP: 80–214 mil tokens (premissa de 150–400 tokens por tool), contra janela padrão de 230 mil.
- Breakpoints efetivos no seed: 2 de 4 (system e último user, ambos com TTL de 1h; tools sem breakpoint; checkpoint desligado).
- Timeouts: handshake e listagem 30s; chamada de tool 60s; pool ocioso 120s; watchdog de lease 60s.
- Compactação: transcript de 300 mil chars (40 mil de cabeça); tool result 600 chars; argumentos 400 chars; resumo de até 8 mil tokens.

### 3.4 Agente e subagentes

**[alta · confirmado por 2 revisores; sev. contestada crítica→alta] `subagentes-reaper-desligado` — Reaper desligado em produção: filho órfão trava o pai para sempre** · Esforço P
- Evidência: `be/src/engine/reaper.ts:67-69` (só liga com `REAPER_ENABLED === '1'`) e `302-308` (`start()` vira no-op); `reaper.ts:275-284` (o reconciler de notificações perdidas só roda dentro do reaper); `be/src/plugins/websocket.ts:64` (único `start()`); `be/src/workflows/register.ts:63-64` (o worker não liga); `docs/operacao.md:134` (DESLIGADO); a lista de variáveis dos dois serviços de produção não tem `REAPER_ENABLED`; `be/src/engine/orchestrator.ts:297-321` (`onTurnEnd` é o único caminho de notificação do pai).
- Impacto: crash, OOM ou kill no meio de um turno deixa a thread do filho `active` e o turno `running` para sempre; o pai espera uma notificação que nunca chega, sem erro visível. A rede do BE-ERR-09 fica desligada junto.
- Recomendação: ligar `REAPER_ENABLED=1` em exatamente uma réplica (ou padrão ligado com liderança por lock) e alertar no boot se nenhuma réplica assumiu.
- Revisão: rebaixado de crítica para alta porque o boot registra o aviso, o pai não entra em deadlock técnico (o usuário pode seguir em outra thread) e o opt-in é decisão documentada (P-20), para evitar várias réplicas varrendo sem liderança.

**[alta · confirmado] `subagentes-fanout-ilimitado` — Largura e profundidade ilimitadas, sem orçamento por árvore** · Esforço M
- Evidência: `be/src/engine/orchestrator.ts:22-24` (D4: profundidade e largura ilimitadas; `depth` é só telemetria) e `234-235`; `be/src/engine/org-concurrency.ts:36-39, 50` (teto de 100 por processo, "best-effort", multiplica por réplica); `be/src/tools/native/subagent.ts:38-62, 72-80` (o próprio código admite que não há teto para gasto que sobrevive ao run — por isso a tool é negada em workflow).
- Impacto: um pedido inocente ("paraleliza isso") abre N filhos que abrem netos, todos rodando turnos de LLM sem orçamento por árvore; as defesas existentes são por org e reativas, e o orçamento é mensal.
- Recomendação: teto configurável de largura por pai e de profundidade, com erro legível que oriente a consolidar; e/ou orçamento de tokens por árvore passado no spawn.

**[alta · confirmado] `subagentes-notificacao-sem-teto` — O resultado do filho volta inteiro, sem corte nem condensação** · Esforço P
- Evidência: `be/src/engine/orchestrator.ts:340-343, 345-358` (`finalText` cru); `be/src/engine/mailbox.ts:232-239, 292-323` (vai inteiro para a mensagem do pai); `be/src/engine/turn-runner.ts:744-758, 994-1039` (entra no `messages` do pai); `be/src/persistence/event-payload-schemas.ts:39-46` (também persiste inteiro). Contraste: `orchestrator.ts:246` (`spawnReason` cortado em 500) e `410` (corte de 500 só para auditoria); `be/system-prompt.md:111` (só pede concisão).
- Impacto: o pai recebe o texto final inteiro de cada filho, multiplicado por N filhos no mesmo turno, e paga isso nos turnos seguintes até a compactação (que só dispara a 90% de 230 mil); contraria o padrão da Anthropic de retorno condensado com referência externa.
- Recomendação: cortar o `finalText` na notificação (ex.: 4–8 mil chars, com aviso de corte) e instruir o filho a devolver resumo denso mais a referência ao `threadId` para o detalhe.

**[alta · confirmado] `subagentes-sem-cascata` — Cancelar o pai não desce para filhos e netos** · Esforço M
- Evidência: `be/src/engine/orchestrator.ts:456-464` (`abortChild` aborta só o id nomeado); `be/src/ws/handler.ts:599-612` (o Parar aborta só a thread conectada); `be/src/engine/registry.ts:152-177` (abort por thread; cada lease nova traz controller novo); `be/src/engine/dispatcher.ts:265-294` e `orchestrator.ts:297-372` (a notificação do filho acorda o pai sem checar cancelamento); `be/src/persistence/threads.ts:324` (`listThreadTree` existe, mas só é usada em leitura); flag `isCascade` sem uso (`registry.ts:60`).
- Impacto: cancelar o pai não toca a subárvore, que continua gastando; e cada neto que termina acorda o intermediário cancelado, rodando turnos que ninguém vai ler.
- Recomendação: abort recursivo pela árvore, ou marcar a raiz como cancelada para o `onTurnEnd` descartar notificações de subárvore morta.
- Revisão: o próprio doc de prontidão admite que o cascade "não está implementado, mas a flag existe para o futuro".

**[alta · confirmado] `subagentes-abort-cross-replica-morto` — Abort entre réplicas publica num canal que ninguém escuta** · Esforço M
- Evidência: `be/src/engine/registry-redis.ts:154, 156-167`; `be/src/engine/reaper.ts:23-26, 48, 153, 181`; `docs/motor-agent-prod-readiness/engine.html:190` e `arquitetura.html:631` (prometem o assinante que não existe).
- Impacto: com várias réplicas, `agent_abort` ou Parar na réplica errada não tem efeito e devolve `false`; e o reaper eleito numa réplica pode declarar órfão (e notificar erro ao pai) um turno longo vivo em outra.
- Recomendação: assinar o canal de abort em todas as réplicas; no reaper, checar se o lock existe no Redis antes de declarar órfão. Ver o tema transversal em 3.0.

**Não verificados (média, baixa, info):**

| Achado | Sev. | Esf. | Evidência | Impacto | Recomendação |
|---|---|---|---|---|---|
| `subagentes-filtro-tools-fura-nativas` — o filtro `tools` do spawn não alcança as nativas | média | P | `be/src/engine/orchestrator.ts:226-228`; `be/src/tools/native/subagent.ts:128-134` | o filho "só leitura" ainda abre netos e usa `workflow_*` e `setup_*` | aplicar o filtro também às nativas, ou documentar que só cobre MCP |
| `subagentes-persona-herdada-sumida` — herdar o setup não traz a persona; `setupId` explícito traz | média | P | `orchestrator.ts:186-223`; `be/src/config/thread-config.ts:500`; `subagent.ts:137-141` | os dois caminhos geram agentes diferentes sem aviso | injetar a persona também no caso herdado, ou explicitar nas descrições |
| `subagentes-workspace-compartilhado` — filhos paralelos no mesmo workspace | média | G | `orchestrator.ts:15-16`; `be/system-prompt.md:100-107` | edições intercaladas ou sobrescritas silenciosas no mesmo arquivo | orientar a particionar o escopo de escrita por filho, ou worktree/branch por filho |
| `subagentes-doc-concurrency-divergente` — header diz 16; o código aplica 100 | baixa | P | `be/src/engine/org-concurrency.ts:14, 50`; `be/.env.example:251` | premissa errada (fator 6) em dimensionamento e incidentes | corrigir o header |
| `subagentes-shortid-morto` — `shortId` existe e nunca é usado | baixa | P | `orchestrator.ts:469-471` | id longo (UUID) no transcript e nas tools | usar como alias em `agent_status`/`agent_abort` ou remover |
| `subagentes-exatamente-uma-vez-ok` — idempotência e exactly-once bem resolvidos | info | — | `orchestrator.ts:108-155, 317-321`; `be/src/engine/mailbox-db.ts:98-119` | ponto forte: retry não duplica filho; duas réplicas não notificam duas vezes | manter; testar concorrência real entre réplicas |
| `subagentes-agrupamento-midturn-ok` — agrupamento e injeção no meio do turno funcionam | info | — | `be/src/engine/dispatcher.ts:143-149`; `turn-runner.ts:956-1048`; `mailbox.ts:232-239` | ponto forte: fan-out não vira enxurrada de turnos | manter; medir a latência ponta a ponta |

**Métricas medidas (subagentes):**
- Tamanho em linhas: `orchestrator.ts` 471; `subagent.ts` 281; `mailbox.ts` 335; `mailbox-db.ts` 175; `dispatcher.ts` 303; `reaper.ts` 322; `reconciler.ts` 120; `registry-redis.ts` 264; `setup.ts` 520; `system-prompt.md` 288 linhas (26.575 bytes).
- Custo por spawn: 1 findUnique (idempotência) + 1 getThread (com Redis) + 1 findFirst do setup (só com `setupId`) + INSERT da thread + INSERT do evento `subagent_spawned` + lookup da org + INSERT na mailbox.
- Custo por notificação: findFirst + updateMany (claim) + count de irmãos + push na mailbox (lookup + insert) + pump do pai (`hasPending` + drain).
- Tempos: reaper com stale de 30 min e varredura de 60s; reconciler de 1h; lock Redis com TTL de 30s e heartbeat de 10s; guarda de aborto de 5s; drain de shutdown de 30s.
- Tetos: rate limit externo de 60 submits/min por (org, usuário), com os internos isentos; concorrência por org de 100 em voo por processo; `setup_list` limitado a 50; `spawnReason` cortado em 500 chars; `finalText` sem corte.
- Contrato de prompt: seção de subagentes com ~40 linhas no system prompt e descrição do `agent_spawn` com ~11 linhas; só `title` e `task` são obrigatórios.
- Produção (janela lida): nenhum evento de spawn, notificação, reaper ou abort visível na amostra.


### 3.5 Workflows

**[alta · confirmado] `workflows-outputs-in-list-payload` — Lista de passos e handshake do WS carregam o `output` inteiro de cada passo** · Esforço M
- Evidência: `be/src/workflows/contract.ts:132-135` (`output: row.output ?? null` no envelope do passo); `be/src/ws/run-channel.ts:149-181` (handshake do WS: findMany sem `take`, com output inteiro); `be/src/routes/workflows.ts:592-602` (`GET /api/runs/:id/steps` sem `take`) e `604-609` (o comentário admite que fan-out de 300 passos não pode viajar na lista — tiraram `input` e `attempts`, mas o `output` ficou); `be/src/workflows/executors.ts:494` (corpo HTTP de até 1 MB por passo); `be/src/workflows/definition.ts:118` (até 10.000 passos); `fe/src/features/run/components/RunView.tsx:267-274` (o painel de foco usa o output da lista).
- Impacto: um run com 50 passos `http` pode gerar até 50 MB por handshake e por GET; um `map` de 1.000 filhos baixa megabytes só para montar a espinha. O navegador pode travar antes de renderizar (risco já registrado em `docs/workflows/02-arquitetura.md:108-110`).
- Recomendação: na lista, `output: null` com prévia curta (1–2 KB) ou indicador de truncamento; output completo só em `/steps/:id` e no frame `step_output`; ajustar o reducer do front.

**[média · confirmado; sev. contestada alta→média] `workflows-absence-budget-no-cap` — Run sem `budget` não tem teto nenhum** · Esforço M
- Evidência: `be/src/workflows/runs.ts:270, 286-302` (`resolveRunCaps`: o declarado vence; ausente vira env ou "sem teto"); `be/src/workflows/state.ts:378-402`; `be/.env.example:359-365` (`WF_DEFAULT_MAX_USD`, `_STEPS` e `_WALL_MS` vazios; "antes vinha escondido no default do banco: US$ 5, 200 passos, 30 min"); `be/src/tools/native/workflow.ts:167-168, 738-740` (o `workflow_draft` só avisa); `be/src/engine/agent-step.ts:174-178` (sem `timeoutMs`, passo de agente não tem prazo).
- Impacto: um workflow publicado sem `budget` (ou antes de o operador definir tetos padrão) pode gastar sem limite de USD, passos ou tempo; a única rede é o orçamento mensal da org, que é fail-open se o Redis cair.
- Recomendação: definir em produção `WF_DEFAULT_MAX_USD` (ex.: 5), `WF_DEFAULT_MAX_STEPS` (ex.: 500) e `WF_DEFAULT_MAX_WALL_MS` (ex.: 1.800.000); opcionalmente exigir `budget` com `WF_REQUIRE_BUDGET=1`; aviso explícito no retorno do `workflow_draft` quando o `budget` vier vazio.
- Revisão: rebaixado porque é decisão de produto explícita e documentada (`docs/workflows/00-visao-geral.md:242, 284`; `05-guia-operacional.md:383`), com o orçamento da org conferido antes de cada chamada ao LLM (`be/src/engine/turn-runner.ts:832-841`). A recomendação vale como política da instalação.

**Não verificados (média, baixa, info):**

| Achado | Sev. | Esf. | Evidência | Impacto | Recomendação |
|---|---|---|---|---|---|
| `workflows-eventlog-no-cursor` — o log do run relê desde `seq=0` a cada 4s e corta em 500 | média | P | `fe/src/features/run/model/queries.ts:122-134`; `be/src/routes/workflows.ts:645-687` (já aceita `after`) | runs com mais de 500 eventos nunca mostram o resto; o mesmo payload volta a cada 4s | passar o último `seq` como `after`, ou usar os frames `run_event` do WS |
| `workflows-races-can-reopen-after-terminal` — `requestResume` pode reabrir run cancelado | média | P | `be/src/workflows/runs.ts:556-573, 614-626, 687-697`; `be/src/workflows/reconcile.ts:1063-1064` | run que deveria ficar `canceled` volta a `running` | incluir `cancelRequestedAt: null` e status não terminais no WHERE; teste L4 |
| `workflows-runpubrun-pauses-carried` — cancelamento na janela `pausing` | baixa | P | `runs.ts:583-657, 779-850`; `reconcile.ts:1166-1168` | caso raro: depois de cancelado nessa janela, não dá para retomar | WHERE com `pauseRequestedAt: null` e status não terminais, como no cancel; teste L4 |
| `workflows-api-runs-no-pagination-large-result` — `total` por `count` separado pode divergir | baixa | P | `be/src/routes/workflows.ts:505-582` | "número errado que parece certo" quando um run nasce entre as duas queries | transação com isolamento adequado, ou documentar o `total` como aproximado |
| `workflows-runpubrun-idempotency-race` — dois `startRun` com a mesma chave ao mesmo tempo | baixa | P | `runs.ts:352-357`; `be/prisma/schema.prisma:794` | o segundo recebe 500 em vez do run reutilizado | capturar o conflito de unique (P2002) e reler o run existente |
| `workflows-cancelreason-overload-pausecolumn` — motivo do cancelamento gravado em `pauseReason` | baixa | P | `runs.ts:616-622, 791-797`; `schema.prisma:782` | auditoria confusa, com dois domínios na mesma coluna | coluna `cancelReason` ou documentar o uso |
| `workflows-rest-pause-expires-deadcolumn` — `pause_expires_at` nunca é calculada nem consultada | baixa | P | `schema.prisma:784`; `runs.ts:695`; `reconcile.ts:1166-1168`; `be/src/queue/retention.ts:200-204` | run pausado fica pausado para sempre e escapa da retenção | calcular o prazo (ex.: `WF_PAUSE_MAX_DAYS`, 30 dias) e cancelar ao vencer |
| `workflows-lease-no-renewal-mid-tick` — a lease do reconciliador não é renovada durante o tick | baixa | P | `reconcile.ts:66, 129-153, 1696-1905`; `be/src/queue/sweeper.ts:256-266` | run muito grande pode ter dois ticks concorrentes (trabalho desperdiçado; as outras barreiras impedem execução dupla) | renovar a lease a cada ~20s durante o tick, ou lease maior |
| `workflows-docs-operacao-stale` — `docs/operacao.md` diverge do código | baixa | M | `docs/operacao.md:175` ("concorrência de IA = 2") × `be/src/workers/index.ts:168` (padrão 100); `docs/workflows/02-arquitetura.md:58` (`wf:reconcile` × `wf-reconcile`) | o runbook consultado em incidente orienta errado | sincronizar os docs e marcar a versão do código que vale |
| `workflows-fanout-default-concurrency-100-doc-divergence` — concorrência padrão do fan-out | info | — | `reconcile.ts:1759`; `docs/workflows/00-visao-geral.md:98`; `be/src/workflows/definition.ts:114` | consistente hoje; só rastreabilidade | ligar na doc o teto (200) ao padrão de runtime (100) |
| `workflows-isc-mcp-pool-leak-info` — produção registra "LEAK detected" no pool MCP | info | M | log do `motor-be-ws` (browser, agentpack, mp); `be/src/observability/metrics.ts:156` | algum `acquire` sem `release` num caminho de erro: vazamento gradual de handles | rastrear o caminho e alertar sobre `mcpLeakedRefs` |
| `workflows-cron-no-sheduler-info` — `trigger` aceita `schedule`, mas não há agendador | info | P a G | `schema.prisma:706`; `runs.ts:488`; `be/src/queue/queues.ts` | o dono pode achar que existe agendamento; hoje só por fora (cron + curl) | implementar o agendamento ou remover `schedule` e documentar |

**Métricas medidas (workflows):**
- ~10.424 linhas de código fora de teste no diretório de workflows (`definition` 1911, `reconcile` 2456, `expr` 1029, `runs` 962, `expanders` 860, `executors` 798, `runner` 764, `state` 492, entre outros), mais 14 arquivos de teste com 302 casos; filas em `be/src/queue` com 1.415 linhas; `workers/index.ts` com 342; `engine/agent-step.ts` com 956; `tools/native/workflow.ts` com 1.581. Total da dimensão: ~14.700 linhas.
- Postgres: 6 models específicos de workflow; 9 status de run e 8 de passo.
- Filas BullMQ: 6 — `wf-reconcile` (10), `wf-step-ai` (100, lock de 120s), `wf-step-det` (100, lock de 60s), `wf-notify` (10, lock de 30s), `wf-maintenance` (1, lock de 60s), `wf-ping` (10). O lock é só atraso de recuperação; o relógio real é o heartbeat da tentativa.
- Padrões de produção: `WORKER_AI_CONCURRENCY` e `WORKER_DET_CONCURRENCY` 100; `ORG_CONCURRENCY_MAX` 100; `DB_CONNECTION_LIMIT` 1000; `WF_DEFAULT_MAX_*` vazios; purga ligada com eventos de 30 dias; retenção de runs de 180 dias.
- Limites do formato: 500 passos de topo; 10.000 passos no total; timeout HTTP de 1h; portão humano de até 30 dias; até 10 retries com backoff de até 600s; expressão de até 4.000 chars, profundidade 32 e 500 nós; sub-workflow com até 5 níveis; corpo HTTP de 1 MB.
- Desempenho medido (WF-PERF-L7-01): `map` de 1.000 transforms em 17,5s (67 passos/s, 100 filhos em voo); `map` de 100 com concorrência 100 em 1,5s; promoção de 50–75 ms (p50/p95).
- Observabilidade: 11 métricas Prometheus + 3 gauges de fila.

### 3.6 Banco, cache, threads e escala

**[alta · confirmado] `dados-convo-redis-writeonly` — O convo no Redis é write-only e um miss zera o contexto do LLM** · Esforço M
- Evidência: `be/src/engine/registry-redis.ts:171` (`getConvo` lê só o `Map` local) e `172-181` (o Redis só recebe SET e DEL; nenhum GET em `be/src`); `be/src/engine/turn-runner.ts:705-706` (`getConvo(threadId) ?? []`, sem fallback ao banco); `be/src/ws/handler.ts:258-267, 397-399, 471, 567, 621` (o convo só é hidratado no connect ou reset; o `launcher-wake` acorda qualquer réplica); `be/src/engine/dispatcher.ts:296-298` (`kick` sem hidratar); `be/src/workflows/notify-launcher.ts:163-170`; o próprio código admite em `turn-runner.ts:869-870` que um restart "apaga da memória do LLM".
- Impacto: um turno que roda sem o convo em memória manda ao LLM só as mensagens novas, sem o histórico — e a resposta fora de contexto ainda é persistida na thread. Acontece **já com 1 réplica**: depois de um restart, quando um workflow acorda a conversa (`kick`) ou chega mensagem antes de um connect nesta instância; e em qualquer cenário com várias réplicas. O SET do convo no Redis é custo puro.
- Recomendação: `getConvo` com fallback: `Map` local → GET no Redis → `getEvents` + `eventsToLlmMessages` do banco; enquanto isso, alerta quando um turno começa com convo vazio numa thread com `lastSeq > 0`.

**[alta · confirmado] `dados-reaper-desligado` — Reaper desligado: turnos órfãos ficam `running` e pais travam** · Esforço P
- Evidência: `be/src/engine/reaper.ts:67-69, 302-307`; `docs/operacao.md:134`; `be/src/plugins/websocket.ts:64`; as variáveis de produção não têm `REAPER_ENABLED`; `reconcileStuckNotifications` e `sweepThreadCounters` só rodam dentro do reaper (`reaper.ts:256, 276`).
- Impacto: crash entre `startTurn` e `finishTurn` deixa o turno `running` para sempre; subagente órfão nunca notifica o pai; o chat fica sem rede de segurança (o sweeper só cobre workflows).
- Recomendação: a mesma de `subagentes-reaper-desligado` (3.4), mais `isBusy` distribuído para o reaper não colher turno longo de outra réplica.

**[alta · confirmado] `dados-broadcaster-local` — Evento de turno pode ir para a réplica que não tem o socket** · Esforço G
- Evidência: `be/src/ws/broadcaster.ts:60-63`; `be/src/ws/handler.ts:234-240, 258-271` (o `launcher-wake` faz todas as réplicas darem `kick`; vence o lock quem chegar primeiro, que pode não ter o socket); `be/src/workflows/register.ts:71` (o engine do worker não emite para socket algum); `be/src/persistence/events.ts:65-113` (o canal `:stream` está pronto, sem assinante).
- Impacto: no caminho feliz (envio pelo WS), turno e socket ficam na mesma réplica. Turnos disparados por `kick` (workflow acordando a conversa) podem rodar na réplica errada: o usuário não vê streaming nem `final` até reconectar; o `thread_busy` tem o mesmo limite.
- Recomendação: publicar os eventos de turno no Redis (o canal `:stream` já existe) e cada réplica repassar só aos seus sockets; ou fixar o turno na réplica do socket.

**[média · confirmado; sev. contestada alta→média] `dados-events-sem-retencao` — `events` de thread ativa cresce sem teto nem purga** · Esforço G
- Evidência: `be/src/queue/retention.ts:29-36` (decisão documentada: events de thread viva nunca são purgados; "particionar events no cenário grande fica para quando o volume pedir") e `113-217`; `be/src/queue/purge.ts:81-145` (só purga `workflow_events`); `be/src/agent/compaction.ts:9-13` (append-only); `be/prisma/schema.prisma:293-321` (5 índices, sem partição nem TTL).
- Impacto: cada turno grava ~3–8 events para sempre; threads longas e fan-out incham tabela e índices e encarecem hidratação, compactação e backup.
- Recomendação: retenção para events só de exibição (`reasoning`, `step_done`) depois de N dias, ou arquivamento de threads inativas; monitorar o tamanho da tabela e a contagem por thread.
- Revisão: rebaixado porque é decisão documentada (não corromper thread arquivada que pode voltar) e o custo de leitura é por thread.

**[baixa · confirmado; contestado (alta→baixa pelo revisor; a investigação de lacunas aponta risco alto)] `dados-pool-1000` — Pool de 1000 conexões por processo × `max_connections` do Postgres** · Esforço P a M
- Evidência: `be/src/persistence/prisma.ts:12, 35-40` (1000 conexões por processo, abertas sob demanda); `docs/operacao.md:9-12` (API + worker = 2000; Postgres precisa de ≥ 2100; "não reduza o pool"); `compose.prod.yml:20-64` (1 `be` + 1 `worker`); `docs/workflows/08-matriz/l7.md:41` (teste L7-16: pico de 151 conexões, base de 42–48, zero erro de pool).
- Impacto: uma 2ª réplica de API eleva o teto a 3000, acima de 2100; sob burst, as queries estouram o `pool_timeout` de 10s.
- Recomendação: não subir réplica sem PgBouncer em modo transação ou `max_connections` dimensionado (hoje a decisão do dono proíbe reduzir o pool).
- Revisão: o revisor rebaixou para baixa assumindo o runbook (≥ 2100), 1 réplica e o pico medido de 151. A investigação de lacunas leu `max_connections = 100` no `postgresql.conf` de produção: se `SHOW max_connections` confirmar, a carga do próprio teste L7-16 não caberia, e o risco volta a **alto já com a topologia atual**.

**Não verificados (média, baixa, info):**

| Achado | Sev. | Esf. | Evidência | Impacto | Recomendação |
|---|---|---|---|---|---|
| `dados-org-concurrency-local` — gate de concorrência e circuit breaker por processo | média | M | `be/src/engine/org-concurrency.ts:37-39, 50-60, 75` | teto real de 100 × (réplicas + worker) por org; o aprendizado de 429/503 não se propaga | contador no Redis, ou publicar o hit de rate limit para as réplicas abrirem o breaker juntas |
| `dados-rate-limit-local` — rate limits HTTP e de submit por processo e por IP | média | M | `be/src/engine/rate-limit.ts:40-41, 69`; `be/src/server/index.ts:181-185`; `be/src/routes/workflows.ts:198-204` | multiplica por réplica; atrás de NAT, usuários dividem um balde | store Redis no `@fastify/rate-limit` e chave por token ou sessão |
| `dados-auth-sem-cache` — cada request autenticado faz 2 leituras no banco | média | P | `be/src/auth/session.ts:201, 217`; `be/src/http/auth.ts:336` | com polling, autenticação vira QPS constante no Postgres | cache de sessão e vínculo no Redis (30–60s), invalidado no fim de sessão e na troca de papel |
| `dados-webhook-fanout-turno` — cada turno faz 2–4 buscas de webhooks | média | P | `be/src/webhooks/dispatcher.ts:239-243, 305`; `turn-runner.ts:506-511, 528, 578, 1248-1259` | org sem webhook paga as buscas; com N webhooks, N POSTs com retry e N INSERTs por turno | cache da lista por org e entrega em background com concorrência limitada |
| `dados-listas-sem-paginacao` — árvore, turns e passos sem paginação | média | M | `be/src/persistence/threads.ts:324-329`; `be/src/persistence/turns.ts:191-211`; `be/src/routes/workflows.ts:597`; `be/src/routes/webhooks.ts:139` | fan-out grande devolve tudo de uma vez (query pesada e JSON gigante) | paginar por cursor, com teto padrão e máximo |
| `dados-compaction-full-scan` — a compactação lê a thread inteira dentro do turno | média | M | `be/src/agent/compaction.ts:148-179, 408-409`; `turn-runner.ts:525-575` | piora o p95 justamente nas conversas mais ativas | compactar em background (ou no fim do turno anterior) e limitar páginas |
| `dados-mapas-sem-eviccao` — mapas por thread, org e conexão crescem até o restart | média | P | `registry-redis.ts:70-72, 216, 222`; `dispatcher.ts:107`; `handler.ts:212`; `busy-owner.ts:34`; `org-concurrency.ts:75`; `rate-limit.ts:69`; `cost-tracker.ts:181` | vazamento lento de memória (um convo pode ter MBs) | TTL ou LRU e limpeza garantida também no caminho de erro |
| `dados-hydrate-sem-singleflight` — misses concorrentes disparam N hidratações | média | P | `be/src/persistence/events.ts:138-217, 344-349` | 5 abas abrindo a mesma thread fria = 5 leituras completas simultâneas | promessa compartilhada por thread e marcador `SET NX` de hidratação |
| `dados-payload-grande` — imagens de até MBs persistidas em 3 lugares | média | M | `be/src/http/ws-protocol.ts:19, 25`; `mailbox-db.ts:87-94`; `events.ts:97, 108-113` | poucas mensagens com imagem por minuto saturam rede e Redis e incham o banco | gravar o binário em `attachments` (o model já existe) e trafegar referência e miniatura |
| `dados-org-slot-por-turno` — o slot é por turno: teto de 100 turnos simultâneos por org | média | P | `be/src/agent/loop.ts:434-448, 599`; `org-concurrency.ts:13-17` | acima disso o turno falha em vez de esperar | granularidade é decisão de produto (ver 3.9); o conserto é a negação virar espera ou retry |
| `dados-publish-stream-sem-subscriber` — publish do JSON num canal sem consumidor | baixa | P | `events.ts:70, 112`; `be/src/persistence/keys.ts:60-61` | custo sem leitor | ver `tempo-real-events-canal-sem-ouvinte` (confirmado, alta) |
| `dados-convo-rewrite-full` — o turno regrava o convo inteiro no Redis | baixa | P | `turn-runner.ts:705-706`; `registry-redis.ts:172-177` | CPU e rede a cada turno, sem leitor | gravar só quando mudar; com o fallback de leitura, de forma incremental |
| `dados-indices-faltantes` — três consultas quentes sem índice composto ideal | baixa | P | `threads.ts:278-288`; `turns.ts:205-211`; `mailbox-db.ts:132-142`; `schema.prisma:218-229, 283, 347, 352` | com volume, ordenação em memória e drain por step mais caro | índices compostos para lista de threads, turns por thread e drain da mailbox, conferindo com EXPLAIN |
| `dados-index-redundante` — índice `events(thread, seq)` duplica o unique | baixa | P | `schema.prisma:312-313` | um índice a mais em cada INSERT da tabela mais escrita | migration removendo o índice redundante |
| `dados-submit-duplo-db` — cada submit faz 2 idas ao banco | baixa | P | `mailbox-db.ts:66-96` | dobra as operações de mailbox sob fan-out | receber o `orgId` no push e tratar o erro de thread inexistente |
| `dados-sweeper-n1` — o sweeper faz 1 SELECT + 1 transação por tentativa órfã | baixa | P | `be/src/queue/sweeper.ts:213-251` | em incidente, centenas de operações a cada 30s competindo com o reconciliador | buscar em lote e recolher com poucos `updateMany` |
| `dados-heartbeats` — heartbeats geram escrita contínua | info | P | `registry-redis.ts:55-56, 113-129`; `be/src/workflows/state.ts:460-489` | pequena (~13 UPDATEs/s com 200 tentativas) | só monitorar |
| `dados-default-divergente` — header diz 16; o código diz 100 | info | P | `org-concurrency.ts:14, 50` | estimativas e alertas com premissa errada | alinhar header, código e `.env.example` |
| `dados-doc-purge-divergente` — doc diz purga desligada; o código liga por padrão | info | P | `docs/operacao.md:132`; `be/src/queue/purge.ts:50-52` | dúvida sobre o que vale em produção | o boot do worker de produção mostra a purga **ligada** (30 dias): corrigir a doc |

**Métricas medidas (dados e escala):**
- Pool Prisma: 1000 conexões por processo; timeouts de conexão 10s, pool 10s e socket 30s. Conta do runbook: 1 API + 1 worker = 2000 ≤ 2100.
- Cache: thread 3600s; events 1800s; turn 1800s; config da org 60s; página de histórico 300s; janela quente de 200 events.
- Turno simples: ~20–25 statements no banco em ~12–15 idas e voltas, e ~40–50 comandos Redis em ~15–20 idas e voltas (contagem por leitura de código). Por step: 1 UPDATE (drain de notificações) + 0–1 HGETALL + 0–5 MULTIs do buffer ao vivo.
- Rate limits: dispatcher 60/min por (org, usuário); HTTP global 100/min por IP; leituras de run 600/min.
- Worker: reconcile 10, step-AI 100, step-det 100, notify 10, maintenance 1; shutdown de 30s; drain da API de 30s.
- Sweeper: a cada 30s, lote de 200, stale de 45s. Purga horária em lotes de 500. Retenção de 30, 7, 90 e 180 dias.
- Custo por turno: HGETALL de pré-check + pipeline de 8 comandos no registro; espelho local de 1,5s.
- Listas: threads até 200, setups 200, workflows e runs 200, events 100 por padrão; **sem teto**: árvore, turns, passos de run, webhooks.
- Schema: 20 models, ~50 índices e 11 uniques; `events` tem unique `(thread, seq)` e índice redundante `(thread, seq)`.
- Emissões por turno: 14 chamadas `emit` no turn-runner + `pushLive` por evento ao vivo; 2–4 emissões de webhook.
- Serviços na leitura: `motor-be-ws` ~232–242 MB e 0,4–1,3% de CPU; `motor-worker-ws` ~122–129 MB e ~0,5% de CPU; Postgres 86 MB; Redis 24 MB.
- Capacidade estimada (sem teste de carga): ~50–80 turnos simultâneos confortáveis por réplica, teto duro de 100 turnos simultâneos por org e processo, ~200–300 turnos/min com turno médio de 10–20s (premissas: threads com menos de 200 events, 1–2 steps por turno, 1 org quente, proxy com latência normal) — **se** o banco estiver no padrão do runbook. Com `max_connections = 100`, a investigação de lacunas estima ~10–30 conversas simultâneas.


### 3.7 Segurança e vazamento de informação

**[crítica ou alta · confirmado; contestado entre revisores (crítica × alta)] `seguranca-cross-tenant-01` — Toda org herda a config da org default no turno** · Esforço G
- Evidência: `be/src/server/index.ts:290-295` (`deps.chatConfig` = config da org default); `be/src/plugins/websocket.ts:38` (`getConfig: () => deps.chatConfig`); `be/src/engine/index.ts:100, 153` e `be/src/engine/turn-runner.ts:347-360, 432` (esse valor vira o `global` de todo turno, de qualquer org); `be/src/setups/service.ts:763-868` (`loadEffectiveConfig` recebe o `global` e nunca lê a `OrgConfig` da org do turno); `be/src/config/thread-config.ts:303-309` (`apiKey` herdada), `328-368` (MCPs com tokens), `412` (TTLs de cache), `514-516` (`systemPromptAppend`); `be/src/routes/threads.ts:460-465` (só a org default atualiza o valor em memória) e `611-622` (a rota de config da thread também usa o da default); `be/src/auth/jit.ts:170-184` (o desenho prevê uma `OrgConfig` por empresa e diz que "nunca copia a config da org default").
- Impacto: para qualquer org que não seja a default, o turno herda `basic.apiKey` (a checagem D12 não barra quando a URL é a mesma), MCPs com tokens e headers de autorização, `systemPromptAppend` e TTLs de cache da default; o custo vai para a chave de cobrança da default. Tudo em silêncio.
- Recomendação: `loadEffectiveConfig(orgId, …)` receber a `OrgConfig` da própria org (cache por org com TTL curto, ou um provedor `(orgId) => config`); o `deps.chatConfig` passa a ser só o template da instalação.
- Revisão: um revisor manteve crítica (o desenho é multi-tenant e o runtime o contradiz; um comentário em `threads.ts:442-445` descreve o oposto do código). Outro rebaixou para alta: a UI hoje força a org default (não há troca de org) e as empresas novas nascem do template sem `apiKey` — latente hoje, **crítico no dia em que a troca de org for liberada**.

**[alta · confirmado por 2 revisores; sev. contestada crítica→alta] `seguranca-ssrf-01` — Entrega de webhook usa `fetch` cru: o guard SSRF não cobre redirect e a assinatura acompanha o salto** · Esforço P
- Evidência: `be/src/webhooks/dispatcher.ts:121-126` (`fetch` sem `redirect: 'manual'` nem `lookup`); `dispatcher.ts:218` (`resolveAndValidate` uma vez, antes do laço de retries); `dispatcher.ts:232-237` (envia `X-Webhook-*` e `X-Motor-Signature-256`, que o undici não derruba em redirect entre origens); `be/src/net/ssrf.ts:27-28, 163-168, 382-396, 460-475` (o `safeRequest` faz tudo isso certo); `be/src/workflows/executors.ts:560` (o passo `http` do workflow já usa o `safeRequest`).
- Impacto: um endpoint de webhook que responda 302 para um host interno (ex.: 169.254.169.254) faz o `fetch` seguir levando a assinatura e o envelope (orgId, threadId, dados do evento); e a cada retry o nome é resolvido de novo (DNS rebinding).
- Recomendação: trocar o `postOnce` pelo `safeRequest` (com os headers de assinatura no próprio `safeRequest`), aceitar só `https:` e apagar a função `allowPrivateHosts` morta.
- Revisão: rebaixado por 2 revisores porque quem cadastra webhook é admin da própria org; o conteúdo vazado e o vetor sustentam alta. Em produção `WEBHOOK_ALLOW_PRIVATE_HOSTS` está vazio (o guard vale na checagem inicial).

**[alta · confirmado por 2 revisores; sev. contestada crítica→alta] `seguranca-ssrf-02` — `POST /api/mcp/test`, aberto a qualquer membro, conecta sem guard SSRF** · Esforço P
- Evidência: `be/src/routes/mcp.ts:11-12, 35-47` (sem checagem de papel; `connectMcp` com a URL do corpo; `err.message` devolvido no 502); `be/src/routes/index.ts:48`; `be/src/http/auth.ts:336` (só identidade); `be/src/tools/mcp-client.ts:230-233, 376-389`; `be/src/net/ssrf.ts:38-39`; `be/public/chat.html:1646, 1707-1763` (botão "testar conexão" sem restrição de papel); problema já listado em `docs/api-terceiros.md:580` (§7.3.4) e em `docs/motor-agent-prod-readiness/bn-mcp.html` (MCP-06).
- Impacto: qualquer membro autenticado envia uma URL interna e recebe de volta a mensagem de erro do destino — oráculo de alcançabilidade da rede interna e, se houver um MCP interno, leitura dele.
- Recomendação: `exigirAdmin` e rate limit próprio na rota; `resolveAndValidate` dentro do `connectMcp` (vale para todos os chamadores).
- Revisão: rebaixado porque exige sessão válida e a leitura útil depende de um MCP interno alcançável.

**[alta · confirmado] `seguranca-ssrf-03` — Teste de conexão do setup faz `fetch` cru na `baseURL` e ecoa a resposta** · Esforço P
- Evidência: `be/src/routes/agents.ts:240-364` (`fetch` cru para a `baseURL` do setup), `82` (anexa `providerMessage.slice(0, 100)` ao status) e `355` (ecoa `err.message`); `be/src/config/secret-origin.ts:61-69` (sem chave quando a origem difere — mas o request sai mesmo assim); `be/src/config/proxy-url.ts:41-77` (só normaliza o caminho); `be/src/setups/visibility.ts:55-57` (no setup privado, o dono pode testar); `be/src/config/thread-config.ts:34-49` (aceita `http://` e IP privado).
- Impacto: um membro cria um setup privado com `baseURL` interna, chama o teste e lê os primeiros 100 chars da resposta do serviço interno, mais as mensagens de erro de transporte.
- Recomendação: usar o `safeRequest` e devolver só o status e uma mensagem genérica.

**[alta · confirmado] `seguranca-ssrf-04` — MCPs configurados na thread (por qualquer membro) alcançam a rede interna no turno seguinte** · Esforço M
- Evidência: `be/src/tools/mcp-client.ts:230-233` (transporte direto da URL) e `252-254, 268-270` (o erro inclui URL e motivo); `be/src/tools/mcp-pool.ts:136-169`; `be/src/agent/bootstrap.ts:134-152`; `be/src/config/thread-config.ts:71-79, 328-368`; `be/src/routes/threads.ts:234-302` (o PATCH da thread aceita `mcps` sem exigir admin); `docs/motor-agent-prod-readiness/bn-mcp.html:160-178` (MCP-06 registrava o furo como médio).
- Impacto: um membro grava na própria thread um MCP apontando para um IP interno; no turno seguinte o backend abre a conexão; os erros vazam a topologia e um MCP interno real pode injetar tools no prompt.
- Recomendação: `resolveAndValidate` no `connectMcp` e na camada de MCPs extras da thread, com `WEBHOOK_ALLOW_PRIVATE_HOSTS` como exceção explícita; idealmente também para MCPs de org e setup.
- Revisão: o MCP-06 original considerou só a config de org (admin); o override por thread, editável por membro, é o vetor mais explorável.

**[alta · confirmado] `seguranca-segredo-01` — No escopo da thread, o `•••` ecoado pela UI vira header real enviado ao MCP** · Esforço P
- Evidência: `be/src/config/secret-origin.ts:178` (o bloco de headers só roda no escopo `setup`) e `173-177`; `be/src/routes/threads.ts:283-287` (o PATCH da thread usa o escopo `thread`); `fe/src/components/thread-config/model/config-draft.ts:78-108, 122-138` (o formulário reenvia os headers inteiros, com `•••`); `fe/src/components/thread-config/ui/McpRow.tsx:55-58, 236, 266-274`; `be/src/config/thread-config.ts:328-368` (os headers viram headers reais); `be/src/config/redact.ts:30-37` (a redação só existe na saída); `docs/setups/00-decisoes.md:222-235` (a D12 manda proteger headers também no PATCH da thread).
- Impacto: o usuário abre a config de MCP da thread, salva sem mexer, e o `•••` literal é gravado e enviado como `Authorization` ao MCP no turno seguinte: autenticação errada e silenciosa, e a chave verdadeira se perde.
- Recomendação: tratar os headers nos dois escopos com a mesma regra do token (filtrar `•••`, manter se a origem não mudou, bloquear se mudou), numa função única; e o front não reenviar valores redigidos.

**Não verificados (média, baixa, info):**

| Achado | Sev. | Esf. | Evidência | Impacto | Recomendação |
|---|---|---|---|---|---|
| `seguranca-info-01` — `GET /info` público e sem rate limit | média | P | `be/src/routes/health.ts:115-127`; `be/src/http/auth.ts:208-213` | um anônimo descobre modelo, caminho do workspace, host, porta e número de WebSockets | proteger com o token de métricas ou expor só versão e uptime |
| `seguranca-info-02` — o teste de conexão ecoa a mensagem do provider | média | P | `be/src/routes/agents.ts:82-109, 114-127` | canal de exfiltração junto com o SSRF do setup | devolver só status e mensagem genérica |
| `seguranca-tools-02` — contrato do `sideEffectWrite` quebrado | média | P | `turn-runner.ts:91-131` e chamadores | as 3 tentativas são 1 | confirmado como `loop-sdk-sideeffect-retry` (3.1, alta) |
| `seguranca-tools-03` — metadados de compactação gravados com snapshot velho | média | P | `turn-runner.ts:562-573, 627-631` (o padrão certo já existe em `1193-1200`) | um PATCH do usuário no meio do turno é sobrescrito em silêncio | reler a thread antes de gravar, como no write de `contextTokens` |
| `seguranca-tools-01` — `seenClientMessageIds` cresce sem teto em sessão longa | baixa | P | `be/src/ws/handler.ts:186-205, 650-660` | vazamento lento enquanto houver socket conectado | TTL por entrada ou fila limitada |
| `seguranca-info-03` — PATCH de webhook rejeita `secret: ""`, contra o próprio comentário | baixa | P | `be/src/routes/webhooks.ts:46, 215` | cliente que siga o comentário recebe 400; o ramo é código morto | decidir a semântica e alinhar schema e comentário |
| `seguranca-tools-04` — cache de `OrgConfig` lê sem prefixo e grava com prefixo | baixa | P | `be/src/persistence/orgConfig.ts:58, 101-108, 165` | o cache nunca acerta: toda leitura vai ao Postgres | padronizar a chave nas três operações |
| `seguranca-rbac-01` — submits internos ignoram o rate limit | baixa | M | `be/src/engine/rate-limit.ts:20-40`; `be/src/engine/dispatcher.ts:265-280` | por desenho, mas abre caminho para um cliente acionar muitos spawns | teto de spawns por turno no setup (ver `subagentes-fanout-ilimitado`) |
| `seguranca-info-04` — em dev, a chave de sessão é derivada do segredo OIDC sem aviso | baixa | P | `be/src/auth/config.ts:86-111` | surpresa em dev; produção exige chave própria (fail-closed) | aviso explícito ao usar a derivação |
| `seguranca-info-05` — webhook aceita `http://` em host público | baixa | P | `be/src/webhooks/dispatcher.ts:200-206`; `be/src/net/ssrf.ts:356-358` | assinatura e corpo expostos a MITM | aceitar só `https:` em produção |
| `seguranca-tools-05` — `setup_update` ecoa o override (redigido) no resultado | baixa | P | `be/src/tools/native/setup.ts:494-498` | cópias de `•••` no histórico; nenhuma chave real vaza | documentar; opcionalmente encurtar o eco |
| `seguranca-tools-06` — tools nativas de setup rechecam o papel | info | — | `setup.ts:62, 246, 425`; `be/src/setups/visibility.ts:18-19` | defesa em profundidade intacta (sem achado) | manter |

**Varredura focada da investigação de lacunas** (webhooks, rotas REST e versionamento; sem rodada adversária):

| Ponto | Evidência | O que encontrou | Recomendação |
|---|---|---|---|
| Webhooks fora do guard (A1, A2, A5) | `be/src/webhooks/dispatcher.ts:113-137, 218, 237-238` | o `fetch` global segue até 20 redirects sem revalidar e leva a assinatura HMAC no salto; DNS rebinding entre validação e conexão | o mesmo de `seguranca-ssrf-01`: `safeRequest` |
| `allowPrivateHosts()` morta (A3) | `dispatcher.ts:79-81` | a função existe e nada a consulta | apagar ou usar de fato |
| Webhook sem TLS obrigatório (A4) | `dispatcher.ts:206`; `docs/api-terceiros.md:393-398` | aceita `http:`; a doc promete o guard e não fala de TLS | só `https:` em produção |
| `/api/mcp/test` sem admin e sem limite próprio (B1) | `be/src/routes/mcp.ts:12-62`; `be/src/routes/index.ts:48` | qualquer membro; 100 sondagens/min por IP | o mesmo de `seguranca-ssrf-02` |
| `test-connection` com `fetch` global (B2) | `be/src/routes/agents.ts:312-321` | sem fixar o endereço nem `redirect: 'manual'` | o mesmo de `seguranca-ssrf-03` |
| `/info` e `/readyz` expõem topologia (B3, B4) | `be/src/routes/health.ts:90-127` | modelo, workspace, host e porta; latências internas | `/info` autenticado ou enxuto |
| Rate limit global só por IP (B5) | `be/src/server/index.ts:181-206` | atrás de NAT, uma org divide 100/min; varreduras cabem na cota | limite por org e usuário nas rotas sensíveis |
| Contrato Motor↔LB sem versão (C1) | `be/docs/contrato-load-balancer.md`; `be/src/provider/anthropic.ts` | nenhum header ou campo de versão no wire; a mudança SETUP-SIMPLE-01 do LB não tinha identificador | `X-Motor-Contract: lb-v1` no request e eco na resposta |
| SSO da Conta sem versão (C2) | `be/src/auth/oidc-client.ts:84-103, 181-218`; `be/src/auth/provisioning.ts:91-100, 195-200` | "SSO Nomad v1" só em comentário; o webhook de provisionamento não tem versão de schema | versão no envelope e recusa de versão maior desconhecida |
| MCP sem versão de contrato (C3, C5) | `be/src/tools/mcp-client.ts:240-243`; `be/package-lock.json:1444-1451` | o handshake envia a versão do Motor (0.2.0), não a do contrato com MyPanel e AgentPack | negociar e logar a versão do protocolo; fixar a lista de tools por servidor |
| Refresh de credencial MCP por HTTP sem guard | `be/src/tools/mcp-client.ts:97-134`; `be/src/config/schemas.ts:201` | `auth.refresh.url` aceita qualquer URL (ex.: metadata da nuvem) | `resolveAndValidate` também no refresh |

**Pontos confirmados como corretos** pela mesma varredura: RBAC com `exigirAdmin` em webhooks, workflows e `test-connection` (`be/src/http/rbac.ts:29-46`); remoção de identidade do corpo e da query (`be/src/http/auth.ts:161-169, 322-338`); redação de segredos (`be/src/config/redact.ts`) e chave presa à origem (D12, `be/src/config/secret-origin.ts`); boot fail-closed fora de desenvolvimento (`be/src/auth/config.ts:146-174`); JWT OIDC com `iss`, `aud`, `exp` e `nonce` e recarga de JWKS; webhook de provisionamento da Conta com HMAC, janela de 5 min e idempotência; sessão com `refresh_token` cifrado (AES-256-GCM) e rotação com trava no Redis; passo `http` do workflow com `safeRequest` (há teste provando que o redirect entre origens derruba credenciais); pool MCP com chave por destino e watchdog de vazamento.

**Métricas (segurança):** `be/src/net/ssrf.ts` 563 linhas (guard completo: IPv4 e IPv6 normalizados, "qualquer registro interno bloqueia", conexão fixada no IP validado, redirect manual); `be/src/webhooks/dispatcher.ts` 334; `be/src/tools/mcp-client.ts` 390; `be/src/auth/session.ts` 374; `be/src/auth/jit.ts` 346; `be/src/http/auth.ts` 348; `be/src/config/secret-origin.ts` 212; `be/src/setups/visibility.ts` 88. Bloqueio de 28 chaves no corpo de erro do provider antes de chegar à UI (`turn-runner.ts:1398-1432`).

### 3.8 Qualidade e modularidade

**[crítica · confirmado; sev. contestada alta→crítica] `qualidade-abort-sem-assinante` — "Parar" entre réplicas documentado e sem assinante** · Esforço P
- Evidência: `be/src/engine/registry-redis.ts:24` (o cabeçalho promete o canal `abort:<threadId>` com a dona escutando) × `registry-redis.ts:164` (o único publish usa um canal único, `${p}:abort`); `be/src/engine/registry.ts:37` (repete a promessa); os assinantes reais são só `be/src/ws/handler.ts:259` e `be/src/workflows/bus.ts:142`; os testes do registry Redis não cobrem o caminho entre réplicas.
- Impacto: o motivo de existir do registry distribuído (Parar em qualquer réplica) não funciona; ver o tema transversal em 3.0.
- Recomendação: implementar o assinante que o cabeçalho promete, ou corrigir os cabeçalhos; testar Parar cruzado com 2 réplicas reais.
- Revisão: elevado para crítica porque o Parar entre réplicas é a razão de existir desse componente.

**Não verificados (média, baixa, info):**

| Achado | Sev. | Esf. | Evidência | Impacto | Recomendação |
|---|---|---|---|---|---|
| `qualidade-any-no-turn-runner` — `turn-runner.ts` concentra casts `any` apesar de `InboxItem` ser bem tipado | média | M | `be/src/engine/turn-runner.ts:561, 745, 761, 776-777, 793-795, 805, 809`; `be/src/engine/mailbox.ts:45-52`. Medido nesta revisão: 16 `as any` e 15 `: any` (o scanner citou 29) | o caminho mais quente (mailbox → LLM) roda sem o cinto do TypeScript; mudança de formato quebra em produção, não no build | type predicates para `InboxItem` e tipar o metadata da thread; remover os casts num lote dedicado |
| `qualidade-arquivos-gigantes-workflows` — workflows concentrados em 3 arquivos (~6 mil linhas) | média | M | `be/src/workflows/reconcile.ts` (2456); `definition.ts` (1911); `be/src/tools/native/workflow.ts` (1581); contraste: `expanders.ts` (860, sem `as any`) | difícil revisar, testar isolado e paralelizar trabalho sem conflito | fatiar o reconcile por tipo de passo, como o `expanders.ts` já faz; quebrar `workflow.ts` por verbo |
| `qualidade-turn-runner-mistura` — `turn-runner.ts` mistura 7+ responsabilidades | média | M | `turn-runner.ts` (1583 linhas: drain, compactação, título automático, custo, persistência, taxonomia de erro, estimativa de tokens); `turn-runner.ts:1401` (re-export de `response-events`) | qualquer mudança (ex.: regra de custo) mexe no arquivo que decide o que o LLM vê | extrair taxonomia de erro e estimativa para `provider/` e o título automático para módulo próprio; deixar só a orquestração |
| `qualidade-loop-cabecalho-generateText` — o cabeçalho fala `generateText`; o código usa `streamText` | baixa | P | `be/src/agent/loop.ts:2, 6, 499, 508`; o comentário da linha 452 confirma o `streamText` | confunde quem lê o mecanismo | atualizar o cabeçalho |
| `qualidade-scripts-sem-guarda` — 8 scripts de dev executam no import | baixa | P | `be/src/engine/__spike-prepare-step.ts`; `be/src/engine/__it-cost.ts`; `be/src/persistence/__smoke.ts` (e outros `__it-*`) | importar por engano executa o script (o spike chama o provider real) | guarda `process.argv[1] === fileURLToPath(import.meta.url)` |
| `qualidade-console-em-vez-de-logger` — `events.ts` usa `console.warn` | baixa | P | `be/src/persistence/events.ts:124, 130, 318` | avisos do caminho mais lido sem correlação de request, org e thread | logger estruturado ou o canal de métricas |
| `qualidade-zadd-any` — cast `as any` no `zadd` esconde o bug que já aconteceu | baixa | P | `events.ts:176-184, 194` (o comentário documenta o incidente da timeline em branco) | a mesma classe de bug pode voltar sem aviso | helper tipado para o `zadd` com teste do flag NX único |
| `qualidade-acoplamento-engine-setups` — o engine importa o serviço de setups | info | — | `be/src/engine/turn-runner.ts` (importa `loadEffectiveConfig` de `setups/service`); `be/src/engine/orchestrator.ts:319, 391` | funciona; atrito se setups ganhar ciclo de vida próprio | decidir: manter e documentar, ou injetar a config efetiva pronta |
| `qualidade-testes-dependem-de-ambiente` — integração exige Postgres, Redis e provider reais | info | — | `be/src/engine/__it-cost.ts:17-24`; `__spike-prepare-step.ts:25`; `be/src/persistence/__smoke.ts` | a camada que prova comportamento real não protege cada PR | promover os invariantes baratos a testes unitários sem infra |
| `qualidade-contrato-lb-explicito` — contrato Motor↔LB explícito e citado no código (ponto forte) | info | — | `be/docs/contrato-load-balancer.md`; `be/src/provider/anthropic.ts:1-23, 51-52, 74`; `be/src/tools/registry.ts:1-21` | ponto forte; sem número de versão | versionar o contrato e repetir o padrão nos contratos de MCP e SSO |
| `qualidade-fe-zero-any` — frontend com zero `as any`; backend com 60 | info | — | busca em `fe/src` e `be/src` sem testes; `fe/src/features/thread-runtime` e `fe/src/components/timeline` | o frontend prova que o padrão é viável no mesmo time | lint que falhe em `as any` novo fora de fronteiras documentadas |

**Métricas (qualidade):** backend com ~57 mil linhas em ~220 arquivos; frontend com ~73 mil linhas em 498 arquivos. Testes: 69 arquivos no backend e 142 no frontend; workflows cobertos em níveis L1 a L7. `as any` em produção: 60 no backend (16 em `turn-runner.ts`, medido nesta revisão) e 0 no frontend. Maiores arquivos do backend: `reconcile.ts` 2456, `definition.ts` 1911, `turn-runner.ts` 1583, `tools/native/workflow.ts` 1581, `expr.ts` 1029, `runs.ts` 962, `agent-step.ts` 956, `setups/service.ts` 868. Maiores do frontend: `validate.ts` 1902, `ws.ts` 668, `config-draft.ts` 579, `blocks.tsx` 535. Scripts de dev fora do build: 8 arquivos, ~1.900 linhas, sem imports em produção.

### 3.9 Refutados pelos revisores

Estes achados foram derrubados por contra-evidência e **não entram no roadmap**. A coluna da direita registra o que sobra de válido, quando sobra.

| Achado refutado | Por que caiu | O que sobra |
|---|---|---|
| `loop-sdk-slot-por-turno` — o slot do gate é por turno, não por request; o default do doc diverge | O slot por turno é desenho intencional (RATE-003, defesa contra fan-out de subagentes), comentado no código. O "16 × 100" está só no JSDoc de `org-concurrency.ts:14`; o default real, o `.env`, os docs operacionais e o teste L4-X02 estão em 100. | Corrigir o JSDoc. A granularidade do slot vira decisão de produto (seção 7). |
| `loop-sdk-fanout-ilimitado` — fan-out ilimitado fura o rate limit e abre o circuit da org | As defesas RATE-001 (rate limit por org e usuário), RATE-003 (gate por org) e P-23 (circuit breaker de 3 hits/60s, 90s aberto) estão no código e nos testes; cada chamada ao LLM, de pai e de filho, adquire slot; os submits internos isentos do rate limit são intencionais. | A falta de teto **por árvore** foi confirmada em `subagentes-fanout-ilimitado` (3.4). |
| `loop-sdk-lockfile-divergente` — o runtime roda `ai` 5.0.179, mas o abort foi conferido na 5.0.253 | Erro factual: o Dockerfile do `be` roda `npm ci` com o `be/package-lock.json` (5.0.253 e 2.0.101); o 5.0.179 é do lock da raiz (dev e CI). O lock do `be` já estava em 5.0.253 antes do fix do abort. | Não existe `check-lock:be` para pegar drift do lock do `be` (risco baixo). |
| `loop-sdk-parse-args` — argumento ilegível vira `{}` e a tool executa assim mesmo | `parseToolArgs` só alimenta o evento enviado à UI; a tool executa com o input do SDK; JSON quebrado o SDK recusa. O caso de `workflow_publish` com definição vazia é comportamento documentado. | JSON válido com campo faltando executa: confirmado em `tools-prompt-args-sem-validacao` (3.3). |
| `tempo-real-fila-descarta-stop` — fila de envio cheia descarta Parar e reset em silêncio | O flush no `onopen` já reenvia stop e reset na reconexão; o descarte só ocorre com a fila saturada (100 itens ou 256 KB) no momento do clique — inviável no caminho típico; a mensagem de usuário é coalescida. | Só o silêncio na UI quando o descarte acontece (raro). |
| `tempo-real-resync-descarta-paginas` — o RESYNC troca o histórico paginado pelos 100 mais novos | O RESYNC poda com preservação (`pruneLiveOnResync`), mantendo o turno em andamento (há teste); páginas anteriores continuam paginando por REST; o ciclo "1 por minuto em aba ociosa" não casa com esse caminho. | Nada. |
| `dados-events-rebuild-full` — abrir o WS carrega a thread inteira e manda 1 frame por evento, travando a UI | O front ignora o replay (`runtime.ts:140`) e a UI já é paginada por REST; reabrir costuma acertar o cache quente do Redis. | O desperdício no servidor foi confirmado em `tempo-real-handshake-repete-historico` (3.2). |
| `dados-isbusy-local` — `isBusy` só enxerga a réplica local (como "alta") | Evidência correta, mas o doc oficial classifica como médio, a mitigação de stale (30 min) cobre a maioria dos casos e o `busy:false` no handshake é corrigido pelo próximo evento. | Confirmado como alta em `tempo-real-busy-local` (3.2): **contestado** entre revisores. |
| `dados-abort-sem-subscriber` — o Parar entre réplicas publica num canal que ninguém assina | Latente: produção roda 1 instância do `be` e todos os fluxos atuais abortam localmente. | Confirmado por 4 revisores de outras dimensões (crítica a média): tema transversal em 3.0. |
| `workflows-shared-redis-prod` — fila e cache na mesma instância Redis (perda silenciosa de jobs) | Decisão registrada pelo dono (`docs/operacao.md` §12, item 8); a instância está em `noeviction`, a política que o BullMQ exige. A perda descrita não está acontecendo. | Risco aceito: instância sem teto de memória para dois usos opostos; separar `REDIS_QUEUE_URL` é decisão do dono (seção 7). |
| `workflows-tls-db-prod-info` — Postgres sem TLS em produção | `DATABASE_TLS_POLICY=allow-insecure` foi decisão do dono (commit `1e36998`): o Postgres roda no mesmo host da aplicação; o aviso de boot cobre o caso de mudança de topologia. | Revisar se o Postgres sair do host. |


## 4. O Motor frente às melhores práticas

Legenda da coluna "Motor hoje": **coberto**, **parcial**, **não** ou **não avaliado** (o scan não mediu). Todas as afirmações sobre o Motor vêm das seções 2 e 3.

### 4.1 Arquitetura de agentes e multiagentes

| Prática (fonte) | Motor hoje | O que mudar |
|---|---|---|
| Começar simples e só adicionar complexidade medida por avaliação ([Anthropic — Building effective agents](https://www.anthropic.com/research/building-effective-agents)) | **Parcial.** A arquitetura já distingue chat, subagente e workflow; não há suíte de avaliação de qualidade nos dados do scan | Criar avaliações por caso de uso antes de ampliar fan-out ou trocar o loop |
| Separar workflow (caminho em código) de agente (o modelo decide) ([Anthropic](https://www.anthropic.com/research/building-effective-agents); [OpenAI Agents SDK — orquestração](https://openai.github.io/openai-agents-python/multi_agent/)) | **Coberto.** "`agent_spawn` é quando o LLM decide paralelizar; o workflow é quando a definição decide" | Manter; documentar ao modelo quando usar cada um |
| Chaining, routing, paralelização (seções e votação), orchestrator-workers e evaluator-optimizer ([Anthropic](https://www.anthropic.com/research/building-effective-agents)) | **Coberto** pelos 11 tipos de passo: `${steps.X.output}` (chaining), `switch` (routing), `map` (seções), `verify` (votação), orquestradora + `workflow_*` (orchestrator-workers), `loop` com `until` (evaluator-optimizer) | Routing ainda depende de um passo `agent` classificador; o campo `model` por passo é ignorado com aviso (`workflow.ts:178`) — avaliar alias de modelo por passo |
| Preferir sequencial; paralelizar só trabalho independente ([Cognition — Don't build multi-agents](https://cognition.com/blog/dont-build-multi-agents)) | **Parcial.** O prompt orienta frentes independentes; não há aviso sobre escrita concorrente no mesmo workspace | Particionar o escopo de escrita por filho e dizer isso no prompt (`subagentes-workspace-compartilhado`) |
| Agente-como-ferramenta como padrão; handoff só quando outro agente assume a conversa ([OpenAI Agents SDK — handoffs](https://openai.github.io/openai-agents-python/handoffs/)) | **Coberto** (subagente com retorno por notificação); não há handoff | Manter; se um dia houver handoff, filtrar o histórico (resumo não anonimiza) |
| Regras de dimensionamento: 1 agente com 3–10 chamadas; 2–4 subagentes para comparação; 10+ só em pesquisa complexa ([Anthropic — Multi-agent research system](https://www.anthropic.com/engineering/multi-agent-research-system)) | **Não.** Nenhuma regra de cardinalidade codificada; largura e profundidade ilimitadas | Escrever as regras no prompt da orquestradora e impor tetos determinísticos (5.4) |
| "Amortecedor" de delegação: só trilha grande, independente e paralela; nunca delegar para verificar o próprio trabalho; preferir 1 filho ([Anthropic — Prompting Claude Opus 5](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-opus-5)) | **Não** codificado | Incluir no system prompt da orquestradora (instrução orienta, teto impõe) |
| Modos de falha multiagente (MAST): repetição, ação incoerente, não encerrar; verificação em dois níveis e mensagens estruturadas ([Why do multi-agent LLM systems fail?](https://arxiv.org/html/2503.13657v3)) | **Parcial.** Tetos de run existem nos workflows; no chat, 999 steps sem teto por turno; a notificação é estruturada, mas vira texto com moldura de autoridade | Teto e condição de parada por turno e por árvore; envelope estruturado com intenção, status e incerteza; checagem do objetivo antes de notificar a conclusão |

### 4.2 Delegação e subagentes

| Prática (fonte) | Motor hoje | O que mudar |
|---|---|---|
| Contrato de delegação com objetivo, fronteiras, formato, tools, orçamento de esforço e condição de parada ([Anthropic — Multi-agent research](https://www.anthropic.com/engineering/multi-agent-research-system)) | **Parcial.** A descrição pede tarefa autocontida e o prompt pede "o resultado, não o caminho"; faltam fronteiras de escrita, orçamento e condição de parada | Contrato explícito no `agent_spawn` (5.4) |
| Contexto isolado, handoff só pelo prompt de delegação, retorno só do resultado final ([Claude Agent SDK — Subagents](https://code.claude.com/docs/en/agent-sdk/subagents)) | **Coberto.** O filho nasce sem o histórico e sem o system prompt do pai; devolve o `finalText` | Capar o retorno (hoje integral) |
| Filhas gravam artefatos e devolvem referências leves ([Anthropic — Multi-agent research](https://www.anthropic.com/engineering/multi-agent-research-system)) | **Não.** O `finalText` volta inteiro e fica no histórico do pai | Resumo denso de até 4–8 mil chars + referência ao `threadId` ou artefato |
| Background assíncrono com notificação; foreground só quando bloqueia ([Claude Code — Subagents](https://code.claude.com/docs/en/sub-agents)) | **Coberto e à frente:** a Anthropic ainda lista o modo assíncrono como evolução; o Motor já o opera com exactly-once e agrupamento | Manter; medir a latência ponta a ponta |
| Tetos determinísticos: profundidade, concorrência, orçamento e limite de turnos por filho ([Claude Agent SDK — Subagents](https://code.claude.com/docs/en/agent-sdk/subagents)) | **Parcial.** Gate de 100 chamadas por org (por processo) e orçamento mensal; sem profundidade, largura, orçamento por árvore nem limite de turnos por filho | Tetos por árvore com erros legíveis para a orquestradora (5.4) |
| Papéis com tools mínimas (allowlist) ([Claude Agent SDK — Subagents](https://code.claude.com/docs/en/agent-sdk/subagents)) | **Parcial.** O filtro `tools` do spawn só alcança MCP; as nativas passam | Aplicar o filtro também às nativas; padrão de negação |
| Escanear a saída do filho contra imitação de tags e marcadores de papel ([Claude Agent SDK — Subagents](https://code.claude.com/docs/en/agent-sdk/subagents)) | **Não.** O texto do filho entra cru dentro de `[NOTIFICAÇÃO DO SISTEMA]` | Delimitar como dado não confiável e neutralizar marcadores antes de entregar ao pai |
| Retomar subagente pela identidade, com transcript persistente ([Claude Agent SDK — Subagents](https://code.claude.com/docs/en/agent-sdk/subagents)) | **Não avaliado** pelo scan (o transcript do filho persiste na thread dele) | Avaliar retomada explícita de um filho pelo `threadId` |

### 4.3 Contexto e prompt

| Prática (fonte) | Motor hoje | O que mudar |
|---|---|---|
| Contexto mínimo útil, recuperação sob demanda e revelação progressiva ([Anthropic — Effective context engineering](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents)) | **Não.** Todo step leva todas as tools (362 num turno real) e o system completo | Núcleo pequeno + busca de tools; detalhes em descrições, não no prompt |
| System prompt "na altitude certa", mínimo e testado, com exemplos ([Anthropic — Effective context engineering](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents); [Prompting best practices](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices)) | **Parcial.** Prompt nativo de 25,9 mil chars com as mesmas regras repetidas nas descrições das tools; cita a tool inexistente `warpgrep`; afirma "sem MCP externo" | Fonte única; seção de tools gerada a partir das tools reais do turno |
| Compactação que preserva decisões; notas externas; filhos que devolvem resumos ([Anthropic — Effective context engineering](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents)) | **Parcial.** Compactação própria na fronteira do turno, com checkpoint; só enxerga a explosão no turno seguinte; sem notas externas por thread | Teto por step; avaliar memória por thread em store dedicada |
| Context editing no servidor para limpar resultados antigos de tools ([Anthropic — Context editing](https://platform.claude.com/docs/en/build-with-claude/context-editing), beta) | **Não** | Avaliar quando o LB repassar o header beta (5.6) |
| Histórico só com acréscimo; blocos de thinking devolvidos intactos ([Anthropic — Thinking](https://platform.claude.com/docs/en/build-with-claude/thinking)) | **Parcial.** Histórico append-only no banco; mas o reasoning é removido antes de persistir, então o replay de thinking assinado não funciona | Decidir: poda sempre (documentada) ou guardar as assinaturas e devolvê-las byte a byte |
| Thinking adaptativo com `effort` (não `budget_tokens`) ([Anthropic — Thinking](https://platform.claude.com/docs/en/build-with-claude/thinking)) | **Coberto** pelo contrato com o LB (adaptativo, resumido, effort alto); há código morto com `budget_tokens` | Remover o código morto; effort por tipo de tarefa e fixo por thread (não quebra cache) |

### 4.4 Tools

| Prática (fonte) | Motor hoje | O que mudar |
|---|---|---|
| Poucas tools compostas por fluxo, em vez de CRUD cru ([Anthropic — Writing tools for agents](https://www.anthropic.com/engineering/writing-tools-for-agents)) | **Parcial.** As 23 nativas são compostas; os MCPs entram como o servidor os expõe | Curadoria de MCPs por setup |
| Namespaces por serviço e recurso ([Anthropic — Writing tools](https://www.anthropic.com/engineering/writing-tools-for-agents)) | **Não.** Nomes crus; colisão decidida em silêncio pelo último MCP | `servidor__tool` com alias curto, validado por avaliação |
| Descrições explícitas e sem ambiguidade; erros acionáveis com exemplo ([Anthropic — Writing tools](https://www.anthropic.com/engineering/writing-tools-for-agents)) | **Parcial.** Descrições nativas ricas (e longas); `workflow_*` recusa argumento ruim com erro legível; `agent_spawn` não valida | Validação de argumentos em todas as nativas |
| Paginação, filtro e truncamento com padrões sensatos (o Claude Code limita resposta de tool a ~25 mil tokens) ([Anthropic — Writing tools](https://www.anthropic.com/engineering/writing-tools-for-agents)) | **Não.** Resultado de tool sem teto (só o workflow corta em 6 mil chars) | Teto por resultado com marcador e leitura paginada |
| Busca de tools com `defer_loading` para catálogos grandes ([Anthropic — Tool search tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool); [Advanced tool use](https://www.anthropic.com/engineering/advanced-tool-use)) | **Não.** Um passo "trivial" de workflow custou US$ 0,25 e ~83 mil tokens de entrada, boa parte de catálogo | Adotar quando o LB repassar server tools; antes disso, `activeTools` por step com seleção própria |
| Retornos semânticos (nomes, não IDs crus) e verbosidade controlável ([Anthropic — Writing tools](https://www.anthropic.com/engineering/writing-tools-for-agents)) | **Não avaliado** para os MCPs; o `shortId` de subagente existe e não é usado (o id exposto é UUID) | Avaliar nos MCPs próprios (MyPanel e AgentPack) |
| Autorização no ponto da tool; guardrail como camada, não como permissão ([OpenAI Agents SDK — Guardrails](https://openai.github.io/openai-agents-python/guardrails/)) | **Parcial.** `toolPolicy` por setup e negação de tools dentro de passos; SSRF aberto em 4 superfícies; MCP de thread aceito sem allowlist | Guard SSRF em toda saída; allowlist de servidores por org |

### 4.5 Loop, API Messages e SDK

| Prática (fonte) | Motor hoje | O que mudar |
|---|---|---|
| Consumir o SSE por eventos tipados; acumular `input_json_delta` até `content_block_stop` ([Anthropic — Streaming](https://platform.claude.com/docs/en/build-with-claude/streaming)) | **Coberto** pelo pacote `ai` | No SDK nativo, `messages.stream` com `finalMessage` ([TypeScript SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript)) |
| Tool use manual: `tool_result` primeiro, todos os resultados numa mensagem só, `is_error` instrutivo ([Handle tool calls](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls); [Parallel tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/parallel-tool-use)) | **Coberto** pelo SDK; argumento inválido não vira `is_error` | No loop nativo: validar contra o schema e devolver `is_error` em vez de executar |
| Ramificar por `stop_reason` (`tool_use`, `pause_turn`, `refusal`, `max_tokens`, janela excedida) ([Handling stop reasons](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons)) | **Parcial.** Overflow de janela vira erro terminal sem compactar e retentar | Ramo próprio para cada motivo (5.1) |
| Cancelamento com `AbortController` e `stream.controller.abort()` ([SDK helpers](https://github.com/anthropics/anthropic-sdk-typescript/blob/main/helpers.md)) | **Coberto** com nome mágico `AbortError` + guarda de 5s | Cancelamento documentado do SDK nativo, mantendo a guarda |
| Retries só para transitórios (408, 409, 429, 5xx, rede), respeitando `retry-after`; timeout por request ([TypeScript SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript); [Rate limits](https://platform.claude.com/docs/en/api/rate-limits)) | **Parcial.** 3 retries sem jitter visível; nenhum timeout padrão | Jitter, timeout por request e detector de stall |
| Prompt caching com até 4 breakpoints e TTL de 5 min ou 1 h ([Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching)) | **Parcial.** O seed usa 2 de 4 breakpoints (system e último user com 1 h); o default do schema é 5 min e diverge do contrato; `cache_creation` não é contado | TTL e breakpoints declarativos por setup; contabilizar a escrita de cache |
| Contar tokens antes de enviar ([Token counting](https://platform.claude.com/docs/en/build-with-claude/token-counting)) | **Não.** Estimativa por chars/4 que ignora as tools | Contar com o modelo de destino quando o LB permitir; senão, incluir as tools na estimativa |
| Batch API para volume assíncrono, com 50% de desconto ([Batch processing](https://platform.claude.com/docs/en/build-with-claude/batch-processing)) | **Não** | Avaliar para avaliações, backfills e classificações em lote |
| Tool runner do SDK como base do loop, com livro-razão durável fora dele ([TypeScript SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript), beta) | **Não** | Avaliar na migração; a persistência por step continua sendo do Motor |
| Conferir GA × beta antes de produção ([Features overview](https://platform.claude.com/docs/en/build-with-claude/overview)) | **Não avaliado** | Recursos beta só atrás de flag por setup, com fallback |

### 4.6 Execução durável de workflows (mercado)

| Prática (fonte) | Motor hoje | O que mudar |
|---|---|---|
| Cada chamada externa como passo durável com identidade estável ([Inngest — Steps](https://www.inngest.com/docs/learn/inngest-steps); [DBOS](https://docs.dbos.dev/typescript/tutorials/workflow-tutorial)) | **Coberto e à frente:** `stepKey` determinístico, `jobId` determinístico, claim condicional, unique por tentativa e "passo com output nunca reexecuta" | Manter |
| Idempotência no destino: o journal não substitui a deduplicação externa ([Restate — Durable steps](https://docs.restate.dev/develop/ts/durable-steps)) | **Parcial.** No máximo um passo por run pode ser pago duas vezes num crash; o passo `http` depende de chave de idempotência do autor | Gerar e enviar uma chave de idempotência por passo nas chamadas externas |
| Retry no nível da atividade, com backoff exponencial com teto e erros não retentáveis ([Temporal — Retry policies](https://docs.temporal.io/retry-policies); [Activities](https://docs.temporal.io/activities)) | **Coberto:** até 10 retries, backoff de até 600s, 4xx terminal, 5xx retenta, 429 com `Retry-After` | Manter |
| Humano no loop com estado persistido e retomada ([LangGraph — Interrupts](https://docs.langchain.com/oss/python/langgraph/interrupts); [AutoGen — Human in the loop](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/human-in-the-loop.html)) | **Coberto:** passo `human` com `waiting` persistido e timeout de até 30 dias | Manter |
| Terminação explícita com motivo ([AutoGen — Teams](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/teams.html)) | **Coberto** nos workflows (orçamento, `maxIterations`, `until`, motivo de parada); **não** no chat (999 steps) | Teto e motivo de parada também no turno do chat |
| Controle determinístico no fluxo; agentes só dentro dos passos ([CrewAI — Flows](https://docs.crewai.com/concepts/flows)) | **Coberto** pelo reconciliador | Manter |
| Trace único por workflow, com redação de dados sensíveis ([OpenAI Agents SDK — Tracing](https://openai.github.io/openai-agents-python/tracing/)) | **Parcial.** Trilha de auditoria (Turn com snapshot e hash, events com `seq`, log do run), sem painel do que foi enviado; o snapshot grava segredos em claro | Painel de inspeção com redação (5.3) |
| Estado serializável para retomar em infraestrutura sem estado ([AutoGen — State](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/state.html)) | **Parcial.** Estado durável no Postgres, mas o convo do turno vive em memória sem fallback | Fallback do convo (memória → Redis → banco) |

### 4.7 Tempo real

| Prática (fonte) | Motor hoje | O que mudar |
|---|---|---|
| Salas por thread e usuário, estado só no servidor ([Socket.IO — Rooms](https://socket.io/docs/v4/rooms/)) | **Parcial.** Salas por thread, em memória de processo | Salas distribuídas por adapter |
| Não assumir entrega (padrão "no máximo uma vez"); persistir antes de emitir e reenviar por cursor ([Socket.IO — Delivery guarantees](https://socket.io/docs/v4/delivery-guarantees/)) | **Parcial.** O histórico persiste antes; não há `seq` por sala nem ack; o replay reenvia tudo | `seq` por sala com replay a partir do cursor do cliente |
| Recuperação de estado para quedas curtas, com fallback de ressincronização ([Connection state recovery](https://socket.io/docs/v4/connection-state-recovery/)) | **Não** | `connectionStateRecovery` (exige o adapter de Redis Streams) + replay por `seq` |
| Adapter Redis entre nós; o Streams adapter suporta recuperação ([Redis Streams adapter](https://socket.io/docs/v4/redis-streams-adapter/); [Redis adapter](https://socket.io/docs/v4/redis-adapter/); [Multiple nodes](https://socket.io/docs/v4/using-multiple-nodes/)) | **Não** (o runbus usa Pub/Sub próprio só para runs) | Redis Streams adapter; sticky só se mantiver o transporte por polling |
| Autenticar no handshake, com renovação de token ([Socket.IO — Middlewares](https://socket.io/docs/v4/middlewares/)) | **Coberto** no upgrade (bearer, cookie, ticket de uso único) | Revalidar a sessão periodicamente |
| Reconexão com backoff e jitter; ack com timeout para comandos ([Client options](https://socket.io/docs/v4/client-options/); [Emitting events](https://socket.io/docs/v4/emitting-events/)) | **Parcial.** Backoff sem jitter; sem ack | Jitter; ack para envio, Parar e reset |
| Backpressure na aplicação: agregar deltas, `volatile` para intermediários, marcos numerados ([Socket.IO — Emitting events](https://socket.io/docs/v4/emitting-events/); [Server options](https://socket.io/docs/v4/server-options/)) | **Parcial.** O canal de run coalesce a 2 Hz; o chat não faz streaming token a token | Coalescer deltas no chat (janela de 30–100 ms) se ligar o streaming |
| Uma conexão multiplexando várias conversas ([OpenAI — WebSocket mode](https://developers.openai.com/api/docs/guides/websocket-mode)) | **Não.** 1 socket por thread + 1 socket só para a sidebar | Uma conexão por aba com várias salas |
| SSE como transporte só entre servidor e provider ([Anthropic — Streaming](https://platform.claude.com/docs/en/build-with-claude/streaming)) | **Coberto:** SSE só entre o SDK e o LB; a UI usa WebSocket | Manter |


## 5. Arquitetura-alvo proposta

Princípio: completar o que o desenho já prevê, sem big-bang. Cada mudança estrutural entra atrás de flag, com shadow e rollback.

### 5.1 Loop com o SDK nativo da Anthropic (`@anthropic-ai/sdk`, API Messages, loop próprio)

O SDK nativo troca só o transporte e o loop. **O contrato com o LB fica igual**: Anthropic Messages, `model: "proxy-managed"`, SSE, `metadata.user_id` estável por conversa.

```mermaid
flowchart TD
    A[montar request<br/>system em camadas + messages do log<br/>tools com cache_control orçado] --> B[messages.stream no proxy LB]
    B --> C[consumir SSE<br/>deltas para o sink, inputs de tool acumulados<br/>usage com cache_creation]
    C --> D{stop_reason}
    D -->|end_turn| E[persistir e fechar o turno]
    D -->|tool_use| F[validar inputs contra o schema<br/>executar em paralelo com timeout<br/>todos os tool_result numa mensagem]
    F --> G[persistir o step<br/>conferir orçamento e tetos<br/>anexar notificações já persistidas]
    G --> B
    D -->|max_tokens, refusal, pause_turn| H[ramo próprio por motivo]
    D -->|janela estourada| I[compactar + 1 retry]
```

1. **Montagem por turno.** `system` em blocos (as camadas do builder de 5.3); `messages` traduzidas do log de events (user e assistant com blocos `text`, `tool_use` e `tool_result`; thinking preservado byte a byte ou podado por flag); `tools` com `input_schema`, em ordem alfabética estável, com `cache_control` dentro do orçamento de 4 breakpoints (prioridade: system, último user, checkpoint, tools).
2. **Por request.** `client.messages.stream({ model: 'proxy-managed', max_tokens: 64000, system, messages, tools, thinking: { type: 'adaptive', display: 'summarized' }, output_config: { effort }, metadata: { user_id } })` apontado para o proxy. **Confirmar no repositório oficial** os nomes exatos das opções de endpoint e autenticação (`baseURL`, `apiKey` ou `authToken`, headers por request) antes de codar.
3. **Consumo do SSE.** Deltas de texto vão para o sink (WebSocket); inputs de tool acumulados até `content_block_stop`; usage lido de `message_start` e `message_delta`, **incluindo** `cache_creation_input_tokens`.
4. **`stop_reason`.** `end_turn` persiste e fecha. `tool_use` valida cada input contra o schema (inválido vira `tool_result` com `is_error`, nunca `{}` silencioso), executa em paralelo (`Promise.allSettled`, timeout por tool, abort encadeado), anexa **todos** os resultados numa única mensagem `user`, **persiste o step** (fecha o buraco do crash), confere orçamento e tetos de steps e tokens e continua. `max_tokens`, `refusal`, `pause_turn` e janela excedida têm ramos próprios; o 400 de janela estourada dispara compactação e 1 retry automático.
5. **Cancelamento.** Parar, timeout e orçamento chamam `stream.controller.abort()` e abortam as tools; a guarda de 5s fica como rede; detector de stall (N minutos sem evento SSE).
6. **Retries e erros.** Só transitórios tipados (429, 5xx, rede), respeitando `retry-after`, com jitter. O SDK oficial tem `maxRetries` (padrão 2) e timeout por request (padrão 10 min) — configurar explicitamente; os erros tipados do SDK (limite de taxa, erro interno, timeout de conexão) viram `errorKind` próprios, inclusive a negação do gate de concorrência como "ocupado, tente de novo".
7. **Notificações no meio do turno.** Append direto em `messages` entre requests, **só depois de persistir** — o problema do P-07 desaparece por construção, junto com o buffer acumulado manual.
8. **Compactação.** A própria continua na fronteira, com fallback de emergência; o compactador passa pelo mesmo slot e entra no custo.

**Ganhos.** Controle total do array por request; cache fino (TTL de 1h, leitura de `cache_creation`); thinking assinado preservado; cancelamento previsível; retries e timeouts explícitos; fim do reparse do body por step e do interceptor; caminho aberto para tool search, context editing e structured outputs (que pode aposentar o scraping de JSON do `agent-step`) — **se o LB repassar** betas e server tools.

**Riscos.** Reescrever o runner de tools (paralelismo, validação, JSON parcial, `is_error`); traduzir o histórico legado (o Postgres guarda formas do AI SDK); quebrar o contrato byte a byte com o LB (o SDK oficial compõe o caminho `/v1/messages` a partir do `baseURL`, diferente do AI SDK; headers `anthropic-version` e betas; nomes de eventos SSE; `Authorization: Bearer` com `x-api-key` placeholder; `metadata.user_id`); perder a abstração multiprovider (que o Motor hoje não usa).

**Migração em 4 etapas.**
1. Tradutor de histórico (log ↔ Messages nativo) com suíte de paridade sobre amostra real; snapshot do wire atual validado contra o LB real, junto com o dono do LB.
2. Interface `AgentTransport` com duas implementações (AI SDK e nativa); runner nativo atrás de flag por setup; shadow-diff do wire.
3. Compactador no transporte novo.
4. Nativo como padrão, com rollback por flag; depois, remover `ai` e `@ai-sdk/anthropic`.

**Esforço.** A investigação de lacunas estima ~400 linhas alteradas, concentradas em `loop.ts`, e 6–8 dias úteis de um dev sênior com a rede de testes. A dimensão do loop estima semanas, contando o runner de tools, o histórico legado, o compactador e a bateria de regressão. **Planejar com a estimativa maior.**

### 5.2 Tempo real só por WebSocket, com socket.io

```mermaid
flowchart LR
    FE[front: 1 conexão por aba<br/>salas user, thread, run] -->|handshake autenticado<br/>ack em user_message, stop, reset| GW[gateway socket.io /v1<br/>em cada réplica da API]
    GW --- AD[(Redis Streams adapter<br/>salas globais + recuperação)]
    WK[worker: passos de workflow] -->|emite nas salas run| AD
    ENG[engine do turno] -->|eventos com seq e v<br/>persistidos antes de emitir| AD
    GW -->|replay por seq| DB[(Postgres: events e workflow_events)]
```

- **Gateway** socket.io ao lado do `/ws` atual (caminho próprio), com **Redis Streams adapter** sobre o Redis existente — suporta recuperação de conexão e retoma depois de queda do Redis; o adapter Redis clássico não suporta recuperação. Isso aposenta o broadcaster em memória.
- **Salas montadas pelo servidor**, a partir da identidade do handshake e da checagem de dono: `org:{org}`, `user:{org}:{user}`, `thread:{id}`, `run:{id}`. O cliente pede para entrar e o servidor valida (o gate de tenant DM-17 continua). Namespace único versionado (`/v1`); a investigação de lacunas também sugere namespaces `/thread` e `/run` — escolher um.
- **Autenticação** no handshake com o que já existe (cookie de sessão, bearer de máquina, ticket de uso único); revalidação periódica; fim de sessão publicado para todas as réplicas.
- **Servidor → cliente:** `seq` monotônico por sala e `v` (versão) em todo frame; eventos persistidos **antes** de emitir; `connectionStateRecovery` (ex.: até 2 min, sem pular middleware) para quedas curtas; com `recovered = false`, o cliente pede o que veio depois do último `seq` e o servidor faz replay a partir do banco. Histórico continua 100% por REST paginado; handshake magro.
- **Cliente → servidor com ack e timeout:** `user_message` (ack com `enqueued` e `clientMessageId`), `stop`, `reset`, `join`. O `clientMessageId` vira chave de idempotência persistida (unique na mailbox) e o ack passa a ser a confirmação de entrega.
- **Tokens:** se ligar streaming no chat, `assistant_delta` com `seq`, coalescido em janelas de 30–100 ms e `volatile`; marcos (fim de bloco, resultado de tool, `final`) persistidos e numerados.
- **Worker → API:** o worker emite nas salas pelo adapter (um barramento só no lugar do runbus próprio), ou mantém o runbus com subscribe por run; `serverSideEmit` para mensagens entre servidores (JSON, sem binário).
- **Estado distribuído:** `busy` derivado do lock no Redis e publicado na sala `user:`; abort por canal assinado, com `AbortReason` serializado; uma conexão por aba (a sidebar usa a sala `user:`).
- **Proxy HTTP na frente do Motor** (o do painel; não confundir com o Load Balance de LLM): upgrade de WebSocket e timeout de leitura acima de 45s (ping de 25s + 20s); sticky só se mantiver o transporte por polling (com `transports: ['websocket']` não é necessário, perdendo o fallback); `maxHttpBufferSize` explícito (padrão do socket.io: 1 MB; hoje o WS aceita 8 MiB) enquanto houver imagem inline — o alvo é upload por HTTP para `attachments` e referência no evento.
- **Catálogo de eventos** versionado e único entre be e fe, regra aditiva, `protocol: {min, max}` no handshake; tipo desconhecido vira métrica.
- **Métricas:** frames por tipo e canal, conexões e reconexões, bytes de replay, descartes, tipos desconhecidos; gauges de salas e sockets por sala.

| Fase | O que entra | Polling que sai |
|---|---|---|
| F0 | gateway, adapter, salas e métricas, em paralelo ao `/ws` | — |
| F1 | `thread_busy` com `seq` na sala `user:` | 2º socket da sidebar |
| F2 | `thread.created`, `thread.updated`, `thread.deleted` na sala `user:` | lista de threads (8s) |
| F3 | `subagent.spawned`, `subagent.result`, `subagent.status` na sala da raiz | árvore e graph (3–5s); modal (2s + 3s) |
| F4 | `run.status` e `step.updated` nas salas `user:` e `run:` | lista de runs (5s); log (4s); passo (3s) |
| F5 | saúde pelo estado do socket + `navigator.onLine` (`readyz` só para infra) | saúde (30s) |
| F6 | handshake magro; opcional: streaming token a token no chat | — |

**Corte.** socket.io não conversa com cliente WebSocket puro (nem o contrário): o corte é por cliente. Rodar os dois em caminhos separados, com shadow (comparar frames), antes de desligar o `/ws`. A investigação de lacunas estima ~5–7 dias úteis para a troca de transporte, além das fases F1–F6.

**Alternativa (decisão do dono):** manter o `ws` nativo e publicar os eventos de turno no Redis (o canal `:stream` já existe) com um assinante por réplica. Resolve as várias réplicas com menos mudança, mas sem ack, recuperação e salas prontas: `seq`, replay e ack teriam de ser construídos à mão.

### 5.3 Tools e system prompt modulares, com painel de inspeção

**Builder de prompt por camadas.** Cada camada tem nome, versão, hash e tamanho, em ordem fixa: (1) núcleo do Motor; (2) política da org; (3) persona do setup; (4) instruções da thread; (5) voz; (6) notificações, sempre como dado delimitado. Semântica explícita: o padrão sugerido é **somar com rótulo**; substituir só com flag explícita e aviso na UI. A seção "suas ferramentas" é gerada a partir das tools realmente montadas no turno (acaba o "sem MCP externo" e o `warpgrep`). O arquivo base fica em cache por mtime.

**Registro de tools.**
- Namespace `servidor__tool` com alias curto; colisão com schemas diferentes vira erro no boot; a `toolPolicy` cita servidor ou `servidor__tool`.
- Tetos: tamanho de descrição e schema por tool; resultado de 20–50 mil chars com `[TRUNCADO]` e leitura paginada; steps por turno (padrão 25–50, configurável) e tokens por turno.
- Validação de argumentos antes de executar; inválido vira erro legível.
- Origem marcada (`[mcp:servidor]`), allowlist e pin de servidores por org, guard SSRF no `connectMcp` e no refresh de credencial.
- Núcleo pequeno + carregamento sob demanda: tool search com `defer_loading` (se o LB repassar) ou seleção própria por request (`activeTools`).

**Módulos** (sugeridos pela investigação de lacunas): `tools/transport.ts` (handshake, listagem, timeouts, refresh); `tools/serialize.ts` (cache_control, metadata, log); `agent/system-prompt.ts` (o builder); `engine/spawn-policy.ts` (idempotência, herança, tetos).

**Painel de inspeção — "o que foi enviado ao modelo".**
- Persistir por turno (e por step, quando algo mudar): nome, hash e tamanho de cada camada do system; lista final de tools com origem, tamanho e se veio sob demanda; breakpoints de cache plantados; tokens de input, output, leitura e **escrita** de cache; compactação aplicada; notificações injetadas e em que ponto; config efetiva **redigida** (sem segredos) e seu hash.
- Expor num endpoint de debug do turno e numa aba "Inspecionar" na timeline e no detalhe do passo de workflow, com diff entre turnos (o que mudou no prefixo e quebrou o cache).
- Acesso: dono da thread e admin.

### 5.4 Protocolo agente↔subagente

```mermaid
sequenceDiagram
    participant O as Orquestradora
    participant M as Motor
    participant F as Filho
    O->>M: agent_spawn com contrato<br/>objetivo, fronteiras, formato, tools, orçamento, parada
    M->>M: confere tetos de profundidade, largura e orçamento da árvore
    M->>F: thread própria + tarefa
    F->>M: resultado: status, resumo de até 4–8 mil chars,<br/>referência ao threadId ou artefato, usage
    M->>O: notificação delimitada como dado não confiável
    O->>M: Parar ou agent_abort
    M->>F: abort em cascata na subárvore
```

- **Spawn com contrato:** objetivo; fronteiras (inclusive escopo de escrita — arquivos ou worktree por filho); formato de entrega; tools (allowlist aplicada também às nativas); orçamento (steps, tokens ou USD) e condição de parada; setup, com regra de persona explícita.
- **Tetos determinísticos:** profundidade, largura por pai, filhos simultâneos por org (distribuído), orçamento por árvore e limite de turnos por filho com resultado parcial; recusa com erro legível que oriente a consolidar.
- **Retorno:** status, resumo denso (até 4–8 mil chars), referência ao `threadId` ou artefato e usage; delimitado como dado não confiável, com marcadores de papel neutralizados.
- **Ciclo de vida:** cancelamento em cascata pela árvore (ou raiz marcada como cancelada, descartando notificações de subárvore morta); reaper ligado em uma réplica, com liderança; abort distribuído com causa tipada.
- **Prompt da orquestradora:** amortecedor de delegação e regras de cardinalidade (4.1); aviso de workspace compartilhado.
- **Tempo real:** o pai assina a sala da família e vê o progresso dos filhos sem polling.

### 5.5 Workflows

Manter o núcleo — reconciliador, barreiras, snapshot por setup, pause e cancel, custo consolidado —, que está à frente do mercado. Mudanças:
- tetos padrão da instalação em produção (`WF_DEFAULT_MAX_*`), aviso explícito no `workflow_draft` e, opcionalmente, `WF_REQUIRE_BUDGET`;
- lista de passos sem `output` (só no detalhe e no frame `step_output`); log do run por cursor de `seq`; lista de runs numa query só;
- correções de corrida (`requestResume`, `requestPause`, `startRun` com a mesma chave), prazo de pausa e renovação da lease em ticks longos;
- no passo `agent`: cache declarativo por setup (TTL de 1h e breakpoints), curadoria e `defer_loading` de tools por setup, alias de modelo por passo para routing, `ttftMs` e taxa de acerto de cache por passo, e `WF_AGENT_MAX_TOOL_STEPS` definido;
- agendamento (`schedule`): implementar ou tirar do enum e documentar;
- longo prazo: sinalizar um sub-run em andamento (hoje só há pause e cancel).

### 5.6 Contratos entre aplicativos

**Motor ↔ Load Balance (proxy de LLM):**

| Item | Hoje | Proposta |
|---|---|---|
| Versão do contrato | nenhuma no wire | `X-Motor-Contract: lb-v1` no request e eco na resposta; o Motor loga e alerta se faltar |
| Caminho e autenticação | o AI SDK compõe `/messages` a partir do `baseURL` normalizado; `Authorization: Bearer` + `x-api-key` placeholder | com o SDK oficial, confirmar `/v1/messages`, `anthropic-version`, betas e autenticação; snapshot do wire atual comparado em shadow |
| Usage | o contrato (§6) diz que o total soma input, leitura e escrita de cache; o Motor só lê os dois primeiros | o LB garantir `cache_creation_input_tokens` em `message_start`/`message_delta`; o Motor passa a ler |
| Custo e modelo | o Motor nunca sabe o modelo; preço fixo de Sonnet | o LB informar o modelo real e/ou o custo por request (header ou evento SSE), ou o Motor manter preço por rota |
| Erros | `overloaded_error` chega sem status | status HTTP coerente e `retry-after` em 429, 503 e 529 |
| Recursos novos | — | o LB repassar betas e server tools (tool search, context editing, compactação) se o Motor adotar; confirmar antes de prometer |
| Roteamento de cache | `metadata.user_id` estável por conversa | manter o formato |

**Outros aplicativos:**
- **Proxy HTTP do MyPanel** na frente do Motor: upgrade de WebSocket, timeout de leitura acima de 45s, sticky se houver transporte por polling.
- **Conta (SSO/OIDC) e webhook de provisionamento:** versão explícita no envelope; recusar versão maior desconhecida.
- **MCPs do MyPanel e do AgentPack:** negociar e logar a versão do protocolo; namespace e pin da lista de tools; guard SSRF; descrições compactas e respostas paginadas (aqui o Motor controla os dois lados).
- **Webhooks de saída** (clientes do Motor): `https` obrigatório, `safeRequest`, versão no envelope, assinatura mantida.
- **Frontend:** catálogo de eventos versionado e compartilhado; `protocol: {min, max}` no handshake.
- **Worker ↔ API:** um barramento só (adapter do socket.io) ou runbus com subscribe por run.


## 6. Roadmap priorizado em ondas

Esforço: P (horas a 1 dia), M (dias), G (semana ou mais). Risco = risco da mudança em si.

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

| # | Ação | Achados | Esforço | Risco | Depende de |
|---|---|---|---|---|---|
| 1.1 | Teto de steps por turno no chat (ex.: 50, configurável por setup), teto de tokens por turno e alerta | `loop-sdk-maxsteps-999`, `tools-prompt-maxsteps-999` | P | baixo (tarefas longas legítimas: tornar configurável) | — |
| 1.2 | Timeout padrão do turno ou detector de stall | `loop-sdk-timeout-zero` | P | baixo | — |
| 1.3 | `sideEffectWrite` com retry real; notificação no meio do turno só depois de persistir, com reenfileiramento | `loop-sdk-sideeffect-retry`, `loop-sdk-p07-injeta-sem-persistir` | P a M | baixo | — |
| 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 | `seguranca-ssrf-01` a `04`, `tools-prompt-ssrf-mcp-test`, varredura de lacunas | P a M | baixo (MCP interno legítimo: exceção explícita por env) | — |
| 1.5 | Redigir o `configSnapshot` antes de gravar (hash calculado antes); headers no escopo da thread; o front não reenviar `•••` | `tools-prompt-snapshot-com-segredos`, `seguranca-segredo-01` | P | baixo | — |
| 1.6 | Abort entre réplicas: assinante + `AbortReason` serializado; `abortChild` só marca `aborted` se confirmou | tema "Parar/abort" (3.0) | P | baixo | — (pré-requisito da 2ª réplica) |
| 1.7 | Ligar `REAPER_ENABLED=1` em exatamente uma réplica, com alerta no boot | `subagentes-reaper-desligado`, `dados-reaper-desligado` | P | médio: o reaper usa o `isBusy` local, seguro com 1 réplica; antes da 2ª, ver 2.3 | — |
| 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 | `subagentes-notificacao-sem-teto`, `tools-prompt-sem-limite-resultado`, `tools-prompt-notificacao-role-user` | P a M | baixo | — |
| 1.9 | Validação de argumentos nas tools nativas | `tools-prompt-args-sem-validacao` | M | baixo | — |
| 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 | `tempo-real-watchdog-reconecta-ocioso`, `-backoff-sem-jitter`, `-busy-travado-desconexao`, `-dialog-estoura-ratelimit` | P | baixo | — |
| 1.11 | Confirmar `SHOW max_connections` em produção e alinhar ao runbook (≥ 2100) ou ajustar `DB_CONNECTION_LIMIT` | `dados-pool-1000` × lacuna | P | médio (mudar o Postgres exige restart) | decisão do dono |
| 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_*` | vários (3.1–3.5) | P | baixo | valores: decisão do dono |
| 1.13 | `/info` protegido ou enxuto; parar de ecoar a mensagem do provider; remover (ou reaproveitar) o publish do canal `:stream` | `seguranca-info-01`, `-info-02`, `tempo-real-events-canal-sem-ouvinte` | P | baixo | — |

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

| # | Ação | Achados | Esforço | Risco | Depende de |
|---|---|---|---|---|---|
| 2.1 | Fallback do convo (memória → Redis → banco) — vale já com 1 réplica | `dados-convo-redis-writeonly` | M | médio | — |
| 2.2 | `cache_creation` no contexto, custo e orçamento; preço por rota ou setup, ou custo vindo do LB | `loop-sdk-usage-cache-creation`, `loop-sdk-preco-fixo` | M | baixo | coordenação com o LB |
| 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 | tema "Estado local" (3.0) e afins | M | médio | 1.6 |
| 2.4 | Tetos por árvore de subagentes, abort em cascata, filtro de tools também nas nativas, persona e escopo de escrita explícitos | `subagentes-*` | M | médio (muda o comportamento do agente: medir) | — |
| 2.5 | Cross-tenant: `loadEffectiveConfig` com a `OrgConfig` da org do turno | `seguranca-cross-tenant-01` | G | alto (toda a resolução de config; testar por org) | **antes** de liberar troca de org na UI |
| 2.6 | Infra: separar `REDIS_QUEUE_URL`; PgBouncer ou `max_connections` dimensionado; reaper com liderança | `dados-pool-1000`; risco aceito do Redis (3.9) | M | médio | decisões do dono |
| 2.7 | Workflows: lista de passos sem `output`; log por cursor; lista de runs numa query; corridas de pause/resume/start; prazo de pausa | `workflows-*` | M | baixo | — |
| 2.8 | Compactação: overflow → compactar + retry; respeitar o Parar; compactador no slot e no custo; checagem por step | `loop-sdk-*` de compactação | M | médio | — |
| 2.9 | Protocolo WS versionado (`v`, catálogo único) e métricas do tempo real | `tempo-real-protocolo-sem-versao`, `-metricas-cegas` | M | baixo | — (base da onda 3) |
| 2.10 | Namespace de tools e `toolPolicy` por `servidor__tool`; seção de tools gerada; fonte única de instruções | `tools-prompt-*` | M | médio (muda nomes vistos pelo modelo; quebra o cache uma vez) | avaliação antes e depois |
| 2.11 | Retenção de events só de exibição; índices compostos; cache de autenticação; cache da lista de webhooks | `dados-*` | M | baixo | decisão de retenção |

### Onda 3 — Estrutural (meses)

| # | Ação | Seção | Esforço | Risco | Depende de |
|---|---|---|---|---|---|
| 3.1 | Loop nativo `@anthropic-ai/sdk` (tradutor, `AgentTransport`, shadow, flag por setup, persistência por step) | 5.1 | G | alto (contrato com o LB) | snapshot do wire e acordo com o dono do LB; decisão sobre replay de thinking |
| 3.2 | socket.io F0–F6 com Redis Streams adapter, salas, `seq`, ack e recuperação | 5.2 | G | médio a alto (corte por cliente) | decisão socket.io × `ws`; 2.3 e 2.9; config do proxy |
| 3.3 | Tools sob demanda, curadoria por setup e painel de inspeção | 5.3 | G | médio | LB repassar server tools (para tool search); 2.10 |
| 3.4 | Contratos versionados (LB, SSO, MCP, webhooks) | 5.6 | M | baixo | acordo com os donos dos outros apps |
| 3.5 | Avaliações reais (~20 casos por uso, juiz LLM e revisão humana) | 4.1 | M | baixo | — |
| 3.6 | Workflows: cache declarativo, alias de modelo por passo, agendamento, sinal para sub-run | 5.5 | M a G | baixo | — |
| 3.7 | Persistência por step; particionamento ou arquivamento de events | 3.1, 3.6 | G | médio | 3.1 |

```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
```

Leitura do grafo: **a 2ª réplica da API só deve subir depois de 1.6, 2.3, 2.6 e do broadcast distribuído** (3.2 ou a alternativa `ws` + pub/sub); **a troca de org na UI só depois de 2.5**; tool search e o custo exato dependem do LB.

## 7. Perguntas de negócio e decisões do dono

**Decisões necessárias:**
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.
2. **Banco:** confirmar `max_connections` em produção; manter "nunca reduzir o pool" e subir o Postgres para ≥ 2100, ou adotar PgBouncer?
3. **Redis:** separar fila e cache (`REDIS_QUEUE_URL`), hoje juntos numa instância de 512 MB sem teto de memória?
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?
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.
6. **SDK nativo:** aprovar a migração incremental, abrindo mão da abstração multiprovider (hoje o Motor só fala Anthropic via LB)?
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?
8. **Multi-org:** quando a UI terá troca de org? O conserto cross-tenant precisa vir antes.
9. **Tools:** "nega por padrão" com curadoria por setup? Quais MCPs ficam no núcleo? Só admin cadastra MCP? Allowlist de hosts?
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?
11. **Subagentes:** rever D4/D6 (largura e profundidade ilimitadas) e adotar tetos por árvore? Isolar a escrita por filho (worktree)?
12. **Slot de concorrência:** manter por turno (desenho RATE-003) ou passar a por request?
13. **Workflows:** exigir `budget`? Implementar o agendamento (`schedule`) ou retirá-lo?
14. **Retenção:** política para events de threads ativas (hoje mantidos para sempre, por decisão)?
15. **Thinking:** assumir a poda (e documentar) ou investir em preservar o thinking assinado?
16. **Load Balance:** quem coordena com o dono do LB as mudanças de contrato (`cache_creation`, modelo e custo, betas, versão)?
17. **Publicação:** este relatório deve ir para o AgentPack de produção atual (`agentpack-v0`)? Requer conexão MCP autenticada com a instalação correta (ver o fim do documento).

**Perguntas abertas que dependem de dados que o scan não leu:**
- `.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.
- Topologia e borda: réplicas, sticky no proxy do painel, API e WS na mesma origem no build do front.
- 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).
- Tamanho real das definições dos MCPs de produção; filhos por pai (p99); tamanho típico do `finalText`; existência de netos.
- Lado do LB: limpeza de sampling e thinking por modelo, `cache_creation` no usage, modelo real no SSE, repasse de betas e server tools.
- Nomes exatos das opções do SDK oficial para endpoint e autenticação customizados.
- 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)
- O `runAgentStep` adquire slot de concorrência da org? A ordenação alfabética das tools é estável? `pruneConsumedMailbox` é chamada?
- Há threads com `pruneOldThinking: false` e histórico legado com reasoning assinado (define a urgência do replay)?

## 8. Fontes

**Anthropic — engenharia e pesquisa**
- [Building effective agents](https://www.anthropic.com/research/building-effective-agents)
- [How we built our multi-agent research system](https://www.anthropic.com/engineering/multi-agent-research-system)
- [Effective context engineering for AI agents](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents)
- [Writing effective tools for agents](https://www.anthropic.com/engineering/writing-tools-for-agents)
- [Advanced tool use](https://www.anthropic.com/engineering/advanced-tool-use)

**Anthropic — Claude Code e Agent SDK**
- [Agent SDK — Subagents](https://code.claude.com/docs/en/agent-sdk/subagents)
- [Claude Code — Subagents](https://code.claude.com/docs/en/sub-agents)
- [Claude Code — Dynamic workflows](https://code.claude.com/docs/en/workflows)

**Anthropic — plataforma e API Messages**
- [Prompting best practices](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices)
- [Prompting Claude Opus 5](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-opus-5)
- [Streaming messages](https://platform.claude.com/docs/en/build-with-claude/streaming)
- [TypeScript SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/typescript)
- [SDK helpers (`helpers.md`)](https://github.com/anthropics/anthropic-sdk-typescript/blob/main/helpers.md)
- [Handle tool calls](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls)
- [Parallel tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/parallel-tool-use)
- [Define tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools)
- [Fine-grained tool streaming](https://platform.claude.com/docs/en/agents-and-tools/tool-use/fine-grained-tool-streaming)
- [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching)
- [Thinking](https://platform.claude.com/docs/en/build-with-claude/thinking)
- [Extended thinking](https://platform.claude.com/docs/en/build-with-claude/extended-thinking)
- [Token counting](https://platform.claude.com/docs/en/build-with-claude/token-counting)
- [Context editing](https://platform.claude.com/docs/en/build-with-claude/context-editing)
- [Tool search tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool)
- [Programmatic tool calling](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling)
- [Memory tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool)
- [MCP connector](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector)
- [Handling stop reasons](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons)
- [Rate limits](https://platform.claude.com/docs/en/api/rate-limits)
- [Batch processing](https://platform.claude.com/docs/en/build-with-claude/batch-processing)
- [Features overview (GA × beta)](https://platform.claude.com/docs/en/build-with-claude/overview)

**Mercado — multiagentes e execução durável**
- [Cognition — Don't build multi-agents](https://cognition.com/blog/dont-build-multi-agents)
- [OpenAI Agents SDK — Orchestrating multiple agents](https://openai.github.io/openai-agents-python/multi_agent/) · [Handoffs](https://openai.github.io/openai-agents-python/handoffs/) · [Guardrails](https://openai.github.io/openai-agents-python/guardrails/) · [Tracing](https://openai.github.io/openai-agents-python/tracing/)
- [LangGraph — Interrupts](https://docs.langchain.com/oss/python/langgraph/interrupts)
- [Inngest — Steps](https://www.inngest.com/docs/learn/inngest-steps)
- [Restate — Durable steps](https://docs.restate.dev/develop/ts/durable-steps)
- [DBOS — Workflow tutorial](https://docs.dbos.dev/typescript/tutorials/workflow-tutorial)
- [Temporal — Activities](https://docs.temporal.io/activities) · [Retry policies](https://docs.temporal.io/retry-policies)
- [CrewAI — Flows](https://docs.crewai.com/concepts/flows)
- [AutoGen — Teams](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/teams.html) · [Human in the loop](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/human-in-the-loop.html) · [State](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/state.html)
- [Why do multi-agent LLM systems fail? (MAST)](https://arxiv.org/html/2503.13657v3)

**Tempo real**
- Socket.IO v4: [Rooms](https://socket.io/docs/v4/rooms/) · [Namespaces](https://socket.io/docs/v4/namespaces/) · [Delivery guarantees](https://socket.io/docs/v4/delivery-guarantees/) · [Connection state recovery](https://socket.io/docs/v4/connection-state-recovery/) · [Redis Streams adapter](https://socket.io/docs/v4/redis-streams-adapter/) · [Redis adapter](https://socket.io/docs/v4/redis-adapter/) · [Using multiple nodes](https://socket.io/docs/v4/using-multiple-nodes/) · [Middlewares](https://socket.io/docs/v4/middlewares/) · [Client options](https://socket.io/docs/v4/client-options/) · [Emitting events](https://socket.io/docs/v4/emitting-events/) · [Server options](https://socket.io/docs/v4/server-options/) · [Server API](https://socket.io/docs/v4/server-api/)
- [ws (README)](https://github.com/websockets/ws/blob/master/README.md)
- [OpenAI — Responses API WebSocket mode](https://developers.openai.com/api/docs/guides/websocket-mode)
- [MDN — Using server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)

**Fontes internas (repositório `motor`, HEAD `69e0b28`)**
- Código citado em cada achado (`be/src/**`, `fe/src/**`, `be/prisma/schema.prisma`).
- `be/docs/contrato-load-balancer.md`; `docs/operacao.md`; `docs/workflows/00-visao-geral.md`, `02-arquitetura.md`, `04-contrato-runtime.md`, `05-guia-operacional.md`, `08-matriz-de-testes.md` e `08-matriz/`; `docs/motor-agent-prod-readiness/*.html`; `docs/auth-sso.md`; `docs/api-terceiros.md`; `docs/setups/00-decisoes.md`; `docs/lockfiles.md`.
- Produção (somente leitura, pelo painel): logs de `motor-be-ws` e `motor-worker-ws`; estatísticas dos serviços; `postgresql.conf` do `motor-prod-postgres`; configuração do `motor-prod-redis`; lista de variáveis (valores mascarados).

## Publicação

O relatório completo está neste arquivo, no sandbox `motor`: `/workspace/.sbcache/scan-motor-2026-10-11/RELATORIO.md`.

**Não foi publicado no AgentPack.** O MCP `agentpack-orq` desta sessão aponta para a instalação **antiga** (`agentpack-api.mp.serendiped.com`), não para a produção atual (`agentpack-v0-api.mp.serendiped.com`). Por instrução, nada foi publicado na instalação antiga e nenhuma configuração ou credencial foi alterada para contornar isso. Publicar na produção atual depende de uma conexão MCP autenticada com a instalação correta; o `knowledgeId` fica vazio até lá. Isso não bloqueia a conclusão do scan.

