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 hebben | Waarde |
|---|---|
| Levende agent-CLI's | 17 |
Gelijktijdige tsc --noEmit processen, gezien binnen twee minuten | 3, daarna 10 |
| Load average | 37 tot 41 |
| Vrij geheugen / geheugencompressor | 87 MB / 7,2 GB |
| Eén typecheck op de desktop-app, verzadigde machine | 7 min 36 aan kloktijd voor 26 s CPU |
| Dezelfde typecheck, rustige machine | 33 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:
- Er is niets veranderd sinds de vorige run, dus we geven het resultaat daarvan terug.
- Er loopt al een run, dus we wachten erop en nemen het resultaat over.
- Anders nemen we het slot en zijn we de enige
tscop 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:
- Kan ik een recent resultaat hergebruiken?
- Kan ik aanhaken bij de run die al loopt?
- 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.
Companion-app: houd je agents onderweg in de gaten
Breng je eigen: Claude, Codex, Antigravity CLI of andere AI-provider.
Stuur bugs en verzoeken direct naar je openbare backlog.
Een glimp van AgentsRoom in actie.
Verder lezen
Hoe AI-coderingsagents op te schalen binnen een ontwikkelteam
Eén ontwikkelaar met een coderingsagent is een productiviteitsverhaal. Vijf ontwikkelaars met twintig agents vormen een coördinatieprobleem. Dit is wat er als eerste misgaat wanneer een team opschaalt, en de setup die standhoudt: toegewijde contextbestanden, duidelijke bestandsverantwoordelijkheid, beoordeling op basis van impact en kosten die je daadwerkelijk kunt zien.
Lees het artikel30 hook events gaan af in een Claude Code-sessie. Slechts 3 kunnen terugpraten.
De volledige lijst met hook events van Claude Code, wanneer elk event afgaat, welke 15 kunnen blokkeren, en de stdout-regel die de output van de meeste hooks stilletjes opslokt. Een praktijkreferentie, opgebouwd uit hooks die in productie draaien over duizenden agentsessies.
Lees het artikelMijn AI-hardloopcoach is een Git-repository en een Claude-agent
Ik loop mijn training uit, mijn horloge synchroniseert, en drie minuten later staat de analyse in mijn repository, is de week bijgesteld en heeft mijn coach een reactie onder de Strava-activiteit geplaatst. Geen app gebouwd, geen server geschreven, geen rekening per token: een Claude-abonnement, AgentsRoom en Markdown-bestanden. Hier is de hele bouw, na te maken.
Lees het artikel