Tien agents draaiden dezelfde typecheck tegelijk. De oplossing was een map.

Zeventien coding agents in dezelfde checkout, tien tsc-processen tegelijk, load average 37 en 87 MB vrij geheugen. Een typecheck van negentig seconden duurde 7 min 36. Dit is de meting, waarom de machine niet aan het rekenen was, en het kleine gedeelde slot dat het oploste. Over te nemen in elke repository.

Op 7 september stopte onze ontwikkelmachine met fatsoenlijk reageren. Geen crash, geen bevriezing. Alles duurde simpelweg tien keer langer, ook dingen die niets met code te maken hadden.

De voor de hand liggende verdachten waren allemaal fout. De laptop werd niet te heet: geen throttling geregistreerd, accu op 30,6 C. Geen op hol geslagen proces at de CPU op. Er was niets uitgerold. Het enige ongewone was dat zeventien agent-CLI's leefden in dezelfde repository, wat hier een normale werkdag is.

Dit is wat er werkelijk gebeurde, gemeten in plaats van geraden.

De meting

Een machine van 16 GB, 8 cores, vijfenhalf uur aan, zeventien agents aan het werk:

Wat we gemeten hebbenWaarde
Levende agent-CLI's17
Gelijktijdige tsc --noEmit processen, gezien binnen twee minuten3, daarna 10
Load average37 tot 41
Vrij geheugen / geheugencompressor87 MB / 7,2 GB
Eén typecheck op de desktop-app, verzadigde machine7 min 36 aan kloktijd voor 26 s CPU
Dezelfde typecheck, rustige machine33 s

De beslissende regel is de voorlaatste. Zesentwintig seconden CPU uitgesmeerd over zevenenhalve minuut is tien procent benutting. De typecheck was niet aan het rekenen. Hij wachtte op geheugen.

En een van die processen werd onderweg door het besturingssysteem gedood. Een gedode tsc stopt met een code die niet nul is en een lege uitvoer, wat niet te onderscheiden is van een echte typefout. Dus behalve traag produceerde de machine ook oordelen waar niemand op kon vertrouwen.

Niemand deed iets fout

Dit is het stuk waar je even bij stil moet staan, want dit maakt de storing zo moeilijk te zien aankomen.

Elk van die agents volgde de regel. Elk had TypeScript bewerkt. Elk had de opdracht zijn types te controleren voordat hij de beurt teruggaf. Elk startte tsc --noEmit. Geen enkele zag de anderen. Er bestaat geen gedeeld bord waarop een agent schrijft "ik doe nu het dure ding, wacht even".

Daarna voedt het zichzelf. De typecheck wordt traag omdat de machine verzadigd is. De agent die hem in de gaten houdt concludeert dat hij vastzit. Dus doodt hij hem en start er een nieuwe. Die reflex is op zichzelf juist en rampzalig in een groep, en het is dezelfde familie storingen die we een maand eerder documenteerden toen agents vastgelopen zoekprocessen achterlieten: Process Guard is het vangnet dat vindt wat wel gestart is, hier voorkomen we het starten.

De drie antwoorden die we niet gekozen hebben

Minder agents draaien. Dat halveert het symptoom en houdt de bug. Twee gelijktijdige typechecks op een belaste machine zijn nog altijd trager dan één, en de vloot inkrimpen is het probleem betalen met precies datgene wat het werk snel maakt.

Eén typecheck aan het eind. Verleidelijk, en fout om een reden die niets met prestaties te maken heeft. Een typefout die je tien tickets later vindt is een wees: de agent die hem schreef is gesloten, zijn context is weg, en een mens moet het hele onderwerp heropenen om één regel te herstellen. We wilden de verificatie niet uitstellen.

Incrementele compilatie. Getest en afgevoerd. De winst is twijfelachtig in --noEmit modus, en gelijktijdige processen beschadigen de gedeelde .tsbuildinfo. Het lost de helft van het probleem op door de andere helft erger te maken.

Wat we in plaats daarvan deden: één check, gedeeld

De regel is niet "minder vaak controleren". Het is één typecheck tegelijk, per project, voor iedereen. Een wrapper-script vervangt N controles door één, en beantwoordt drie gevallen:

  1. Er is niets veranderd sinds de vorige run, dus we geven het resultaat daarvan terug.
  2. Er loopt al een run, dus we wachten erop en nemen het resultaat over.
  3. Anders nemen we het slot en zijn we de enige tsc op de machine.

Vanuit het oogpunt van de agent is er niets veranderd: hij typt yarn typecheck, hij krijgt zijn typefouten. Hij wacht ook nooit langer dan voorheen, want een run waar hij achter wacht is een run die eerder begon dan de zijne. De machine betaalt er één in plaats van tien.

Dat is het hele idee. Het interessante is dat de twee mechanismen die het nodig heeft veel kleiner zijn dan je zou verwachten.

Het slot is een map

Geen bestand, geen database, geen daemon. Een map.

try {
  mkdirSync(lockDir);       // gelukt: het slot is van ons
} catch (err) {
  if (err.code === 'EEXIST') { /* iemand anders houdt het vast, wachten */ }
}

mkdir maakt de map aan of faalt met EEXIST, en doet dat atomair op macOS, Windows en Linux, zonder dependency en zonder native call. Een bestand schrijven en dan controleren of het bestaat zouden twee bewerkingen zijn, en twee bewerkingen is precies waar een tweede agent ertussen glipt.

In de map zetten we een owner.json met de pid, de hostnaam en de starttijd. Dat bestand dient voor diagnose en voor het detecteren van een dood slot. Het is nooit het bestand dat uitsluit.

Een dood slot wordt automatisch overgenomen in twee gevallen: het eigenaarsproces is verdwenen (alleen gecontroleerd als de hostnaam klopt, want een pid betekent niets van de ene machine naar de andere), of het slot is ouder dan vijftien minuten.

Een valkuil die ons een bug kostte. Tussen de mkdir en het schrijven van owner.json zit een venster waarin de eigenaar onleesbaar is. Het slot in dat venster dood verklaren betekent het stelen van degene die het net genomen heeft, precies de race die het bestand moet voorkomen. Dus als er geen leesbare eigenaar is, oordelen we op de leeftijd van de map, niet op het ontbrekende bestand.

De vingerafdruk is een datum en een telling

Geval 1 moet weten of er iets veranderd is sinds de vorige run. Het voor de hand liggende antwoord is de bronbestanden hashen. Dat doen we niet.

De vingerafdruk is <recentste mtime>:<aantal bestanden> over de roots die volgen uit het include veld van de tsconfig, plus de tsconfig zelf.

Bij 2.300 bestanden kost elke byte lezen meer dan de controle die je ermee uitspaart. De datum alleen mist een verwijdering. De telling alleen mist een bewerking. Samen dekken ze allebei. Het geaccepteerde vals negatief zijn twee bewerkingen binnen dezelfde milliseconde die de telling identiek laten, en het ergste geval daar is een resultaat uit de cache dat een paar seconden verouderd is, nooit een stille typefout, want de blokkerende verificatie blijft die van de build.

Een geschreven regel was niet genoeg, dus voegden we een hook toe

De instructie stond vanaf dag één in AGENTS.md: nooit tsc rechtstreeks, altijd het gedeelde script. Het was niet genoeg, en het is de moeite waard eerlijk te zijn over waarom.

Je types controleren na een bewerking is een diep ingesleten reflex. Onder druk typt een agent npx tsc --noEmit zonder de instructies te herlezen. En er is maar één agent nodig die de regel overslaat om de opeenstapeling te herscheppen die het slot moet voorkomen. Over een instructie valt te praten. Over een hook niet.

Dus hebben we een PreToolUse hook op de Bash tool gehangen, die een rechtstreekse tsc weigert en het juiste commando noemt in de weigering:

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

De hook leest de tool-aanroep als JSON op stdin, stopt met 2 en de reden op stderr om te weigeren, stopt met 0 om door te laten. Twee details maken het verschil tussen een nuttige hook en een vervelende.

Hij herkent tsc op commandopositie, niet ergens willekeurig in de string. Overal naar die drie letters zoeken zou ook grep -rn tsc AGENTS.md weigeren. Het patroon eist dus tsc aan het begin van een regel of na ;, &&, ||, | of (, eventueel voorafgegaan door een package runner en een pad. Het laat ook tsc --version door: er is geen reden om een informatieve vlag te weigeren.

Hij weigert een tweede ding dat we niet hadden zien aankomen. We zagen een agent achter het slot wachten, na een tijdje besluiten dat het wel dood zou zijn, en de slotmap verwijderen om zichzelf los te maken. Dat start een tweede zwaar proces naast het levende, oftewel de perfecte omzeiling van alles wat het slot beschermt. Dus het verwijderen van de slot- of cachemap wordt ook geweigerd, met de uitleg dat een dood slot vanzelf wordt overgenomen.

Die tweede weigering hadden we vooraf nooit geschreven. Hij komt voort uit kijken naar wat agents werkelijk doen als ze vastzitten, wat een betere bron van vangrails is dan bedenken wat ze zouden kunnen doen.

Waar het ophoudt

De hook is specifiek voor Claude Code. De andere agent-CLI's in de vloot zien alleen de geschreven regel. Dat is een bekend gat en we nemen het voor lief: een vangrail die het grootste deel van de vloot dekt is beter dan geen vangrail, terwijl we wachten op een hook-standaard die elke CLI leest.

Het gedeelde script zelf is neutraal ten opzichte van de provider, want het is gewoon een commando. Elke CLI die yarn typecheck kan draaien profiteert van het slot, of hij er nu toe gedwongen wordt of niet.

Wat je hieruit meeneemt

De typecheck was ons luidruchtigste geval, geen bijzonder geval. Het patroon geldt voor elk commando dat duur is, idempotent binnen een kort venster, en door elke agent om dezelfde goede reden gestart: dependencies installeren, de volledige testsuite draaien, bouwen voor productie, een dev-server op een vaste poort starten.

Drie vragen, in deze volgorde, en je hebt het hele ontwerp:

  1. Kan ik een recent resultaat hergebruiken?
  2. Kan ik aanhaken bij de run die al loopt?
  3. Anders: ben ik degene die hem start, alleen?

Als meerdere agents jouw machine delen, is wat de moeite van het meten waard is niet hoeveel er draaien. Het is hoeveel van hen hetzelfde commando in dezelfde minuut starten. Dat getal is wat je machine werkelijk voelt, en zolang je er niet naar kijkt, geef je de warmte de schuld.

Wil je het bredere beeld van hoe wij meerdere agents op één repository laten draaien zonder dat ze elkaar in de weg lopen, dan staat dat in Coding agents parallel draaien, en het vangnet voor de processen die wel gestart zijn is Process Guard. Zowel het gedeelde script als de hook leven in de repository van AgentsRoom, oftewel precies waar die zeventien agents die middag aan het werk waren.

Download AgentsRoom

Draai al je AI-agenten, op al je projecten, vanuit één enkel venster.

GratisDownload AgentsRoom

Companion-app: houd je agents onderweg in de gaten

Breng je eigen: Claude, Codex, Antigravity CLI of andere AI-provider.

Download de extensie
Chrome Web Store

Stuur bugs en verzoeken direct naar je openbare backlog.

Een glimp van AgentsRoom in actie.

Meerdere projecten
Multi-provider
Meerdere agenten
Live status
Bestandsverschil & commit
Mobiele metgezel
Live voorbeeld
Agententeams
Browserautomatisering
Backlog-gedreven ontwikkeling
Promptbibliotheek
Vaardighedenbibliotheek
Bekijk alle functies

Verder lezen