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:
- 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) epumpsem esperar. Odrainé atômico: tudo o que estava pendente vira um turno (dispatcher.ts:143-151). - Lock e preparo. O
pumppega o lock da thread no registry (memória ou Redis com TTL de 30s e heartbeat de 10s) e chamarunTurn(be/src/engine/turn-runner.ts:427). OprepareTurnlê a thread e a config efetiva (global < setup < thread) e congela tudo numa linhaTurn, para auditoria e replay. - 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 dewindow × compactAt(padrão 230 mil × 0,9), o compactador resume o histórico antigo, persiste um eventocompactione o LLM passa a ver [resumo] + eventos novos. Falha aqui nunca derruba o turno; após 3 falhas seguidas, escala alerta. - Bootstrap do agente (
turn-runner.ts:650ebe/src/agent/bootstrap.ts). Monta o model (placeholderproxy-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). AtoolPolicydo setup recorta as tools. Ummetadata.user_idestável por conversa maximiza o cache no proxy. - 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. - O loop (
be/src/agent/loop.ts:402,runAgent). Montamessagescom 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 chamastreamTextdo pacoteaiv5 comstopWhen: stepCountIs(maxSteps ?? 999),maxRetries ?? 3e oabortSignaldo turno. A cada step: o modelo gera texto e/ou tool calls, as tools executam,onStepFinishsoma o usage à mão e emiteonSteppara a timeline ao vivo. Entre steps, oprepareStepcheca o orçamento e drena notificações novas da mailbox, reenviando um buffer acumulado. O aborto tem rede dupla:reasoncom nomeAbortError(o único que o SDK reconhece) e uma guarda de 5s que encerra à força. - Persistência da resposta e fechamento (
turn-runner.ts:1091-1271).gravarRespostasplaneja os eventos por passo (reasoningsó 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, ouerroredse 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).
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:
buildNativeTools: 23 tools in-process (4 de subagente, 4 de setup, 15 de workflow);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 allowlistmcps[].tools(filterMcpTools,be/src/tools/registry.ts:28);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;applyToolPolicy(registry.ts:125): recorta pelatoolPolicydo 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.
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.
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).
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) |
<container interno (prod PG)> (postgres:16-alpine) |
4 GB | postgresql.conf com max_connections = 100 e shared_buffers = 128MB (defaults); confirmar override com SHOW |
<container interno (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 porisBudgetExceeded,isUserCancel,isTurnTimeout);turn-runner.ts:1352(turnsFailed.inc()para tudo que não é cancelamento do usuário);be/src/engine/orchestrator.ts:457-458ebe/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>:abortem todas as réplicas, serializar oAbortReasoncom as flags e reconstruir oAbortErrorna 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 (
grepembe/src: sórunbus:*elauncher-wake), e os comentários deregistry-redis.ts:22-26ebe/src/engine/registry.ts:37descrevem 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).grepporcacheCreation|cache_creation|providerMetadataembe/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.cacheCreationInputTokenspor step e somar emcontextTokens, 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(turnTimeoutMspadrã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 chamarunTurnsemmaxSteps);be/src/engine/turn-runner.ts:224-229, 940-952;be/src/engine/agent-step.ts:689ebe/src/workflows/executors.ts:340-343(o limite de workflow depende deWF_AGENT_MAX_TOOL_STEPS, ausente do.env.example); log de produção domotor-be-ws: 81 responseMessages (~41 rodadas de tool), input 2.633.801 + output 23.622 + cached 11.340.563, terminou emoverloaded_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(recebepromise: Promise<unknown>, já iniciada);turn-runner.ts:105-131(await promisesobre 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 efinishTurn. - Recomendação: receber uma função (
() => Promise) e chamá-la a cada tentativa, mantendo o respeito aoabortSignal; 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(usasideEffectWrite, 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á marcouconsumed_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:
sideEffectWritedevolver 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.ts1583;agent-step.ts956;loop.ts654;compaction.ts473;org-concurrency.ts263;provider/anthropic.ts227;bootstrap.ts187;response-events.ts175;messages.ts107;contrato-load-balancer.md74. - Versões: produção instala pelo
be/package-lock.json(ai5.0.253,@ai-sdk/anthropic2.0.101); o lock da raiz (5.0.179 e 2.0.77) atende dev e CI e é o que está nonode_modulesda 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:
stopWhen999;maxRetries3 (o padrão do SDK é 2); guarda de aborto de 5s. Retry instalado: 2s inicial, fator 2, respeitaretry-after; só erro marcado como retentável retenta; abort nunca retenta. - Turno:
turnTimeoutMs0;sideEffectWritecom 0, 200 e 600 ms; compactação em 230 mil × 0,9,keepTurns4, saída do compactador de 8 mil tokens. - Cache padrão: TTL de 5 min em system, tools e messages;
cacheToolsligado;markLastUseremarkLastAssistantdesligados; checkpoint acima de 20 mensagens. - Contrato com o LB:
proxy-managed,max_tokens64000, 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 emMap/Setlocais);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.ymlsem 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 embe/src/routes/threads.ts:114,be/src/routes/agents.ts:480, 541ebe/src/ws/handler.ts:419, 467;be/src/engine/reaper.ts:23-26, 153, 181(o reaper usa o mesmoisBusy, 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
busydo 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) e527-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(getEventssem limite a cada abertura),302-304e429(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 doseqinformado 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) e109-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(publishdo JSON inteiro dentro do MULTI);be/src/persistence/keys.ts:60-62; os únicos assinantes do backend sãorunbus:*(be/src/workflows/bus.ts:142) elauncher-wake(be/src/ws/handler.ts:259), além de um smoke de teste. - Impacto: cada
appendEventpaga 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(rootOfnunca apaga);be/src/ws/busy-owner.ts:34, 41, 46(ownersnunca apaga);be/src/engine/registry-redis.ts:72, 178-180(convoCachesem 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(seenClientMessageIdsem memória) e656-658(esquecido no close);be/src/engine/mailbox-db.ts:87-95(insert sem checagem);be/prisma/schema.prisma:329-356(MailboxItemsem coluna nem unique declientMessageId);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
clientMessageIdcom 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 viranulle 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(emitecompaction_failed,persistence_warningeturn_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
vnos frames, catálogo único be/fe com regra aditiva, validação no parser do run eunknownTypelevado à 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 gaugews_clients);be/src/routes/metrics.ts:19-23;be/src/routes/health.ts:115-127; nenhum contador emws/handler.ts,ws/broadcaster.tsouhttp/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
busydo 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 devolvefalse);be/src/routes/agents.ts:575-592ebe/src/engine/orchestrator.ts:456-464(abortChildmarcaabortedmesmo comok=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 ficaabortedno banco enquanto roda: a árvore mente e o gasto continua. - Recomendação: assinar
<prefixo>:abortem todas as réplicas e só marcaraborteddepois que o abort confirmar; enquanto isso, documentar que o Parar exige sticky. - Revisão: rebaixado por ser latente (1
bee 1workerem 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 serootOfconhecer a thread) e266, 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-260eruntime.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ó
spawneresult. 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(getThreadcom 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) e85-86(só consomethread_busyeconnected);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_busyde 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(psubscribeglobal),18-26(o comentário admite) e124-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 parafinaleerror);be/src/engine/turn-runner.ts:1232-1245(limpa o ao vivo antes dofinal);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 timelineelapsedMs,steps,usageehistoryLengthdo turno; a cauda do parcial do run some até o próximo frame. Não há perda de dado do usuário. - Recomendação:
seqpor sala e replay de janela na reconexão; incluirfinaleerrorno buffer vivo ou persistir o resumo dofinal.
[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 fazgetEvents, config, replay e buffer vivo). Mitigações: cache quente de events;loadEffectiveConfigsó 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_progressestá 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 emturns.config_snapshot) e19-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 carregabasic.apiKey, token/Authorizationde MCP ecompactor.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 oconfigHashsobre a config completa antes de redigir; segredos ficam só emOrgConfigeAgent. - 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/testconecta 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(connectMcpnão usa o guard);be/src/config/thread-config.ts:34-49, 71-79(ExtraMcpSchemaaceita qualquer URL, inclusivehttp://);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.tsnoconnectMcpe 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-02e04em 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 einputSchemaentram 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 etoolPolicyopcionais. - 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 virarole: '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
finalTextcom 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(toolsopcional; ausente = todas);be/src/tools/registry.ts:32(sem filtro, devolve tudo);be/src/persistence/agents.ts:228-243ebe/prisma/schema.prisma:100(o setup padrão nasce semtoolPolicy);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 (
toolPolicypreenchida 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, mascollected[name] = tmanté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 decfg.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__toolcom 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(finalTextsem corte). Mitigações:be/src/workflows/notify-launcher.ts:57-61(workflow corta em 6 mil chars);be/src/agent/history.ts:102-119ebe/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(estimateContextTokenssoma system e messages, sem tools) e1180-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)eString(args.task)viram "undefined" e o spawn acontece);be/src/engine/orchestrator.ts:239-246(sem validação);@ai-sdk/provider-utils(jsonSchema()semvalidateaprova tudo). Contraexemplo bom:be/src/tools/native/workflow.ts:300-330(as toolsworkflow_*recusam argumento ruim). - Impacto: o
requireddos schemas é decorativo para JSON válido com campo faltando:agent_spawncria filho com tarefa "undefined", gastando modelo e poluindo a árvore. - Recomendação: validar
requirede tipos no início de cadaexecutenativo (helper zod compartilhado) e recusar com erro legível. - Revisão: o mecanismo original estava errado (
parseToolArgssó alimenta a UI e JSON quebrado o SDK já recusa — verloop-sdk-parse-argsem 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_validatesozinha: 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 comREAPER_ENABLED === '1') e302-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(únicostart());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 temREAPER_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
activee o turnorunningpara 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=1em 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) e234-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(finalTextcru);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 nomessagesdo pai);be/src/persistence/event-payload-schemas.ts:39-46(também persiste inteiro). Contraste:orchestrator.ts:246(spawnReasoncortado em 500) e410(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
finalTextna notificação (ex.: 4–8 mil chars, com aviso de corte) e instruir o filho a devolver resumo denso mais a referência aothreadIdpara 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(abortChildaborta 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-294eorchestrator.ts:297-372(a notificação do filho acorda o pai sem checar cancelamento);be/src/persistence/threads.ts:324(listThreadTreeexiste, mas só é usada em leitura); flagisCascadesem 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
onTurnEnddescartar 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:190earquitetura.html:631(prometem o assinante que não existe). - Impacto: com várias réplicas,
agent_abortou Parar na réplica errada não tem efeito e devolvefalse; 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.ts471;subagent.ts281;mailbox.ts335;mailbox-db.ts175;dispatcher.ts303;reaper.ts322;reconciler.ts120;registry-redis.ts264;setup.ts520;system-prompt.md288 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 eventosubagent_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_listlimitado a 50;spawnReasoncortado em 500 chars;finalTextsem corte. - Contrato de prompt: seção de subagentes com ~40 linhas no system prompt e descrição do
agent_spawncom ~11 linhas; sótitleetasksã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 ?? nullno envelope do passo);be/src/ws/run-channel.ts:149-181(handshake do WS: findMany semtake, com output inteiro);be/src/routes/workflows.ts:592-602(GET /api/runs/:id/stepssemtake) e604-609(o comentário admite que fan-out de 300 passos não pode viajar na lista — tiraraminputeattempts, mas ooutputficou);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
httppode gerar até 50 MB por handshake e por GET; ummapde 1.000 filhos baixa megabytes só para montar a espinha. O navegador pode travar antes de renderizar (risco já registrado emdocs/workflows/02-arquitetura.md:108-110). - Recomendação: na lista,
output: nullcom prévia curta (1–2 KB) ou indicador de truncamento; output completo só em/steps/:ide no framestep_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,_STEPSe_WALL_MSvazios; "antes vinha escondido no default do banco: US$ 5, 200 passos, 30 min");be/src/tools/native/workflow.ts:167-168, 738-740(oworkflow_draftsó avisa);be/src/engine/agent-step.ts:174-178(semtimeoutMs, 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) eWF_DEFAULT_MAX_WALL_MS(ex.: 1.800.000); opcionalmente exigirbudgetcomWF_REQUIRE_BUDGET=1; aviso explícito no retorno doworkflow_draftquando obudgetvier 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 (
definition1911,reconcile2456,expr1029,runs962,expanders860,executors798,runner764,state492, entre outros), mais 14 arquivos de teste com 302 casos; filas embe/src/queuecom 1.415 linhas;workers/index.tscom 342;engine/agent-step.tscom 956;tools/native/workflow.tscom 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_CONCURRENCYeWORKER_DET_CONCURRENCY100;ORG_CONCURRENCY_MAX100;DB_CONNECTION_LIMIT1000;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):
mapde 1.000 transforms em 17,5s (67 passos/s, 100 filhos em voo);mapde 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(getConvolê só oMaplocal) e172-181(o Redis só recebe SET e DEL; nenhum GET embe/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; olauncher-wakeacorda qualquer réplica);be/src/engine/dispatcher.ts:296-298(kicksem hidratar);be/src/workflows/notify-launcher.ts:163-170; o próprio código admite emturn-runner.ts:869-870que 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:
getConvocom fallback:Maplocal → GET no Redis →getEvents+eventsToLlmMessagesdo banco; enquanto isso, alerta quando um turno começa com convo vazio numa thread comlastSeq > 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êmREAPER_ENABLED;reconcileStuckNotificationsesweepThreadCounterssó rodam dentro do reaper (reaper.ts:256, 276). - Impacto: crash entre
startTurnefinishTurndeixa o turnorunningpara 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), maisisBusydistribuí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(olauncher-wakefaz todas as réplicas daremkick; 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:streamestá 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 nemfinalaté reconectar; othread_busytem o mesmo limite. - Recomendação: publicar os eventos de turno no Redis (o canal
:streamjá 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") e113-217;be/src/queue/purge.ts:81-145(só purgaworkflow_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(1be+ 1worker);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_timeoutde 10s. - Recomendação: não subir réplica sem PgBouncer em modo transação ou
max_connectionsdimensionado (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 = 100nopostgresql.confde produção: seSHOW max_connectionsconfirmar, 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;
eventstem unique(thread, seq)e índice redundante(thread, seq). - Emissões por turno: 14 chamadas
emitno turn-runner +pushLivepor 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, 153ebe/src/engine/turn-runner.ts:347-360, 432(esse valor vira oglobalde todo turno, de qualquer org);be/src/setups/service.ts:763-868(loadEffectiveConfigrecebe oglobale nunca lê aOrgConfigda org do turno);be/src/config/thread-config.ts:303-309(apiKeyherdada),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) e611-622(a rota de config da thread também usa o da default);be/src/auth/jit.ts:170-184(o desenho prevê umaOrgConfigpor 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,systemPromptAppende 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 aOrgConfigda própria org (cache por org com TTL curto, ou um provedor(orgId) => config); odeps.chatConfigpassa 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-445descreve 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 semapiKey— 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(fetchsemredirect: 'manual'nemlookup);dispatcher.ts:218(resolveAndValidateuma vez, antes do laço de retries);dispatcher.ts:232-237(enviaX-Webhook-*eX-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(osafeRequestfaz tudo isso certo);be/src/workflows/executors.ts:560(o passohttpdo workflow já usa osafeRequest). - Impacto: um endpoint de webhook que responda 302 para um host interno (ex.: 169.254.169.254) faz o
fetchseguir 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
postOncepelosafeRequest(com os headers de assinatura no própriosafeRequest), aceitar sóhttps:e apagar a funçãoallowPrivateHostsmorta. - 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_HOSTSestá 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;connectMcpcom a URL do corpo;err.messagedevolvido 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 emdocs/api-terceiros.md:580(§7.3.4) e emdocs/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:
exigirAdmine rate limit próprio na rota;resolveAndValidatedentro doconnectMcp(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(fetchcru para abaseURLdo setup),82(anexaproviderMessage.slice(0, 100)ao status) e355(ecoaerr.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(aceitahttp://e IP privado). - Impacto: um membro cria um setup privado com
baseURLinterna, 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
safeRequeste 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) e252-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 aceitamcpssem 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:
resolveAndValidatenoconnectMcpe na camada de MCPs extras da thread, comWEBHOOK_ALLOW_PRIVATE_HOSTScomo 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 escoposetup) e173-177;be/src/routes/threads.ts:283-287(o PATCH da thread usa o escopothread);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 comoAuthorizationao 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 canalabort:<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:259ebe/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. |
<container interno (Redis compartilhado)> — 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) | 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; OpenAI Agents SDK — orquestração) | 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) | 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) | 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) | 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) | 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) | 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?) | 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) | 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) | 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) | 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) | 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) | 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) | 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) | 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) | 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) | 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; 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) | 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, 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) | 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) |
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) | 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) | 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) | 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) | 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; 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) | 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) | 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) |
Coberto pelo pacote ai |
No SDK nativo, messages.stream com finalMessage (TypeScript SDK) |
Tool use manual: tool_result primeiro, todos os resultados numa mensagem só, is_error instrutivo (Handle tool calls; 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) |
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) |
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; 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) | 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) | 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) | 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, 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) | 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; DBOS) | 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) | 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; 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; AutoGen — Human in the loop) | Coberto: passo human com waiting persistido e timeout de até 30 dias |
Manter |
| Terminação explícita com motivo (AutoGen — Teams) | 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) | Coberto pelo reconciliador | Manter |
| Trace único por workflow, com redação de dados sensíveis (OpenAI Agents SDK — 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) | 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) | 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) | 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) | 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; Redis adapter; 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) | 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; 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; 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) | 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) | 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.
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]
- Montagem por turno.
systemem blocos (as camadas do builder de 5.3);messagestraduzidas do log de events (user e assistant com blocostext,tool_useetool_result; thinking preservado byte a byte ou podado por flag);toolscominput_schema, em ordem alfabética estável, comcache_controldentro do orçamento de 4 breakpoints (prioridade: system, último user, checkpoint, tools). - 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,apiKeyouauthToken, headers por request) antes de codar. - Consumo do SSE. Deltas de texto vão para o sink (WebSocket); inputs de tool acumulados até
content_block_stop; usage lido demessage_startemessage_delta, incluindocache_creation_input_tokens. stop_reason.end_turnpersiste e fecha.tool_usevalida cada input contra o schema (inválido viratool_resultcomis_error, nunca{}silencioso), executa em paralelo (Promise.allSettled, timeout por tool, abort encadeado), anexa todos os resultados numa única mensagemuser, persiste o step (fecha o buraco do crash), confere orçamento e tetos de steps e tokens e continua.max_tokens,refusal,pause_turne janela excedida têm ramos próprios; o 400 de janela estourada dispara compactação e 1 retry automático.- 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). - Retries e erros. Só transitórios tipados (429, 5xx, rede), respeitando
retry-after, com jitter. O SDK oficial temmaxRetries(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) viramerrorKindpróprios, inclusive a negação do gate de concorrência como "ocupado, tente de novo". - Notificações no meio do turno. Append direto em
messagesentre requests, só depois de persistir — o problema do P-07 desaparece por construção, junto com o buffer acumulado manual. - 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.
- 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.
- Interface
AgentTransportcom duas implementações (AI SDK e nativa); runner nativo atrás de flag por setup; shadow-diff do wire. - Compactador no transporte novo.
- Nativo como padrão, com rollback por flag; depois, remover
aie@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
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
/wsatual (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/threade/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:
seqmonotônico por sala ev(versão) em todo frame; eventos persistidos antes de emitir;connectionStateRecovery(ex.: até 2 min, sem pular middleware) para quedas curtas; comrecovered = false, o cliente pede o que veio depois do últimoseqe 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 comenqueuedeclientMessageId),stop,reset,join. OclientMessageIdvira 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_deltacomseq, coalescido em janelas de 30–100 ms evolatile; 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;
serverSideEmitpara mensagens entre servidores (JSON, sem binário). - Estado distribuído:
busyderivado do lock no Redis e publicado na salauser:; abort por canal assinado, comAbortReasonserializado; uma conexão por aba (a sidebar usa a salauser:). - 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);maxHttpBufferSizeexplícito (padrão do socket.io: 1 MB; hoje o WS aceita 8 MiB) enquanto houver imagem inline — o alvo é upload por HTTP paraattachmentse 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__toolcom alias curto; colisão com schemas diferentes vira erro no boot; atoolPolicycita servidor ouservidor__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 noconnectMcpe 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
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
threadIdou 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 noworkflow_drafte, opcionalmente,WF_REQUIRE_BUDGET; - lista de passos sem
output(só no detalhe e no framestep_output); log do run por cursor deseq; lista de runs numa query só; - correções de corrida (
requestResume,requestPause,startRuncom 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 edefer_loadingde tools por setup, alias de modelo por passo para routing,ttftMse taxa de acerto de cache por passo, eWF_AGENT_MAX_TOOL_STEPSdefinido; - 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):
httpsobrigató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 |
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:
- 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.
- Banco: confirmar
max_connectionsem produção; manter "nunca reduzir o pool" e subir o Postgres para ≥ 2100, ou adotar PgBouncer? - Redis: separar fila e cache (
REDIS_QUEUE_URL), hoje juntos numa instância de 512 MB sem teto de memória? - 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? - 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.
- SDK nativo: aprovar a migração incremental, abrindo mão da abstração multiprovider (hoje o Motor só fala Anthropic via LB)?
- Tempo real: socket.io (como pedido) ou evoluir o
wsnativo com pub/sub? Streaming token a token no chat é desejado? O corte por cliente é aceitável? - Multi-org: quando a UI terá troca de org? O conserto cross-tenant precisa vir antes.
- Tools: "nega por padrão" com curadoria por setup? Quais MCPs ficam no núcleo? Só admin cadastra MCP? Allowlist de hosts?
- 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?
- Subagentes: rever D4/D6 (largura e profundidade ilimitadas) e adotar tetos por árvore? Isolar a escrita por filho (worktree)?
- Slot de concorrência: manter por turno (desenho RATE-003) ou passar a por request?
- Workflows: exigir
budget? Implementar o agendamento (schedule) ou retirá-lo? - Retenção: política para events de threads ativas (hoje mantidos para sempre, por decisão)?
- Thinking: assumir a poda (e documentar) ou investir em preservar o thinking assinado?
- Load Balance: quem coordena com o dono do LB as mudanças de contrato (
cache_creation, modelo e custo, betas, versão)? - 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:
.envefetivo de produção (mascarado no painel):DB_CONNECTION_LIMIT,ORG_CONCURRENCY_MAX,WF_*,CACHE_*,CHAT_TRUST_PROXY,METRICS_TOKEN;REAPER_ENABLEDnã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_creationno 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
runAgentStepadquire slot de concorrência da org? A ordenação alfabética das tools é estável?pruneConsumedMailboxé chamada? - Há threads com
pruneOldThinking: falsee histórico legado com reasoning assinado (define a urgência do replay)?
8. Fontes
Anthropic — engenharia e pesquisa
- Building effective agents
- How we built our multi-agent research system
- Effective context engineering for AI agents
- Writing effective tools for agents
- Advanced tool use
Anthropic — Claude Code e Agent SDK
Anthropic — plataforma e API Messages
- Prompting best practices
- Prompting Claude Opus 5
- Streaming messages
- TypeScript SDK
- SDK helpers (
helpers.md) - Handle tool calls
- Parallel tool use
- Define tools
- Fine-grained tool streaming
- Prompt caching
- Thinking
- Extended thinking
- Token counting
- Context editing
- Tool search tool
- Programmatic tool calling
- Memory tool
- MCP connector
- Handling stop reasons
- Rate limits
- Batch processing
- Features overview (GA × beta)
Mercado — multiagentes e execução durável
- Cognition — Don't build multi-agents
- OpenAI Agents SDK — Orchestrating multiple agents · Handoffs · Guardrails · Tracing
- LangGraph — Interrupts
- Inngest — Steps
- Restate — Durable steps
- DBOS — Workflow tutorial
- Temporal — Activities · Retry policies
- CrewAI — Flows
- AutoGen — Teams · Human in the loop · State
- Why do multi-agent LLM systems fail? (MAST)
Tempo real
- Socket.IO v4: Rooms · Namespaces · Delivery guarantees · Connection state recovery · Redis Streams adapter · Redis adapter · Using multiple nodes · Middlewares · Client options · Emitting events · Server options · Server API
- ws (README)
- OpenAI — Responses API WebSocket mode
- MDN — 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.mde08-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-wsemotor-worker-ws; estatísticas dos serviços;postgresql.confdo<container interno (prod PG)>; configuração do<container interno (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 (<host interno (AgentPack antigo)>), não para a produção atual (<host interno (AgentPack prod)>). 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.