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śmyWartość
Żywe CLI agentów17
Równoległe procesy tsc --noEmit, zaobserwowane w ciągu dwóch minut3, potem 10
Load average37 do 41
Wolna pamięć / kompresor pamięci87 MB / 7,2 GB
Jeden typecheck aplikacji desktopowej, przeciążona maszyna7 min 36 czasu zegarowego na 26 s procesora
Ten sam typecheck, spokojna maszyna33 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:

  1. Nic się nie zmieniło od ostatniego przebiegu, więc zwracamy jego wynik.
  2. Przebieg już trwa, więc czekamy na niego i bierzemy jego wynik.
  3. W przeciwnym razie bierzemy blokadę i jesteśmy jedynym tsc na 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:

  1. Czy mogę użyć ponownie świeżego wyniku?
  2. Czy mogę dołączyć do przebiegu, który już trwa?
  3. 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.

Za darmoPobierz AgentsRoom

Aplikacja towarzysząca: monitoruj agentów w podróży

Użyj Claude, Codex, Antigravity CLI lub innego dostawcy AI.

Zainstaluj rozszerzenie
Chrome Web Store

Wysyłaj bugi i prośby bezpośrednio do swojego publicznego backlogu.

Spojrzenie na AgentsRoom w akcji.

Wiele projektów
Multi-provider
Wielu agentów
Status na żywo
Diff i commit
Aplikacja mobilna
Podgląd na żywo
Zespoły agentów
Testy w przeglądarce
Dev oparta na backlogu
Biblioteka promptów
Biblioteka umiejętności
Zobacz wszystkie funkcje

Czytaj dalej