Dez agentes rodaram o mesmo typecheck ao mesmo tempo. A solução era uma pasta.
Dezessete agentes de código no mesmo checkout, dez tsc em paralelo, load average 37 e 87 MB de RAM livre. Um typecheck de noventa segundos levou 7 min 36. Aqui está a medição, por que a máquina não estava calculando, e o pequeno bloqueio compartilhado que resolveu. Copiável para qualquer repositório.
No dia 7 de setembro, nossa máquina de desenvolvimento parou de responder direito. Não foi um crash, não foi um congelamento. Simplesmente tudo levava dez vezes mais tempo, inclusive coisas que não tinham nada a ver com código.
Os suspeitos óbvios estavam todos errados. O portátil não estava esquentando: nenhum throttling registrado, bateria a 30,6 C. Nenhum processo desgovernado comia a CPU. Nada tinha sido publicado. A única coisa fora do comum é que dezessete CLIs de agentes estavam vivos no mesmo repositório, o que aqui é um dia de trabalho normal.
Veja o que estava realmente acontecendo, medido e não adivinhado.
A medição
Máquina de 16 GB, 8 núcleos, ligada havia cinco horas e meia, dezessete agentes trabalhando:
| O que medimos | Valor |
|---|---|
| CLIs de agentes vivos | 17 |
Processos tsc --noEmit concorrentes, vistos em dois minutos | 3, depois 10 |
| Load average | 37 a 41 |
| RAM livre / compressor de memória | 87 MB / 7,2 GB |
| Um typecheck no app desktop, máquina saturada | 7 min 36 de relógio para 26 s de CPU |
| O mesmo typecheck, máquina calma | 33 s |
A linha decisiva é a penúltima. Vinte e seis segundos de CPU espalhados por sete minutos e meio dão dez por cento de utilização. O typecheck não estava calculando. Estava esperando memória.
E um desses processos foi morto pelo sistema no meio do caminho. Um tsc morto sai com código diferente de zero e saída vazia, o que é indistinguível de um erro de tipo de verdade. Então, além de lenta, a máquina produzia veredictos em que ninguém podia confiar.
Ninguém fez nada de errado
Esta é a parte que merece uma pausa, porque é ela que torna a pane tão difícil de prever.
Cada um daqueles agentes seguia a regra. Cada um tinha editado TypeScript. Cada um tinha a instrução de verificar seus tipos antes de devolver a mão. Cada um rodou tsc --noEmit. Nenhum enxergava os outros. Não existe um quadro comum onde um agente escreva "estou fazendo a coisa cara agora, esperem".
Depois isso se retroalimenta. O typecheck fica lento porque a máquina está saturada. O agente que o observa conclui que ele travou. Então mata e inicia outro. Esse reflexo está certo isoladamente e é catastrófico em grupo, e é a mesma família de pane que documentamos um mês antes, quando agentes deixavam para trás processos de busca travados: o Process Guard é a rede que encontra o que já começou, aqui a gente impede o começo.
As três respostas que não escolhemos
Rodar menos agentes. Isso divide o sintoma e mantém o bug. Dois typechecks simultâneos em uma máquina carregada continuam mais lentos que um só, e reduzir a frota é pagar o problema com aquilo que dá velocidade ao trabalho.
Um único typecheck no final. Tentador, e errado por uma razão que nada tem a ver com desempenho. Um erro de tipo encontrado dez tickets depois é órfão: o agente que o escreveu está fechado, seu contexto se perdeu, e um humano precisa reabrir o assunto inteiro para corrigir uma linha. Não queríamos adiar a verificação.
A compilação incremental. Testada e descartada. O ganho é duvidoso no modo --noEmit, e processos concorrentes corrompem o .tsbuildinfo compartilhado. Ela resolve metade do problema piorando a outra metade.
O que fizemos no lugar: uma única verificação, compartilhada
A regra não é "verificar menos vezes". É um typecheck por vez, por projeto, para todo mundo. Um script de invólucro substitui N verificações por uma só, e responde a três casos:
- Nada mudou desde a última execução, então devolvemos o resultado dela.
- Uma execução já está em andamento, então a gente espera e pega o resultado dela.
- Caso contrário, pegamos o bloqueio e somos o único
tscda máquina.
Do ponto de vista do agente, nada mudou: ele digita yarn typecheck, ele recebe seus erros de tipo. E ele nunca espera mais do que antes, porque uma execução que ele aguarda é uma execução que começou antes da dele. A máquina paga por uma em vez de dez.
É a ideia inteira. O interessante é que os dois mecanismos de que ela precisa são bem menores do que se imagina.
O bloqueio é uma pasta
Não é um arquivo, não é um banco, não é um daemon. Uma pasta.
try {
mkdirSync(lockDir); // deu certo: o bloqueio é nosso
} catch (err) {
if (err.code === 'EEXIST') { /* alguém está segurando, a gente espera */ }
}
mkdir cria a pasta ou falha com EEXIST, e faz isso de forma atômica no macOS, Windows e Linux, sem dependência nem chamada nativa. Escrever um arquivo e depois verificar se ele existe seriam duas operações, e duas operações é exatamente onde um segundo agente se enfia.
Dentro da pasta a gente deixa um owner.json com o pid, o nome do host e a hora de início. Esse arquivo serve ao diagnóstico e à detecção de um bloqueio morto. Nunca é ele que exclui.
Um bloqueio morto é retomado automaticamente em dois casos: o processo dono sumiu (verificado apenas quando o nome do host bate, porque um pid não significa nada de uma máquina para outra), ou o bloqueio tem mais de quinze minutos.
Uma armadilha que nos custou um bug. Entre o mkdir e a escrita do owner.json existe uma janela em que o dono é ilegível. Declarar o bloqueio morto nessa janela é roubá-lo de quem acabou de pegá-lo, exatamente a corrida que o arquivo existe para evitar. Então, quando nenhum dono é legível, a gente julga pela idade da pasta, não pelo arquivo ausente.
A impressão digital é uma data e uma contagem
O caso 1 precisa saber se algo mudou desde a última execução. A resposta óbvia seria calcular o hash dos arquivos de origem. A gente não faz isso.
A impressão digital é <mtime mais recente>:<número de arquivos> sobre as raízes deduzidas do campo include do tsconfig, mais o próprio tsconfig.
Em 2.300 arquivos, ler cada byte custa mais caro que a verificação que se economiza. Só a data não enxerga uma remoção. Só a contagem não enxerga uma edição. Juntas, cobrem as duas. O falso negativo assumido são duas edições no mesmo milissegundo que deixam a contagem idêntica, e o pior caso ali é um resultado de cache alguns segundos desatualizado, nunca um erro de tipo silencioso, porque a verificação bloqueante continua sendo a do build.
Uma regra escrita não bastou, então acrescentamos um hook
A instrução estava no AGENTS.md desde o primeiro dia: nunca rodar tsc direto, sempre o script compartilhado. Não bastou, e vale a pena dizer honestamente por quê.
Verificar os tipos depois de uma edição é um reflexo profundamente enraizado. Sob pressão, um agente digita npx tsc --noEmit sem reler as instruções. E basta um agente furar a regra para recriar o acúmulo que o bloqueio existe para evitar. Uma instrução se discute. Um hook não.
Então ligamos um hook PreToolUse na ferramenta Bash, que recusa um tsc direto e nomeia o comando certo na recusa:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/scripts/hooks/block-direct-tsc.mjs\"" }
]
}
]
}
}
O hook lê a chamada da ferramenta em JSON no stdin, sai com 2 e o motivo no stderr para recusar, sai com 0 para deixar passar. Dois detalhes fazem a diferença entre um hook útil e um hook chato.
Ele reconhece tsc em posição de comando, não em qualquer lugar da string. Procurar as três letras em qualquer lugar recusaria também grep -rn tsc AGENTS.md. O padrão exige então tsc no começo de linha ou depois de ;, &&, ||, | ou (, eventualmente precedido de um executor de pacotes e de um caminho. Ele também deixa passar tsc --version: não há razão para recusar uma opção informativa.
Ele recusa uma segunda coisa que não tínhamos antecipado. Vimos um agente esperar atrás do bloqueio, decidir depois de um tempo que ele devia estar morto, e apagar a pasta de bloqueio para se destravar. Isso inicia um segundo processo pesado ao lado do vivo, ou seja, o desvio perfeito de tudo que o bloqueio protege. Apagar a pasta de bloqueio ou de cache passou a ser recusado também, com a explicação de que um bloqueio morto é retomado sozinho.
Essa segunda recusa a gente nunca teria escrito por antecipação. Ela veio de observar o que os agentes realmente fazem quando estão travados, o que é uma fonte de proteções melhor do que imaginar o que eles poderiam fazer.
Onde isso para
O hook é específico do Claude Code. Os outros CLIs de agentes da frota só enxergam a regra escrita. É um buraco conhecido e a gente o assume: uma proteção que cobre a maior parte da frota vale mais do que nenhuma proteção enquanto se espera um padrão de hooks que todos os CLIs leiam.
O script compartilhado, esse, é neutro em relação ao fornecedor, já que é um comando como outro qualquer. Qualquer CLI capaz de rodar yarn typecheck aproveita o bloqueio, seja obrigado a isso ou não.
O que levar disso
O typecheck era nosso caso mais barulhento, não um caso especial. O padrão se aplica a todo comando caro, idempotente em uma janela curta, e iniciado por cada agente pela mesma boa razão: instalar as dependências, rodar a suíte de testes completa, construir para produção, subir um servidor de dev em uma porta fixa.
Três perguntas, nesta ordem, e você tem o projeto inteiro:
- Posso reaproveitar um resultado recente?
- Posso entrar na execução que já está em andamento?
- Caso contrário, sou eu quem a inicia, sozinho?
Se vários agentes compartilham sua máquina, o que merece ser medido não é quantos estão rodando. É quantos deles iniciam o mesmo comando no mesmo minuto. Esse número é o que sua máquina sente de verdade, e enquanto você não olhar para ele, vai culpar o calor.
Se você quer a visão de conjunto sobre como fazemos vários agentes rodarem no mesmo repositório sem que eles pisem uns nos outros, isso está em Executar agentes de código em paralelo, e a rede de segurança para os processos que de fato começaram é o Process Guard. Tanto o script compartilhado quanto o hook vivem no repositório do AgentsRoom, ou seja, exatamente onde aqueles dezessete agentes trabalhavam naquela tarde.
Baixar AgentsRoom
Rode todos os seus agentes de IA, em todos os seus projetos, de uma única janela.
App complementar: acompanhe seus agentes em qualquer lugar
Use Claude, Codex, Antigravity CLI ou outro provedor de IA.
Envie bugs e pedidos direto para o seu backlog público.
Continue lendo
30 eventos de hook disparam em uma sessão do Claude Code. Só 3 podem responder.
A lista completa dos eventos de hook do Claude Code, quando cada um dispara, quais 15 podem bloquear e a regra de stdout que engole silenciosamente a saída da maioria dos hooks. Uma referência de campo construída rodando hooks em produção em milhares de sessões de agentes.
Leia o artigoComo Escalar Agentes de Codificação de IA em uma Equipe de Desenvolvimento
Um desenvolvedor com um agente de codificação é uma história de produtividade. Cinco desenvolvedores com vinte agentes é um problema de coordenação. Aqui está o que quebra primeiro quando uma equipe se expande, e a configuração que se mantém: arquivos de contexto comprometidos, propriedade clara dos arquivos, revisão por raio de explosão e custo que você pode realmente ver.
Leia o artigoMeu treinador de corrida é um repositório Git e um agente Claude
Termino o treino, meu relógio sincroniza e três minutos depois a análise está escrita no meu repositório, a semana foi reajustada e meu treinador deixou um comentário embaixo da atividade no Strava. Nenhum app desenvolvido, nenhum servidor escrito, nenhuma fatura de API por token: uma assinatura Claude, o AgentsRoom e arquivos Markdown. Aqui está a montagem completa, reprodutível.
Leia o artigo