ADR 0098 — Limites de transporte e janela efetiva de compactação coerentes
- Status: Aceito
- Data: 2026-08-19
- Contexto: correção do
413 request entity too largenas PRs (achado por uso real), RN-412
Contexto
O gate de QA/SecOps morria com 413 request entity too large em PRs
legítimas. Comentários espalhados pelo código (apps/engine/config/runtime.exs,
apps/engine/lib/engine/actions/terminal_executor.ex, docker/docker-compose.yml)
atribuíam o erro ao PROVIDER de LLM. A investigação confirmou que essa
atribuição estava errada: Engine.Agents.FalhaDeTurno.origem({413, _})
já classificava a falha como "codigo", não "modelo" — e o
classificador estava certo, só ninguém tinha seguido a pista até o
fim.
A causa real tem duas pontas independentes, e as duas precisam fechar juntas — corrigir só uma adia o estouro, não o resolve:
- A api nunca configurou limite de body do Express.
NestFactory.createemapps/api/src/main.tsnão tocava o parser JSON, então valia o default do Express: 100 KB. O Phoenix (engine) aceita corpos de até 8 MB. No sentido mais pesado do transporte — engine → api, especificamentePOST /internal/sessions/:sessionId/llm-turn, que recebe o HISTÓRICO INTEIRO da conversa a cada iteração doToolLoop— a api era o gargalo mais estreito por um fator de 80×, sem que ninguém tivesse decidido isso: era ausência de configuração, não escolha. - A compactação de contexto do engine era estruturalmente
inalcançável antes do estouro.
Engine.Harness.ContextManager.Defaultdecide compactar quando a estimativa de tokens ultrapassathreshold * context_window. Dois defeitos empurravam esse gatilho para muito depois do limite de transporte:estimate/1somava só o campocontentdas mensagens. MensagensassistantcomtoolCalls(o formato de uma chamada de ferramenta) têmcontentvazio — os argumentos da tool call, que SÃO bytes reais no corpo HTTP, contavam como ~zero tokens.- A janela de compactação usava só
context_window— 128.000 tokens nos cinco agentes de gate/dev (qa_automacao_agent.ex,qa_performance_seguranca_agent.ex,qa_estrategia_agent.ex,appsec_agent.ex,dev_agent_server.ex). Comthreshold: 0.7, isso dá compactação em ~350 KB de payload estimado — bem depois de qualquer limite de transporte razoável, e pior ainda quando a estimativa em si já estava subcontando.
Com um gate de 60 iterações (teto de agentes de execução/gate) e três a quatro tool results de 32 KiB (o teto INDIVIDUAL já fechado pela RN-150), o corpo acumulado ultrapassava 100 KB muito antes de a compactação sequer considerar agir.
Decisão
As duas pontas fecham na mesma correção, deliberadamente juntas:
apps/api/src/main.tspassa a configurar explicitamente o limite do parser JSON (app.useBodyParser('json', { limit })), lido deAPI_JSON_BODY_LIMITcom default10mb— folga sobre os 8 MB que o Phoenix já aceita, sem exigir novo deploy se o teto do engine mudar.Engine.Harness.ContextManager.Defaultganha:estimate/1contacontentde TODA mensagem MAIS a serialização JSON detoolCallsde mensagensassistant— a heurística de bytes-por-token é a MESMA do tokenizer aproximado (Engine.Harness.Tokenizer.bytes_per_token/0, nova função pública, para não duplicar a constante).- A janela EFETIVA de compactação vira
min(context_window, teto_de_transporte), onde o teto de transporte é UMA config nova (transport_max_body_bytes, default 8 MiB — o teto do PRÓPRIO transporte do engine, não replicado nos cinco arquivos de agente, que continuam declarandocontext_window: 128_000porque isso descreve o MODELO, não o transporte). - O corte entre mensagens antigas (sumarizadas) e recentes
(preservadas) passa a respeitar FRONTEIRA DE ITERAÇÃO do
ToolLoop(group_by_iteration/1) — uma mensagemassistantcomtoolCallse osrole: "tool"que a respondem viajam sempre juntas para o mesmo lado do corte. Cortar no meio quebraria o protocolo de tool-use do provider (resultado de ferramenta sem a chamada correspondente no histórico).
A regra geral que esta ADR registra: a compactação deve disparar ANTES do corpo estourar o limite HTTP real, não antes do modelo "esquecer" a janela dele. Um teto de transporte que só existe implicitamente (o default de uma lib) não é um teto — é uma falha esperando o payload certo.
Consequências
- O valor de
transport_max_body_bytes(8 MiB) é config declarada, não calibrada contra tráfego real — mesma régua de honestidade que os pesos da busca híbrida (RAG) e os tetos desearch_workspace(RN-150) já seguem: ponto de partida ajustável, não número definitivo. - Subir só o limite da api sem corrigir a estimativa do engine (ou vice-versa) reintroduz o defeito sob outra forma: ou o corpo volta a crescer sem freio até um teto maior, ou a compactação passa a agir cedo demais e resumir contexto que ainda caberia no transporte. As duas pontas nasceram no mesmo commit.
dev.awaiting_gatecomo estado do dev agent deveria ficar mais raro na prática (o gate não morre mais por 413 em payload legítimo), mas a RN-412 também estendeDEV_PENDING_TYPESpara segurar a sessão nesse estado — defesa em profundidade, não dependência de que este ADR feche 100% dos casos de gate lento/travado por outro motivo.