Como quatro minutos de silêncio mataram meu agente de IA
Um agente terminou o trabalho e o orquestrador o matou antes de salvar. A liveness vinha do stdout, então uma síntese longa e silenciosa parecia morte. Um keepalive de 15 linhas resolveu.
O agente fez tudo certo. Leu os sensores, puxou dados reais e escreveu uma
síntese afiada: três oportunidades de conteúdo com ganchos e sinais de mercado
ao vivo. Aí, três segundos antes de salvar esse trabalho, o sistema o matou. A
execução voltou como stranded.
Os dois primeiros instintos estavam errados
O write-back falhou com um 409, depois um 401, depois adapter_failed. Meus
dois primeiros instintos estavam errados.
- Postar um comentário para manter a execução viva. A ideia: gerar atividade para o orquestrador enxergar uma execução viva. Teatro. O detector de travamento nunca lê o campo “última atividade”. Confirmei isso no código antes de apostar. O relógio que importa é o heartbeat do processo, e um comentário não encosta nele.
- Culpar a fonte de dados instável. Uma ferramenta tinha dado 404 mais cedo na execução, então culpei ela. Nada a ver. O 404 era um bloqueio por fingerprint numa requisição vinda de datacenter, um bug real, mas não esse.
A falha era uma cadeia silenciosa
Lendo o código do runtime linha a linha, a falha era uma cadeia, e cada elo era invisível sozinho:
- O modelo passou minutos gerando uma síntese longa sem nenhuma tool call, então o processo filho não emitiu stdout.
- Sem stdout, o adapter não repassou nada, então o heartbeat da execução envelheceu.
- Um heartbeat velho parece exatamente uma execução morta, então o orquestrador subiu uma duplicata para recuperá-la.
- A duplicata roubou o lock de checkout. Agora o write-back da execução real era
rejeitado: o ator dela não segurava mais o lock. Esse é o
409, depois o401, depois o strand.
O orquestrador não conseguia distinguir “essa execução morreu” de “essa execução ficou quieta para pensar”. Ele só tinha um sinal, e lia silêncio como morte.
Quinze linhas e um bug autoinfligido
O patch tinha umas quinze linhas: um timer que emite uma linha de heartbeat sempre que o processo fica quieto além de um limite, zerado por qualquer saída real, limpo quando o filho termina. Montei uma tarefa propositalmente mais pesada para forçar uma síntese longa e silenciosa. Rodou uns dez minutos e fechou limpa. A antiga morria em cinco. A correção do runtime voltou para o upstream como pull request público contra o adapter.
No caminho, recarreguei o container com docker restart e deixei a task do
swarm órfã: zero réplicas, a mesh parou de rotear, a API servia 404 enquanto o
app estava saudável lá dentro. O certo no swarm é service update --force.
Dois bugs numa sessão, os dois instrutivos.
Lição
Todo sistema que infere “vivo” a partir de saída vai acabar matando um worker que fica quieto para fazer o trabalho de verdade. Liveness e progresso são sinais diferentes. Se você só tem um canal, mande heartbeat nele com um timer independente do trabalho em si, ou o sistema vai punir a concentração.