A gente queria que o Claude Code rodasse sozinho. Começar uma tarefa, repeti-la de forma autônoma por dezenas de iterações, cuidar do próprio contexto e seguir por horas sem ninguém de babá. Conceito simples. Três semanas de inferno de infraestrutura para fazer funcionar de verdade.
Esta é a linha do tempo completa de cada bug, condição de corrida e falha de arquitetura que encontramos ao construir os sistemas autônomos de loop e de agentes do SynaBun. Cada correção revelava um problema pior por baixo. Não é exagero.
A proposta: loops autônomos
A ideia era simples. Você dá ao Claude uma tarefa repetitiva — interagir com comunidades de IA no Twitter, monitorar um deploy, processar um lote de dados — e ele roda essa tarefa em loop. Cada iteração recebe um contexto novo, o hook Stop dispara o ciclo seguinte e a coisa toda se sustenta sem você encostar no teclado.
Fizemos a primeira versão em um dia. Ferramenta MCP com as ações start, stop e status. Arquivo de estado em disco. O hook Stop lê o estado, incrementa o contador de iterações, injeta o prompt da tarefa e bloqueia a resposta do Claude para forçar o ciclo seguinte. Design limpo. Funcionou no primeiro teste.
Aí tentamos rodar para valer.
Primeira semana: tudo morre depois de 3 iterações
O loop começava bem. Iteração 1, ótimo. Iteração 2, tudo certo. Iteração 3 — silêncio. O Claude simplesmente parava de responder. Sem erro, sem crash, sem mensagem de timeout. Só... nada.
Causa raiz: falha na detecção do prompt. O driver do loop, no lado do servidor, precisava detectar quando o Claude estava pronto para receber entrada, observando o caractere de prompt > na saída do terminal. A regex que usávamos para detectar esse prompt tinha buracos. A saída de terminal do Claude Code vem envolta em sequências de escape ANSI — códigos de cor, posicionamento de cursor, flags de modo DEC — e a nossa regex só reconhecia prompts em ASCII limpo. Qualquer sequência ANSI entre a quebra de linha e o caractere > quebrava a detecção por completo.
Mas piorava. O buffer de saída tinha 2KB. As respostas do Claude passavam disso o tempo todo, o que fazia o caractere de prompt rolar para fora do limite do buffer. O driver procurava um prompt que já tinha sido descartado da memória.
Correção: Aumentamos o buffer de 2KB para 8KB. Reescrevemos a regex de detecção do prompt para remover as sequências ANSI antes de comparar. Adicionamos um campo stopReason ao estado do loop para distinguir entre limite de iterações, limite de tempo e falha na detecção do prompt. Mas estávamos só começando.
O problema dos IDs de sessão órfãos
Quando o driver do loop envia /clear entre as iterações para dar um contexto novo ao Claude, o Claude Code gera um novo ID de sessão. O nosso arquivo de estado do loop era indexado pelo ID de sessão original. Depois do /clear, o nome do arquivo de estado já não batia com a sessão ativa. O hook Stop buscava o arquivo de estado pelo ID de sessão, não achava nada e concluía que não havia loop ativo.
Tecnicamente, o loop continuava vivo em disco. Só tinha ficado invisível para todos os hooks da cadeia.
Tentativa de correção 1: Adicionar uma varredura de fallback. Se a busca direta pelo ID de sessão falhar, varrer todos os arquivos de estado de loop atrás de algum com active: true. Funcionava em cenários de um único usuário, mas era catastroficamente errado em contextos paralelos. Quando os testes rodavam ao mesmo tempo, o hook Stop de um teste roubava o arquivo de loop de outro pela varredura de fallback.
Tentativa de correção 2: Remover de vez a varredura de fallback do hook Stop. Em vez disso, deixar o hook prompt-submit cuidar da renomeação. Quando o marcador [SynaBun Loop] chega depois do /clear, o hook prompt-submit renomeia o arquivo de estado do ID de sessão antigo para o novo. Quando o hook Stop dispara, o arquivo já está no caminho certo.
Tentativa de correção 3 (sim, teve uma terceira): Adicionar uma checagem de recência e um statSync à varredura de fallback do prompt-submit. Só arquivos recentes (com menos de 5 minutos) valem. Arquivos de loop velhos, de loops encerrados ou que travaram, são ignorados em vez de sequestrar a sessão seguinte.
O limite de tempo que matava os loops de navegador
Definimos MAX_MINUTES como 60 e DEFAULT_MINUTES como 30. Razoável para tarefas rápidas de automação. Totalmente insuficiente para loops de automação de navegador, que precisam navegar por páginas, esperar carregamentos, tirar snapshots, clicar em elementos e digitar em formulários. Uma única iteração de interação no Twitter podia levar de 3 a 4 minutos. Com o padrão de 30 minutos, você consegue talvez 8 iterações antes que o limite de tempo mate o loop.
Correção: MAX_MINUTES subiu para 480. DEFAULT_MINUTES subiu para 60. A janela da varredura de fallback por ID de sessão também deixou de ser fixa em 5 minutos e passou a usar o valor de maxMinutes do próprio loop, para que loops longos não deixem órfãos os próprios arquivos de estado.
A armadilha da tela de login
Os loops de navegador davam de cara com uma página de login do Twitter. O agente, seguindo suas regras de autonomia ("se algo falhar, tente uma abordagem alternativa, não pare para perguntar"), abandonava o navegador do SynaBun por completo e recorria a WebSearch e WebFetch. Começava a fazer requisições HTTP cruas ao Twitter em vez de usar o navegador com o qual deveria interagir. Os resultados eram inúteis, mas o loop seguia iterando, queimando créditos de API com lixo.
Causa raiz: As instruções de autonomia eram amplas demais. "Tente uma alternativa" vale para falhas técnicas — um erro 500, um timeout, um elemento do DOM não encontrado. Mas uma tela de login não é uma falha técnica. É um bloqueio que exige ação humana. Só o usuário pode fazer login, resolver um CAPTCHA ou concluir o 2FA.
Correção: Dividir a regra de autonomia em duas categorias. Falhas técnicas: tentar de novo, buscar alternativas, seguir em frente. Bloqueios que exigem ação humana (páginas de login, CAPTCHAs, 2FA, telas de pagamento): PARAR imediatamente, avisar o usuário, ESPERAR a resolução. Nada de recorrer ao WebSearch. O painel do navegador é interativo — o usuário pode resolver, mas só se o agente pausar por tempo suficiente para isso.
Segunda semana: brain rot
O loop rodou por 50 iterações. Nas primeiras 15, a qualidade era excelente. Interações variadas, bom tom, nenhum alvo repetido. Aí, lá pela iteração 20, as coisas começaram a desandar. Estratégias repetitivas. As mesmas frases de abertura. Na iteração 30, ele já postava duas vezes em contas com as quais já tinha interagido. Na iteração 40, tinha esquecido completamente as regras de tom e usava travessões em todas as respostas — exatamente a formatação que tínhamos proibido de forma explícita.
Causa raiz: amnésia de compactação. O Claude Code tem uma janela de contexto. Quando ela enche, o sistema compacta a conversa — resume as mensagens antigas para liberar espaço. Depois da compactação, o nosso hook Stop reinjetava o prompt da tarefa. Só que o prompt da tarefa era estático. Trazia as instruções originais e mais nada. Todo o contexto dinâmico que o Claude tinha acumulado — com quais contas interagiu, quais estratégias funcionaram, que padrões descobriu — tinha sumido. Compactado em um resumo que não incluía nada disso.
Não era um bug no sentido tradicional. Era uma lacuna de arquitetura. O sistema de loop tinha sido projetado só para autonomia dentro de uma sessão. Tinha zero resiliência entre compactações. Sem integração com a memória, sem acompanhamento de progresso, sem nenhum estado que sobrevivesse ao reset do contexto.
O sistema anti-brain rot
Construímos uma solução em três camadas:
Camada 1 — Diário do loop. Uma nova ação update na ferramenta MCP de loop permite que o Claude escreva um resumo curto depois de cada iteração. Esses resumos ficam guardados no arquivo de estado como um diário rotativo (últimas 10 entradas, 200 caracteres cada). O hook Stop injeta as últimas 3 entradas do diário no prompt de cada iteração, além de um resumo de progresso atualizado continuamente. Mesmo depois da compactação, o Claude recebe uma visão condensada do histórico recente.
Camada 2 — Memória obrigatória. A cada 5 iterações, o hook Stop bloqueia o loop até que o Claude chame remember para persistir o progresso acumulado na memória vetorial do SynaBun. Não é opcional. O hook literalmente se recusa a continuar o loop enquanto a chamada de memória não for feita. Se o Claude ignorar a diretiva, o hook tenta de novo com urgência crescente. Em seguida, o hook post-remember atualiza o estado do loop para registrar quando a memória foi salva pela última vez.
Camada 3 — Ponte de compactação. O hook pre-compact captura o estado do loop ativo antes de a compactação começar. O hook session-start procura esse estado capturado depois que a compactação termina e injeta um bloco ACTIVE LOOP RECOVERY com a tarefa completa, o diário, o resumo de progresso e uma diretiva para buscar com recall as memórias de progresso armazenadas. Além disso, há uma ancoragem de estilo em pontos de controle importantes (iterações 1, 10, 20, 30, 40) para reorientar o Claude nas regras originais de tom e formatação.
Também aprendemos uma lição dolorosa sobre hooks: os três hooks da cadeia precisam conhecer todos os modos de sessão. O hook session-start, o hook prompt-submit e o hook stop injetam instruções, cada um por conta própria. Quando ensinamos só um dos hooks a reconhecer o loop, os outros dois continuaram injetando contexto conflitante. Cada novo "modo" de operação precisa passar pelos três hooks, ou as coisas quebram de jeitos sutis e enlouquecedores.
A condição de corrida do /clear
Este ficou invisível por duas semanas. O driver do loop envia /clear entre as iterações, espera o prompt e então envia a mensagem da próxima iteração. Só que o driver faz polling a cada 2 segundos. Quando ele consulta, o Claude já terminou de responder e imprimiu o prompt >. O prompt está parado no buffer.
Passo 1 do driver: zerar o buffer. Depois, esperar uma nova saída que contenha o prompt.
Viu o problema? O prompt já estava no buffer. Zerar o buffer antes significava esperar um novo prompt que nunca chegaria. O driver esperava 75 segundos e então declarava falha na detecção do prompt. Depois de 10 falhas consecutivas, matava o loop.
Correção: Verificar se o prompt já está no buffer ANTES de zerá-lo. Se o Claude já está no prompt, pular a espera por completo e seguir para o /clear. O reset do buffer depois de enviar /clear ficou como estava — esse, sim, precisa esperar a saída pós-clear.
O vazamento de loop entre sessões
Três caminhos de código diferentes, no driver do loop e nas varreduras de fallback dos hooks, percorriam TODOS os arquivos de estado de loop no diretório de dados sem verificar o terminalSessionId. Se você tivesse duas sessões de terminal abertas — uma rodando um loop, outra em trabalho interativo —, o estado do loop de uma sessão vazava para a outra. O hook Stop da sessão interativa pegava o arquivo de estado do loop e começava a injetar prompts de iteração do loop em uma conversa que não tinha nada a ver com ele.
Correção: Adicionamos verificações de propriedade nos três caminhos de código. O driver do loop só se conecta a loops cujo terminalSessionId corresponde ao terminal atual. O hook prompt-submit preserva o terminalSessionId nas renomeações. A varredura de fallback confere a propriedade antes de assumir um arquivo de estado.
Semana três: os agentes entram na festa
Os loops finalmente estavam estáveis. Hora de adicionar agentes — processos do Claude Code em segundo plano, lançados pelo Estúdio de automações da Neural Interface. Você configura uma tarefa, escolhe um modelo, ativa as ferramentas do SynaBun e lança. O agente roda sozinho em segundo plano.
Nada funcionou.
O bug de timing do DOM
Os agentes lançados pelo Estúdio de automações sempre vinham com synabun=false e browser=none. Sem ferramentas MCP, sem navegador. Todo agente era inútil para o que deveria fazer.
Causa raiz: Em confirmLaunch(), a função chamava closeLaunchDialog() — que chama .remove() no elemento do diálogo — ANTES de ler os valores do DOM. Cinco chamadas a document.getElementById() liam de uma árvore DOM destruída. withSynabun retornava undefined (o elemento já não existia), que virava false. O seletor de submodo retornava null e caía no modo "single". As iterações ficavam no padrão de 1. O máximo de minutos ficava no padrão de 30.
Todas as opções de configuração, sem exceção, se perdiam em silêncio.
Correção: Mover as 5 leituras do DOM para antes de closeLaunchDialog(). Capturar os valores enquanto o diálogo existe e só então fechá-lo.
O sequestro pela sequência de boot do CLAUDE.md
Com a correção do DOM, os agentes finalmente eram lançados com as ferramentas do SynaBun. Mas gastavam os 4 primeiros turnos executando a sequência de boot do CLAUDE.md — cumprimentar o usuário, recuperar as sessões recentes, recuperar as preferências de estilo de comunicação — em vez de fazer a tarefa. O agente foi lançado como uma automação em segundo plano. Não há usuário para cumprimentar. Não há preferências de comunicação para carregar. Ele estava queimando turnos caros de API em cerimônia.
Depois ele soltava um texto como "Vou navegar até o Twitter..." sem chamar ferramenta nenhuma. No modo --print, uma resposta só de texto (sem blocos tool_use) encerra a sessão. O agente dizia o que ia fazer e morria antes de fazer.
Correção: Adicionamos um prompt de sistema defaultAgentPrompt que sobrescreve o comportamento de boot do CLAUDE.md. Instruções explícitas: pule o cumprimento, pule as recuperações, vá direto para a tarefa. Encadeie ToolSearch com chamadas de ferramenta de verdade na mesma resposta — nunca gere texto sem uma chamada de ferramenta. Subimos maxTurns de 25 para 200, para dar folga à automação de navegador.
A continuidade de sessão causava compactação
Os agentes usavam --resume para manter o contexto entre as iterações, com um SESSION_CONTINUITY_WINDOW de 5 (retomar a mesma sessão por 5 iterações consecutivas). Ou seja, o contexto se acumulava por 5 iterações antes de ser zerado. Em agentes de automação de navegador, que fazem de 10 a 15 chamadas de ferramenta por iteração, isso enchia a janela de contexto rapidinho. A compactação entrava em ação. O agente travava.
Correção: Sessão nova a CADA iteração. Nada de --resume, nunca. Gerar um novo randomUUID() para cada iteração. Cada iteração recebe a tarefa completa reinjetada, mais as entradas do diário das iterações anteriores. Um pouco mais de overhead de ToolSearch por iteração, mas o risco de compactação desaparece. O diário dá continuidade suficiente.
Iterações sem nenhuma chamada de ferramenta
No modo --print, respostas só de texto encerram a sessão com código de saída 0. O controlador de agentes via o código de saída 0 (sucesso) e passava para a próxima iteração. Mas a iteração não tinha feito nada — o modelo falava do que faria, sem fazer.
Correção: Contar as chamadas de ferramenta por iteração. Se uma iteração termina com 0 chamadas de ferramenta e código de saída 0, tentar de novo uma vez, com um prompt enfático: "Você PRECISA chamar ferramentas. Não descreva o que vai fazer. Faça."
A guerra de prioridades da compactação
Mesmo com sessões novas a cada iteração, os loops comuns (conduzidos pelo hook Stop) ainda esbarravam em problemas de compactação. O hook Stop tinha um sistema de prioridades: o CHECK 1 era a compactação (prioridade máxima), o CHECK 1.5 era a continuação do loop. Quando havia um loop ativo e uma compactação pendente, a verificação de compactação ganhava sempre. O loop ficava parado enquanto a compactação rodava e, às vezes, nunca se recuperava.
Correção: Adicionamos um PRE-CHECK que roda antes de todas as outras verificações. Uma varredura rápida por loops ativos. Se há um loop ativo, pular a verificação de compactação por completo e limpar qualquer flag pending-compact. O loop tem prioridade sobre a compactação porque a sessão nova a cada iteração já evita o inchaço de contexto — a compactação não é necessária.
O que aprendemos
Construir sistemas autônomos de IA não é um problema de engenharia de software. É um problema de gerenciamento de estado sobre um substrato não confiável. O Claude Code é uma caixa-preta com uma interface de terminal. Você não pode inspecionar o estado interno dele. Não pode prever quando a compactação vai disparar. Não pode controlar o comportamento do modelo de forma determinística. As únicas ferramentas que você tem são hooks (que disparam em pontos específicos do ciclo de vida), arquivos de estado (que você mesmo gerencia) e engenharia de prompt (que funciona até parar de funcionar).
Todas as suposições que fizemos sobre confiabilidade estavam erradas:
- "O prompt sempre vai ser detectável" — errado. Sequências de escape ANSI, tamanhos de buffer e timing, tudo conspira para escondê-lo.
- "Os IDs de sessão são estáveis" — errado. O
/cleargera um novo. A recuperação pós-compactação também. - "O contexto sobrevive à compactação" — errado. A compactação tem perdas. O estado dinâmico é a primeira coisa a ir embora.
- "O modelo segue instruções" — errado. Ele segue instruções até o contexto ficar lotado; aí começa a se desviar. As regras de estilo são a primeira baixa.
- "Um protótipo que funciona significa que o sistema funciona" — catastroficamente errado. Cada bug só aparecia depois da iteração 3, ou depois de 20 minutos, ou depois da compactação, ou sob carga concorrente.
A arquitetura final tem cinco arquivos de hook, um driver de loop no servidor, um controlador de agentes com lógica de retentativa, um sistema de diário, armazenamento obrigatório de memórias, uma ponte de compactação, verificações de propriedade e uma escalada para envio forçado quando a detecção do prompt falha três vezes seguidas. É complexa porque o problema é complexo. Tentamos abordagens mais simples primeiro. Todas morreram depois de 3 iterações.
O sistema de loops já roda campanhas de engajamento em redes sociais em produção há mais de uma semana. Loops de automação de navegador com 50 iterações, que sobrevivem à compactação, lidam bem com telas de login, mantêm o contexto por horas de execução e entregam qualidade consistente da iteração 1 à iteração 50.
Funciona. Só custou três semanas de inferno para chegar aqui.
Todos os bugs, correções e decisões de arquitetura documentados acima estão guardados na memória persistente do SynaBun — 21+ entradas em várias categorias. Toda vez que mexemos no sistema de loops, a IA que o construiu lembra exatamente o que deu errado da última vez. É para isso que serve a memória persistente. Sua IA deve aprender com os próprios erros para que você não precise explicá-los de novo.
E olha que, da primeira vez, tivemos que explicá-los vezes de sobra.