Pular para o conteúdo
← lab
post-mortemholds-upagentsinfraorchestration

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.

  1. 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.
  2. 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 o 401, 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.