Dziesięciu agentów uruchomiło ten sam typecheck naraz. Rozwiązaniem okazał się katalog.
Siedemnastu agentów kodujących w jednym checkoucie, dziesięć procesów tsc naraz, load average 37 i 87 MB wolnej pamięci. Dziewięćdziesięciosekundowy typecheck zajął 7 min 36. Oto pomiar, powód, dla którego maszyna wcale nie liczyła, i mała współdzielona blokada, która to naprawiła. Do przeniesienia do dowolnego repozytorium.
7 września nasza maszyna deweloperska przestała porządnie odpowiadać. Nie było awarii ani zawieszenia. Po prostu wszystko trwało dziesięć razy dłużej, łącznie z rzeczami, które nie miały nic wspólnego z kodem.
Oczywiści podejrzani okazali się chybieni. Laptop się nie przegrzewał: żadnego zarejestrowanego throttlingu, bateria na 30,6 C. Żaden oszalały proces nie zjadał procesora. Nic nie zostało wdrożone. Jedyną nietypową rzeczą było to, że siedemnaście CLI agentów żyło w tym samym repozytorium, co u nas jest normalnym dniem pracy.
Oto co działo się naprawdę, zmierzone, a nie zgadnięte.
Pomiar
Maszyna 16 GB, 8 rdzeni, włączona od pięciu i pół godziny, siedemnastu agentów przy pracy:
| Co zmierzyliśmy | Wartość |
|---|---|
| Żywe CLI agentów | 17 |
Równoległe procesy tsc --noEmit, zaobserwowane w ciągu dwóch minut | 3, potem 10 |
| Load average | 37 do 41 |
| Wolna pamięć / kompresor pamięci | 87 MB / 7,2 GB |
| Jeden typecheck aplikacji desktopowej, przeciążona maszyna | 7 min 36 czasu zegarowego na 26 s procesora |
| Ten sam typecheck, spokojna maszyna | 33 s |
Rozstrzygający jest przedostatni wiersz. Dwadzieścia sześć sekund procesora rozłożone na siedem i pół minuty to dziesięć procent wykorzystania. Typecheck nie liczył. Czekał na pamięć.
A jeden z tych procesów został po drodze zabity przez system. Zabity tsc kończy się kodem różnym od zera i pustym wyjściem, czego nie da się odróżnić od prawdziwego błędu typu. Więc oprócz tego, że maszyna była wolna, wydawała jeszcze werdykty, którym nikt nie mógł ufać.
Nikt nie zrobił nic złego
To jest ta część, przy której warto się zatrzymać, bo to ona sprawia, że tę awarię tak trudno przewidzieć.
Każdy z tych agentów przestrzegał reguły. Każdy edytował TypeScript. Każdy miał polecenie sprawdzić swoje typy przed oddaniem sterów. Każdy uruchomił tsc --noEmit. Żaden nie widział pozostałych. Nie istnieje wspólna tablica, na której agent napisałby "robię teraz tę kosztowną rzecz, poczekajcie".
Potem to się samo napędza. Typecheck zwalnia, bo maszyna jest przeciążona. Agent, który go pilnuje, stwierdza, że się zaciął. Więc go zabija i uruchamia kolejny. Ten odruch jest słuszny w pojedynkę i katastrofalny w grupie, a to ta sama rodzina awarii, którą opisaliśmy miesiąc wcześniej, gdy agenci zostawiali po sobie zablokowane procesy wyszukiwania: Process Guard to siatka, która znajduje to, co już wystartowało, a tutaj zapobiegamy samemu startowi.
Trzy odpowiedzi, których nie wybraliśmy
Uruchamiać mniej agentów. To dzieli objaw i zostawia błąd. Dwa równoczesne typechecki na obciążonej maszynie nadal są wolniejsze niż jeden, a zmniejszanie floty to płacenie za problem tym, co daje pracy szybkość.
Jeden typecheck na końcu. Kuszące i błędne z powodu, który nie ma nic wspólnego z wydajnością. Błąd typu znaleziony dziesięć zgłoszeń później jest sierotą: agent, który go napisał, jest zamknięty, jego kontekst przepadł, a człowiek musi otworzyć cały temat od nowa, żeby poprawić jedną linijkę. Nie chcieliśmy odraczać weryfikacji.
Kompilacja przyrostowa. Przetestowana i odrzucona. Zysk jest wątpliwy w trybie --noEmit, a równoległe procesy psują współdzielony .tsbuildinfo. Rozwiązuje połowę problemu, pogarszając drugą połowę.
Co zrobiliśmy zamiast tego: jedno sprawdzenie, współdzielone
Reguła nie brzmi "sprawdzać rzadziej". Brzmi jeden typecheck naraz, na projekt, dla wszystkich. Skrypt opakowujący zastępuje N sprawdzeń jednym i obsługuje trzy przypadki:
- Nic się nie zmieniło od ostatniego przebiegu, więc zwracamy jego wynik.
- Przebieg już trwa, więc czekamy na niego i bierzemy jego wynik.
- W przeciwnym razie bierzemy blokadę i jesteśmy jedynym
tscna maszynie.
Z punktu widzenia agenta nic się nie zmieniło: wpisuje yarn typecheck, dostaje swoje błędy typów. Nigdy też nie czeka dłużej niż wcześniej, bo przebieg, za którym czeka, to przebieg uruchomiony przed jego własnym. Maszyna płaci za jeden zamiast za dziesięć.
To cały pomysł. Ciekawe jest to, że oba potrzebne mu mechanizmy są dużo mniejsze, niż można by przypuszczać.
Blokadą jest katalog
Nie plik, nie baza danych, nie demon. Katalog.
try {
mkdirSync(lockDir); // udało się: blokada jest nasza
} catch (err) {
if (err.code === 'EEXIST') { /* ktoś inny ją trzyma, czekamy */ }
}
mkdir albo tworzy katalog, albo kończy się błędem EEXIST, i robi to atomowo na macOS, Windows i Linuksie, bez zależności i bez wywołania natywnego. Zapisanie pliku, a potem sprawdzenie, czy istnieje, to byłyby dwie operacje, a dwie operacje to dokładnie miejsce, w które wciska się drugi agent.
W katalogu zostawiamy owner.json z pidem, nazwą hosta i godziną startu. Ten plik służy do diagnostyki i do wykrywania martwej blokady. To nigdy nie on wyklucza.
Martwa blokada jest przejmowana automatycznie w dwóch przypadkach: proces właściciela zniknął (sprawdzane tylko wtedy, gdy zgadza się nazwa hosta, bo pid nic nie znaczy między maszynami) albo blokada ma więcej niż piętnaście minut.
Pułapka, która kosztowała nas błąd. Między mkdir a zapisem owner.json istnieje okno, w którym właściciel jest nieczytelny. Uznanie blokady za martwą w tym oknie to odebranie jej temu, kto właśnie ją wziął, czyli dokładnie ten wyścig, któremu plik ma zapobiegać. Dlatego gdy żaden właściciel nie jest czytelny, oceniamy po wieku katalogu, a nie po brakującym pliku.
Odciskiem palca jest data i licznik
Przypadek 1 musi wiedzieć, czy coś się zmieniło od ostatniego przebiegu. Oczywistą odpowiedzią byłoby policzenie skrótu plików źródłowych. Nie robimy tego.
Odcisk palca to <najświeższy mtime>:<liczba plików> po katalogach głównych wyprowadzonych z pola include w tsconfig, plus sam tsconfig.
Przy 2300 plikach przeczytanie każdego bajtu kosztuje więcej niż sprawdzenie, które w ten sposób oszczędzamy. Sama data nie widzi usunięcia. Sam licznik nie widzi edycji. Razem pokrywają jedno i drugie. Przyjęty fałszywy negatyw to dwie zmiany w tej samej milisekundzie, które zostawiają licznik bez zmian, a najgorszy przypadek to wynik z pamięci podręcznej przeterminowany o kilka sekund, nigdy cichy błąd typu, bo blokującą weryfikacją pozostaje ta z builda.
Zapisana reguła nie wystarczyła, więc dodaliśmy hooka
Instrukcja była w AGENTS.md od pierwszego dnia: nigdy tsc bezpośrednio, zawsze współdzielony skrypt. Nie wystarczyła i warto uczciwie powiedzieć dlaczego.
Sprawdzenie typów po edycji to odruch wdrukowany bardzo głęboko. Pod presją agent wpisuje npx tsc --noEmit bez ponownego czytania instrukcji. A wystarczy jeden agent łamiący regułę, żeby odtworzyć kumulację, której blokada ma zapobiegać. O instrukcji da się dyskutować. O hooku nie.
Podpięliśmy więc hooka PreToolUse do narzędzia Bash, który odmawia bezpośredniego tsc i nazywa właściwe polecenie w odmowie:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/scripts/hooks/block-direct-tsc.mjs\"" }
]
}
]
}
}
Hook czyta wywołanie narzędzia jako JSON na stdin, kończy się kodem 2 z powodem na stderr, żeby odmówić, i kodem 0, żeby przepuścić. Dwa szczegóły decydują o różnicy między hookiem użytecznym a uciążliwym.
Rozpoznaje tsc na pozycji polecenia, a nie gdziekolwiek w łańcuchu znaków. Szukanie tych trzech liter wszędzie odrzucałoby też grep -rn tsc AGENTS.md. Wzorzec wymaga więc tsc na początku linii albo po ;, &&, ||, | lub (, ewentualnie poprzedzonego uruchamiaczem pakietów i ścieżką. Przepuszcza też tsc --version: nie ma powodu odmawiać informacyjnej opcji.
Odmawia drugiej rzeczy, której nie przewidzieliśmy. Zobaczyliśmy agenta, który czekał za blokadą, po chwili uznał, że musi być martwa, i skasował katalog blokady, żeby się odblokować. To uruchamia drugi ciężki proces obok żywego, czyli idealne obejście wszystkiego, co blokada chroni. Dlatego kasowanie katalogu blokady albo pamięci podręcznej też jest odrzucane, z wyjaśnieniem, że martwa blokada przejmowana jest sama.
Tej drugiej odmowy nigdy nie napisalibyśmy z wyprzedzeniem. Wzięła się z obserwacji tego, co agenci naprawdę robią, gdy są zablokowani, a to lepsze źródło barierek niż wyobrażanie sobie, co mogliby zrobić.
Gdzie to się kończy
Hook jest specyficzny dla Claude Code. Pozostałe CLI agentów we flocie widzą tylko zapisaną regułę. To znana dziura i bierzemy ją na siebie: barierka obejmująca większość floty jest lepsza niż brak barierki, dopóki czekamy na standard hooków czytany przez każde CLI.
Sam współdzielony skrypt jest neutralny wobec dostawcy, bo to zwykłe polecenie. Każde CLI, które potrafi uruchomić yarn typecheck, korzysta z blokady, niezależnie od tego, czy coś je do tego zmusza.
Co z tego wynieść
Typecheck był naszym najgłośniejszym przypadkiem, a nie przypadkiem szczególnym. Wzorzec dotyczy każdego polecenia kosztownego, idempotentnego w krótkim oknie czasu i uruchamianego przez każdego agenta z tego samego dobrego powodu: instalacja zależności, pełny zestaw testów, build produkcyjny, serwer deweloperski na stałym porcie.
Trzy pytania, w tej kolejności, i masz cały projekt:
- Czy mogę użyć ponownie świeżego wyniku?
- Czy mogę dołączyć do przebiegu, który już trwa?
- A jeśli nie, czy to ja go uruchamiam, sam?
Jeśli kilku agentów dzieli twoją maszynę, mierzyć warto nie to, ilu z nich działa. Warto mierzyć, ilu z nich uruchamia to samo polecenie w tej samej minucie. To ta liczba jest tym, co twoja maszyna naprawdę odczuwa, i dopóki na nią nie spojrzysz, będziesz obwiniać ciepło.
Jeśli chcesz szerszy obraz tego, jak uruchamiamy kilku agentów na jednym repozytorium tak, żeby sobie nie wchodzili w drogę, znajdziesz go w Uruchamianie agentów kodujących równolegle, a siatką bezpieczeństwa dla procesów, które jednak wystartowały, jest Process Guard. Zarówno współdzielony skrypt, jak i hook żyją w repozytorium AgentsRoom, czyli dokładnie tam, gdzie tamtego popołudnia pracowało tych siedemnastu agentów.
Pobierz AgentsRoom
Uruchamiaj wszystkich swoich agentów AI, we wszystkich projektach, z jednego okna.
Aplikacja towarzysząca: monitoruj agentów w podróży
Użyj Claude, Codex, Antigravity CLI lub innego dostawcy AI.
Wysyłaj bugi i prośby bezpośrednio do swojego publicznego backlogu.
Spojrzenie na AgentsRoom w akcji.
Czytaj dalej
Jak skalować agentów AI do kodowania w zespole deweloperskim
Jeden deweloper z agentem kodującym to historia o produktywności. Pięciu deweloperów z dwudziestoma agentami to problem koordynacji. Oto co psuje się jako pierwsze, gdy zespół się rozrasta, oraz konfiguracja, która działa: zobowiązane pliki kontekstowe, jasna własność plików, przegląd według promienia eksplozji i koszty, które naprawdę widać.
Czytaj artykułNajlepsze narzędzia do uruchamiania wielu agentów kodujących w 2026
Conductor, Crystal, Claude Squad, Vibe Kanban, AgentsRoom: szczere porównanie najlepszych narzędzi do równoległego uruchamiania wielu agentów kodujących w 2026.
Czytaj artykułMój trener biegowy AI to repozytorium Git i agent Claude
Kończę trening, zegarek się synchronizuje, a trzy minuty później analiza jest już zapisana w moim repozytorium, tydzień został przestawiony, a mój trener zostawił komentarz pod aktywnością w serwisie Strava. Żadnej napisanej aplikacji, żadnego serwera, żadnego rachunku za tokeny: abonament Claude, AgentsRoom i pliki Markdown. Oto cała konstrukcja, do powtórzenia u siebie.
Czytaj artykuł