Dix agents ont lancé le même typecheck en même temps. La solution était un dossier.

Dix-sept agents de code dans le même checkout, dix tsc en parallèle, load average 37 et 87 Mo de RAM libre. Un typecheck de quatre-vingt-dix secondes a mis 7 min 36. Voici la mesure, pourquoi la machine ne calculait pas, et le petit verrou partagé qui a réglé le problème. Recopiable dans n'importe quel dépôt.

Le 7 septembre, notre machine de développement a cessé de répondre correctement. Pas un plantage, pas un gel. Simplement, tout mettait dix fois plus de temps, y compris ce qui n'avait rien à voir avec le code.

Les suspects évidents étaient tous faux. Le portable ne chauffait pas : aucun throttling enregistré, batterie à 30,6 C. Aucun process fou ne mangeait le CPU. Rien n'avait été déployé. La seule chose inhabituelle, c'est que dix-sept CLI d'agents étaient vivants dans le même dépôt, ce qui est une journée de travail normale chez nous.

Voilà ce qui se passait vraiment, mesuré et pas deviné.

La mesure

Machine 16 Go, 8 cœurs, allumée depuis cinq heures et demie, dix-sept agents au travail :

Ce qu'on a mesuréValeur
CLI d'agents vivants17
tsc --noEmit concurrents, constatés en deux minutes3, puis 10
Load average37 à 41
RAM libre / compresseur mémoire87 Mo / 7,2 Go
Un typecheck sur l'app desktop, machine saturée7 min 36 de mur pour 26 s de CPU
Le même typecheck, machine calme33 s

La ligne décisive est l'avant-dernière. Vingt-six secondes de CPU étalées sur sept minutes et demie, ça fait dix pour cent d'utilisation. Le typecheck ne calculait pas. Il attendait de la mémoire.

Et un de ces process a été tué par le système en cours de route. Un tsc tué sort avec un code non nul et une sortie vide, ce qui est indiscernable d'une vraie erreur de type. Donc en plus d'être lente, la machine produisait des verdicts auxquels personne ne pouvait se fier.

Personne n'a rien fait de mal

C'est la partie qui mérite qu'on s'y arrête, parce que c'est elle qui rend la panne si difficile à voir venir.

Chacun de ces agents suivait la règle. Chacun avait édité du TypeScript. Chacun avait pour consigne de vérifier ses types avant de rendre la main. Chacun a lancé tsc --noEmit. Aucun ne voyait les autres. Il n'existe pas de tableau commun où un agent écrirait « je suis en train de faire le truc cher, attendez ».

Ensuite ça s'auto-alimente. Le typecheck ralentit parce que la machine est saturée. L'agent qui le surveille en conclut qu'il est bloqué. Alors il le tue et en relance un. Ce réflexe est correct tout seul et catastrophique en groupe, et c'est la même famille de panne que celle documentée un mois plus tôt quand des agents laissaient derrière eux des process de recherche coincés : Process Guard est le filet qui trouve ce qui a démarré, ici on empêche le démarrage.

Les trois réponses qu'on n'a pas prises

Lancer moins d'agents. Ça divise le symptôme et ça garde le bug. Deux typechecks simultanés sur une machine chargée sont toujours plus lents qu'un seul, et réduire la flotte, c'est payer le problème avec ce qui fait la vitesse du travail.

Un seul typecheck à la fin. Tentant, et faux pour une raison qui n'a rien à voir avec la performance. Une erreur de type trouvée dix tickets plus tard est orpheline : l'agent qui l'a écrite est fermé, son contexte est perdu, et un humain doit rouvrir tout le sujet pour corriger une ligne. On ne voulait pas différer la vérification.

La compilation incrémentale. Testée et écartée. Le gain est douteux en mode --noEmit, et des process concurrents corrompent le .tsbuildinfo partagé. Elle règle la moitié du problème en aggravant l'autre.

Ce qu'on a fait à la place : un seul check, partagé

La règle n'est pas « vérifier moins souvent ». C'est un typecheck à la fois, par projet, pour tout le monde. Un script d'enrobage remplace N vérifications par une seule, et répond à trois cas :

  1. Rien n'a changé depuis le dernier run, on renvoie son résultat.
  2. Un run est déjà en cours, on l'attend et on prend son résultat.
  3. Sinon, on prend le verrou et on est le seul tsc de la machine.

Du point de vue de l'agent, rien n'a changé : il tape yarn typecheck, il obtient ses erreurs de type. Il n'attend jamais plus longtemps qu'avant non plus, parce qu'un run qu'il attend est un run qui a démarré avant le sien. La machine en paie un au lieu de dix.

C'est toute l'idée. Ce qui est intéressant, c'est que les deux mécanismes dont elle a besoin sont beaucoup plus petits qu'on ne l'imagine.

Le verrou est un dossier

Pas un fichier, pas une base, pas un démon. Un dossier.

try {
  mkdirSync(lockDir);       // ça passe : le verrou est à nous
} catch (err) {
  if (err.code === 'EEXIST') { /* quelqu'un le tient, on attend */ }
}

mkdir crée le dossier ou échoue en EEXIST, et il le fait de façon atomique sur macOS, Windows et Linux, sans dépendance ni appel natif. Écrire un fichier puis vérifier s'il existe, ce serait deux opérations, et deux opérations c'est précisément l'endroit où un deuxième agent se glisse.

Dans le dossier, on dépose un owner.json avec le pid, le nom d'hôte et l'heure de départ. Ce fichier sert au diagnostic et à la détection d'un verrou mort. Ce n'est jamais lui qui exclut.

Un verrou mort est repris automatiquement dans deux cas : le process propriétaire a disparu (vérifié seulement si le nom d'hôte correspond, un pid n'ayant aucun sens d'une machine à l'autre), ou le verrou a plus de quinze minutes.

Un piège qui nous a coûté un bug. Entre le mkdir et l'écriture d'owner.json, il existe une fenêtre où le propriétaire est illisible. Déclarer le verrou mort dans cette fenêtre, c'est le voler à celui qui vient de le prendre, exactement la course que le fichier existe pour empêcher. Donc quand aucun propriétaire n'est lisible, on juge sur l'âge du dossier, pas sur le fichier absent.

L'empreinte est une date et un compte

Le cas 1 a besoin de savoir si quelque chose a changé depuis le dernier run. La réponse évidente serait de hacher les fichiers source. On ne le fait pas.

L'empreinte, c'est <mtime la plus récente>:<nombre de fichiers> sur les racines déduites du champ include du tsconfig, plus le tsconfig lui-même.

Sur 2 300 fichiers, lire chaque octet coûte plus cher que la vérification qu'on économise. La date seule ne voit pas une suppression. Le compte seul ne voit pas une édition. Ensemble, ils couvrent les deux. Le faux négatif assumé, ce sont deux modifications dans la même milliseconde qui laissent le compte identique, et le pire cas est un résultat de cache périmé de quelques secondes, jamais une erreur de type silencieuse, parce que la vérification bloquante reste celle du build.

Une règle écrite n'a pas suffi, on a ajouté un hook

La consigne était dans AGENTS.md depuis le premier jour : jamais tsc en direct, toujours le script partagé. Ça n'a pas suffi, et ça vaut la peine de dire honnêtement pourquoi.

Vérifier ses types après une édition est un réflexe profondément ancré. Sous pression, un agent tape npx tsc --noEmit sans relire les consignes. Et il suffit d'un seul agent qui déroge pour recréer la meute que le verrou existe pour empêcher. Une consigne se discute. Un hook non.

On a donc branché un hook PreToolUse sur l'outil Bash, qui refuse un tsc direct et nomme la bonne commande dans le refus :

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/scripts/hooks/block-direct-tsc.mjs\"" }
        ]
      }
    ]
  }
}

Le hook lit l'appel d'outil en JSON sur stdin, sort en 2 avec le motif sur stderr pour refuser, en 0 pour laisser passer. Deux détails font la différence entre un hook utile et un hook pénible.

Il reconnaît tsc en position de commande, pas n'importe où dans la chaîne. Chercher les trois lettres partout refuserait aussi grep -rn tsc AGENTS.md. Le motif exige donc tsc en début de ligne ou après ;, &&, ||, | ou (, éventuellement précédé d'un lanceur de paquets et d'un chemin. Il laisse aussi passer tsc --version : aucune raison de refuser une option informative.

Il refuse une deuxième chose qu'on n'avait pas anticipée. On a vu un agent attendre derrière le verrou, décider au bout d'un moment qu'il devait être mort, et supprimer le dossier de verrou pour se débloquer. Ça démarre un deuxième process lourd à côté du vivant, c'est-à-dire le contournement parfait de tout ce que le verrou protège. Supprimer le dossier de verrou ou de cache est donc refusé aussi, avec l'explication qu'un verrou mort est repris tout seul.

Ce second refus, on ne l'aurait jamais écrit à l'avance. Il vient de l'observation de ce que les agents font vraiment quand ils sont bloqués, ce qui est une meilleure source de garde-fous que d'imaginer ce qu'ils pourraient faire.

Là où ça s'arrête

Le hook est propre à Claude Code. Les autres CLI d'agents de la flotte ne voient que la règle écrite. C'est un trou connu et on l'assume : un garde-fou qui couvre la majorité de la flotte vaut mieux qu'aucun garde-fou en attendant un standard de hooks que tous les CLI lisent.

Le script partagé, lui, est neutre vis-à-vis du fournisseur, puisque c'est une commande comme une autre. N'importe quel CLI capable de lancer yarn typecheck profite du verrou, qu'on l'y oblige ou non.

Ce qu'il faut en retenir

Le typecheck était notre cas le plus bruyant, pas un cas particulier. Le motif s'applique à toute commande chère, idempotente sur une courte fenêtre, et lancée par chaque agent pour la même bonne raison : installer les dépendances, jouer la suite de tests complète, construire pour la production, démarrer un serveur de dev sur un port fixe.

Trois questions, dans cet ordre, et tu as toute la conception :

  1. Est-ce que je peux réutiliser un résultat récent ?
  2. Est-ce que je peux rejoindre le run déjà en cours ?
  3. Sinon, est-ce que je suis celui qui le démarre, tout seul ?

Si plusieurs agents partagent ta machine, ce qui mérite d'être mesuré n'est pas combien tournent. C'est combien d'entre eux démarrent la même commande dans la même minute. Ce nombre-là, c'est ce que ta machine ressent vraiment, et tant que tu ne le regardes pas, tu accuseras la chaleur.

Si tu veux la vue d'ensemble sur la façon dont on fait tourner plusieurs agents sur un même dépôt sans qu'ils se marchent dessus, c'est dans Faire tourner des agents de code en parallèle, et le filet de sécurité pour les process qui, eux, ont bien démarré, c'est Process Guard. Le script partagé et le hook vivent tous les deux dans le dépôt d'AgentsRoom, c'est-à-dire là où ces dix-sept agents travaillaient cet après-midi-là.

Télécharger AgentsRoom

Lancez tous vos agents IA, sur tous vos projets, depuis une seule fenêtre.

GratuitTélécharger AgentsRoom

App companion : suivez vos agents en déplacement

Utilisez Claude, Codex, Antigravity CLI ou un autre fournisseur IA.

Installer l'extension
Chrome Web Store

Remontez bugs et demandes directement dans votre backlog public.

Aperçu d'AgentsRoom en action.

Multi-projets
Multi-provider
Multi-agents
Statut en direct
Diff & commit
App mobile
Aperçu live
Équipes d'agents
Tests navigateur
Dev pilotée par backlog
Bibliothèque de prompts
Bibliothèque de skills
Voir toutes les fonctionnalités

Continuer la lecture