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 misuratoValore
CLI di agenti vivi17
Processi tsc --noEmit concorrenti, visti in due minuti3, poi 10
Load averageda 37 a 41
RAM libera / compressore di memoria87 MB / 7,2 GB
Un typecheck sull'app desktop, macchina satura7 min 36 di orologio per 26 s di CPU
Lo stesso typecheck, macchina tranquilla33 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:

  1. Non è cambiato niente dall'ultima esecuzione, quindi restituisce il suo risultato.
  2. Un'esecuzione è già in corso, quindi la aspetta e ne prende il risultato.
  3. Altrimenti prende il lock ed è l'unico tsc della 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:

  1. Posso riutilizzare un risultato recente?
  2. Posso unirmi all'esecuzione già in corso?
  3. 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.

GratisScarica AgentsRoom

App companion: monitora i tuoi agenti in movimento

Usa Claude, Codex, Antigravity CLI o un altro provider IA.

Installa l'estensione
Chrome Web Store

Invia bug e richieste direttamente nel tuo backlog pubblico.

Uno sguardo ad AgentsRoom in azione.

Multi-progetto
Multi-provider
Multi-agente
Stato in tempo reale
Diff e commit
App mobile
Anteprima live
Team di agenti
Test browser
Dev guidata da backlog
Libreria di prompt
Libreria di skill
Vedi tutte le funzionalità

Continua a leggere