Dieci agenti hanno lanciato lo stesso typecheck insieme. La soluzione era una cartella.
Diciassette agenti di codice nello stesso checkout, dieci processi tsc insieme, load average 37 e 87 MB di RAM libera. Un typecheck da novanta secondi ci ha messo 7 min 36. Ecco la misura, perché la macchina non calcolava, e il piccolo lock condiviso che ha risolto. Copiabile in qualsiasi repository.
Il 7 settembre la nostra macchina di sviluppo ha smesso di rispondere come si deve. Non un crash, non un blocco. Semplicemente tutto ci metteva dieci volte più tempo, comprese cose che non avevano niente a che fare con il codice.
I sospetti ovvi erano tutti sbagliati. Il portatile non scaldava: nessun throttling registrato, batteria a 30,6 C. Nessun processo impazzito mangiava la CPU. Non era stato deployato niente. L'unica cosa insolita era che diciassette CLI di agenti erano vivi nello stesso repository, che qui è una normale giornata di lavoro.
Ecco cosa stava succedendo davvero, misurato e non indovinato.
La misura
Macchina da 16 GB, 8 core, accesa da cinque ore e mezza, diciassette agenti al lavoro:
| Cosa abbiamo misurato | Valore |
|---|---|
| CLI di agenti vivi | 17 |
Processi tsc --noEmit concorrenti, visti in due minuti | 3, poi 10 |
| Load average | da 37 a 41 |
| RAM libera / compressore di memoria | 87 MB / 7,2 GB |
| Un typecheck sull'app desktop, macchina satura | 7 min 36 di orologio per 26 s di CPU |
| Lo stesso typecheck, macchina tranquilla | 33 s |
La riga decisiva è la penultima. Ventisei secondi di CPU distribuiti su sette minuti e mezzo fanno dieci per cento di utilizzo. Il typecheck non calcolava. Aspettava memoria.
E uno di quei processi è stato ucciso dal sistema operativo a metà strada. Un tsc ucciso esce con un codice diverso da zero e un output vuoto, il che è indistinguibile da un vero errore di tipo. Quindi, oltre a essere lenta, la macchina produceva verdetti di cui nessuno poteva fidarsi.
Nessuno ha sbagliato niente
Questa è la parte su cui vale la pena fermarsi, perché è quella che rende il guasto così difficile da vedere arrivare.
Ognuno di quegli agenti seguiva la regola. Ognuno aveva modificato TypeScript. Ognuno aveva la consegna di verificare i propri tipi prima di restituire il lavoro. Ognuno ha lanciato tsc --noEmit. Nessuno vedeva gli altri. Non esiste una lavagna comune su cui un agente scriva: sto facendo la cosa costosa, aspettate un attimo.
Poi si autoalimenta. Il typecheck rallenta perché la macchina è satura. L'agente che lo sorveglia ne conclude che è bloccato. Allora lo uccide e ne rilancia un altro. Quel riflesso è corretto da solo e catastrofico in gruppo, ed è la stessa famiglia di guasto che avevamo documentato un mese prima, quando gli agenti si lasciavano dietro processi di ricerca bloccati: Process Guard è la rete che trova quello che è partito, qui impediamo la partenza.
Le tre risposte che non abbiamo preso
Lanciare meno agenti. Dimezza il sintomo e si tiene il bug. Due typecheck simultanei su una macchina carica sono comunque più lenti di uno solo, e ridurre la flotta significa pagare il problema con ciò che rende veloce il lavoro.
Un solo typecheck alla fine. Allettante, e sbagliato per una ragione che non ha niente a che vedere con le prestazioni. Un errore di tipo trovato dieci ticket dopo è orfano: l'agente che lo ha scritto è chiuso, il suo contesto è perso, e una persona deve riaprire tutto l'argomento per correggere una riga. Non volevamo rimandare la verifica.
La compilazione incrementale. Testata e scartata. Il guadagno è dubbio in modalità --noEmit, e processi concorrenti corrompono il .tsbuildinfo condiviso. Risolve metà del problema peggiorando l'altra metà.
Cosa abbiamo fatto invece: un solo controllo, condiviso
La regola non è "verificare meno spesso". È un typecheck alla volta, per progetto, per tutti. Uno script wrapper sostituisce N verifiche con una sola, e risponde a tre casi:
- Non è cambiato niente dall'ultima esecuzione, quindi restituisce il suo risultato.
- Un'esecuzione è già in corso, quindi la aspetta e ne prende il risultato.
- Altrimenti prende il lock ed è l'unico
tscdella macchina.
Dal punto di vista dell'agente non è cambiato niente: digita yarn typecheck, ottiene i suoi errori di tipo. E non aspetta nemmeno mai più di prima, perché un'esecuzione dietro cui aspetta è un'esecuzione partita prima della sua. La macchina ne paga una invece di dieci.
Questa è tutta l'idea. La parte interessante è che i due meccanismi che le servono sono molto più piccoli di quanto ci si aspetti.
Il lock è una cartella
Non un file, non un database, non un demone. Una cartella.
try {
mkdirSync(lockDir); // riesce: il lock è nostro
} catch (err) {
if (err.code === 'EEXIST') { /* lo tiene qualcun altro, aspettiamo */ }
}
mkdir crea la cartella oppure fallisce con EEXIST, e lo fa in modo atomico su macOS, Windows e Linux, senza dipendenze né chiamate native. Scrivere un file e poi controllare se esiste sarebbero due operazioni, e due operazioni sono esattamente il punto in cui si infila un secondo agente.
Dentro la cartella depositiamo un owner.json con il pid, il nome host e l'ora di avvio. Quel file serve alla diagnosi e a rilevare un lock morto. Non è mai lui a escludere.
Un lock morto viene ripreso automaticamente in due casi: il processo proprietario è sparito (verificato solo se il nome host corrisponde, visto che un pid non significa nulla da una macchina all'altra), oppure il lock ha più di quindici minuti.
Una trappola che ci è costata un bug. Tra il mkdir e la scrittura di owner.json esiste una finestra in cui il proprietario non è leggibile. Dichiarare il lock morto in quella finestra significa rubarlo a chi lo ha appena preso, esattamente la corsa che il file esiste per impedire. Quindi quando nessun proprietario è leggibile, giudichiamo sull'età della cartella, non sul file assente.
L'impronta è una data e un conteggio
Il caso 1 ha bisogno di sapere se qualcosa è cambiato dall'ultima esecuzione. La risposta ovvia sarebbe fare l'hash dei file sorgente. Non lo facciamo.
L'impronta è <mtime più recente>:<numero di file> sulle radici dedotte dal campo include del tsconfig, più il tsconfig stesso.
Su 2.300 file, leggere ogni byte costa più della verifica che si risparmia. La data da sola non vede una cancellazione. Il conteggio da solo non vede una modifica. Insieme coprono entrambe. Il falso negativo assunto sono due modifiche nello stesso millisecondo che lasciano il conteggio identico, e il caso peggiore è un risultato di cache vecchio di qualche secondo, mai un errore di tipo silenzioso, perché la verifica bloccante resta quella della build.
Una regola scritta non è bastata, abbiamo aggiunto un hook
La consegna era in AGENTS.md dal primo giorno: mai tsc diretto, sempre lo script condiviso. Non è bastata, e vale la pena dire onestamente perché.
Verificare i tipi dopo una modifica è un riflesso radicato in profondità. Sotto pressione, un agente digita npx tsc --noEmit senza rileggere le consegne. E basta un solo agente che deroghi per ricreare la calca che il lock esiste per impedire. Una consegna si discute. Un hook no.
Abbiamo quindi collegato un hook PreToolUse allo strumento Bash, che rifiuta un tsc diretto e nomina il comando giusto nel rifiuto:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/scripts/hooks/block-direct-tsc.mjs\"" }
]
}
]
}
}
L'hook legge la chiamata allo strumento come JSON su stdin, esce con 2 e il motivo su stderr per rifiutare, con 0 per lasciar passare. Due dettagli fanno la differenza tra un hook utile e uno fastidioso.
Riconosce tsc in posizione di comando, non in un punto qualsiasi della stringa. Cercare le tre lettere ovunque rifiuterebbe anche grep -rn tsc AGENTS.md. Il pattern richiede quindi tsc a inizio riga o dopo ;, &&, ||, | oppure (, eventualmente preceduto da un lanciatore di pacchetti e da un percorso. Lascia passare anche tsc --version: non c'è motivo di rifiutare un'opzione informativa.
Rifiuta una seconda cosa che non avevamo previsto. Abbiamo visto un agente aspettare dietro il lock, decidere dopo un po' che doveva essere morto, e cancellare la cartella di lock per sbloccarsi. Questo avvia un secondo processo pesante accanto a quello vivo, cioè l'aggiramento perfetto di tutto ciò che il lock protegge. Cancellare la cartella di lock o di cache viene quindi rifiutato anch'esso, con la spiegazione che un lock morto viene ripreso da solo.
Quel secondo rifiuto non lo avremmo mai scritto in anticipo. Viene dall'osservare cosa fanno davvero gli agenti quando sono bloccati, che è una fonte di guardrail migliore dell'immaginare cosa potrebbero fare.
Dove si ferma
L'hook è specifico di Claude Code. Gli altri CLI di agenti della flotta vedono solo la regola scritta. È un buco noto e ce lo assumiamo: un guardrail che copre la maggior parte della flotta vale più di nessun guardrail, in attesa di uno standard di hook che tutti i CLI leggano.
Lo script condiviso, invece, è neutro rispetto al fornitore, perché è un comando come un altro. Qualsiasi CLI capace di lanciare yarn typecheck beneficia del lock, che ci sia costretto o no.
Cosa portarsi via
Il typecheck era il nostro caso più rumoroso, non un caso particolare. Lo schema si applica a qualsiasi comando costoso, idempotente su una finestra breve, e lanciato da ogni agente per la stessa buona ragione: installare le dipendenze, far girare la suite di test completa, costruire per la produzione, avviare un server di sviluppo su una porta fissa.
Tre domande, in quest'ordine, e hai tutto il progetto:
- Posso riutilizzare un risultato recente?
- Posso unirmi all'esecuzione già in corso?
- Altrimenti, sono io quello che la avvia, da solo?
Se più agenti condividono la tua macchina, quello che vale la pena misurare non è quanti stanno girando. È quanti di loro avviano lo stesso comando nello stesso minuto. Quel numero è ciò che la tua macchina sente davvero, e finché non lo guardi, darai la colpa al calore.
Se vuoi il quadro d'insieme su come facciamo girare più agenti su uno stesso repository senza che si pestino i piedi, è in Eseguire agenti di codice in parallelo, e la rete di sicurezza per i processi che invece sono partiti è Process Guard. Lo script condiviso e l'hook vivono entrambi nel repository di AgentsRoom, cioè proprio dove quei diciassette agenti stavano lavorando quel pomeriggio.
Scarica AgentsRoom
Esegui tutti i tuoi agenti AI, su tutti i tuoi progetti, da una sola finestra.
App companion: monitora i tuoi agenti in movimento
Usa Claude, Codex, Antigravity CLI o un altro provider IA.
Invia bug e richieste direttamente nel tuo backlog pubblico.
Uno sguardo ad AgentsRoom in azione.
Continua a leggere
30 eventi di hook si attivano in una sessione Claude Code. Solo 3 possono rispondere.
L'elenco completo degli eventi di hook di Claude Code, quando si attiva ciascuno, quali 15 possono bloccare, e la regola di stdout che si mangia in silenzio l'output della maggior parte degli hook. Un riferimento sul campo costruito eseguendo hook in produzione su migliaia di sessioni di agenti.
Leggi l'articoloCome Scalare gli Agenti di Codifica AI in un Team di Sviluppo
Un sviluppatore con un agente di codifica è una storia di produttività. Cinque sviluppatori con venti agenti sono un problema di coordinamento. Ecco cosa si rompe per primo quando un team si espande, e la configurazione che tiene: file di contesto impegnati, chiara proprietà dei file, revisione per raggio d'azione e costi che puoi effettivamente vedere.
Leggi l'articoloIl mio allenatore di corsa è un repository Git e un agente Claude
Finisco la seduta, l'orologio si sincronizza e tre minuti dopo l'analisi è scritta nel mio repository, la settimana è stata riaggiustata e il mio allenatore ha lasciato un commento sotto l'attività Strava. Nessuna app sviluppata, nessun server scritto, nessuna fattura API a token: un abbonamento Claude, AgentsRoom e file Markdown. Ecco il montaggio completo, riproducibile.
Leggi l'articolo