Десять агентів запустили ту саму перевірку типів водночас. Розв'язком виявилася тека.

Сімнадцять кодових агентів в одному чекауті, десять процесів tsc паралельно, load average 37 і 87 МБ вільної пам'яті. Перевірка типів на дев'яносто секунд забрала 7 хв 36. Ось виміри, пояснення того, чому машина не рахувала, і маленьке спільне блокування, яке все виправило. Копіюється в будь-який репозиторій.

7 вересня наша робоча машина перестала нормально відповідати. Не падіння, не зависання. Просто все стало забирати вдесятеро більше часу, зокрема й те, що взагалі не пов'язане з кодом.

Очевидні підозрювані всі виявилися ні до чого. Ноутбук не перегрівався: тротлінгу не зафіксовано, батарея 30,6 C. Жоден скажений процес не їв CPU. Нічого не деплоїли. Єдине незвичне: сімнадцять CLI агентів були живі в одному репозиторії, а це в нас звичайний робочий день.

Ось що відбувалося насправді, виміряне, а не вгадане.

Виміри

Машина 16 ГБ, 8 ядер, працює п'ять з половиною годин, сімнадцять агентів у справі:

Що ми вимірялиЗначення
Живих CLI агентів17
Паралельних процесів tsc --noEmit, помічених за дві хвилини3, потім 10
Load averageвід 37 до 41
Вільна пам'ять / компресор пам'яті87 МБ / 7,2 ГБ
Одна перевірка типів десктопного застосунку, машина забита7 хв 36 за годинником на 26 с CPU
Та сама перевірка типів, машина спокійна33 с

Вирішальний рядок передостанній. Двадцять шість секунд CPU, розтягнуті на сім з половиною хвилин, це десять відсотків завантаження. Перевірка типів не рахувала. Вона чекала на пам'ять.

І один із цих процесів система вбила просто на ходу. Убитий tsc завершується з ненульовим кодом і порожнім виводом, що не відрізнити від справжньої помилки типів. Тобто машина не просто гальмувала, вона видавала вердикти, яким ніхто не міг довіряти.

Ніхто не зробив нічого поганого

Ось на чому варто затриматися, бо саме через це таку поломку так важко побачити заздалегідь.

Кожен із цих агентів дотримувався правила. Кожен правив TypeScript. Кожному було сказано перевірити типи перед тим, як віддати роботу. Кожен запустив tsc --noEmit. Жоден не бачив решти. Немає спільної дошки, де агент написав би «я зараз роблю дорогу річ, зачекайте».

Далі воно саме себе підгодовує. Перевірка типів сповільнюється, бо машина забита. Агент, який за нею стежить, вирішує, що вона застрягла. Отже, вбиває її і запускає нову. Цей рефлекс правильний наодинці і катастрофічний у групі, і це та сама родина поломок, що ми описали місяцем раніше, коли агенти лишали по собі залиплі процеси пошуку: Process Guard це сітка, яка знаходить те, що вже запустилося, а тут ми заважаємо запуску.

Три рішення, яких ми не ухвалили

Запускати менше агентів. Це ділить симптом навпіл і лишає баг. Дві одночасні перевірки типів на завантаженій машині все одно повільніші за одну, а скорочувати флот означає платити за проблему тим самим, що робить роботу швидкою.

Одна перевірка типів наприкінці. Спокусливо і хибно з причини, що не має нічого спільного з продуктивністю. Помилка типів, знайдена на десять тікетів пізніше, це сирота: агент, який її написав, закритий, його контекст втрачено, і людині доводиться заново піднімати всю тему заради одного рядка. Ми не хотіли відкладати перевірку.

Інкрементальна компіляція. Спробували і відкинули. Виграш сумнівний у режимі --noEmit, а паралельні процеси псують спільний .tsbuildinfo. Вона розв'язує половину проблеми, погіршуючи другу.

Що ми зробили натомість: одна перевірка на всіх

Правило не «перевіряти рідше». Воно таке: одна перевірка типів за раз, на проєкт, для всіх. Скрипт-обгортка замінює N перевірок однією і відповідає на три випадки:

  1. Від минулого запуску нічого не змінилося, віддаємо його результат.
  2. Запуск уже триває, чекаємо на нього і беремо його результат.
  3. Інакше беремо блокування і лишаємося єдиним tsc на машині.

З погляду агента нічого не змінилося: він набирає yarn typecheck, він отримує свої помилки типів. І чекає він не довше, ніж раніше, бо запуск, за яким він стоїть, стартував раніше за його власний. Машина платить за один замість десяти.

Ось і вся ідея. Цікаво те, що обидва потрібні їй механізми набагато менші, ніж можна подумати.

Блокування це тека

Не файл, не база, не демон. Тека.

try {
  mkdirSync(lockDir);       // вийшло: блокування наше
} catch (err) {
  if (err.code === 'EEXIST') { /* його тримає хтось інший, чекаємо */ }
}

mkdir або створює теку, або падає з EEXIST, і робить це атомарно на macOS, Windows і Linux, без залежностей і без нативних викликів. Записати файл, а потім перевірити, чи він існує, це дві операції, а дві операції це саме те місце, куди вклинюється другий агент.

У теку ми кладемо owner.json з pid, іменем хоста і часом старту. Цей файл потрібен для діагностики і для виявлення мертвого блокування. Виключенням займається ніколи не він.

Мертве блокування перехоплюється автоматично у двох випадках: процес-власник зник (перевіряється лише за збігу імені хоста, бо pid нічого не означає на іншій машині), або блокуванню більше п'ятнадцяти хвилин.

Пастка, що коштувала нам бага. Між mkdir і записом owner.json є вікно, у якому власника не прочитати. Оголосити блокування мертвим у цьому вікні означає вкрасти його в того, хто щойно його взяв, тобто влаштувати саме ті перегони, заради запобігання яким файл і існує. Тому коли читабельного власника немає, ми судимо за віком теки, а не за відсутнім файлом.

Відбиток це дата і кількість

Випадку 1 треба знати, чи змінилося щось від минулого запуску. Очевидна відповідь: хешувати вихідні файли. Ми так не робимо.

Відбиток це <найсвіжіша mtime>:<кількість файлів> по коренях, виведених із поля include у tsconfig, плюс сам tsconfig.

На 2 300 файлах прочитати кожен байт дорожче, ніж перевірка, яку ми заощаджуємо. Сама дата не побачить видалення. Сама кількість не побачить правку. Разом вони закривають обидва випадки. Свідомий хибнонегативний випадок це дві правки в ту саму мілісекунду, які лишають кількість незмінною, і найгірше, що тут буває, це застарілий на кілька секунд результат із кешу, але ніколи не мовчазна помилка типів, бо блокувальна перевірка лишається на збірці.

Написаного правила не вистачило, і ми додали хук

Інструкція була в AGENTS.md з першого дня: ніколи tsc напряму, завжди спільний скрипт. Цього не вистачило, і варто чесно сказати чому.

Перевірити типи після правки це глибоко вкорінений рефлекс. Під тиском агент набирає npx tsc --noEmit, не перечитуючи інструкцій. І достатньо одного агента, який відступив від правила, щоб відтворити ту саму лавину, заради запобігання якій блокування й існує. Інструкція обговорювана. Хук ні.

Тому ми повісили хук PreToolUse на інструмент Bash: він відхиляє прямий tsc і називає правильну команду просто у відмові:

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

Хук читає виклик інструмента як JSON на stdin, виходить із кодом 2 і причиною в stderr, щоб відмовити, і з кодом 0, щоб пропустити. Дві деталі відрізняють корисний хук від набридливого.

Він упізнає tsc у позиції команди, а не будь-де в рядку. Пошук цих трьох літер де завгодно відхилив би і grep -rn tsc AGENTS.md. Тому шаблон вимагає tsc на початку рядка або після ;, &&, ||, | чи (, можливо з запускачем пакетів і шляхом попереду. Він також пропускає tsc --version: немає причин відмовляти інформаційному прапорцю.

Він відхиляє другу річ, якої ми не передбачили. Ми побачили, як агент чекає за блокуванням, за якийсь час вирішує, що воно мертве, і видаляє теку блокування, щоб розблокуватися. Так поруч із живим стартує другий важкий процес, тобто ідеальний обхід усього, що блокування захищає. Тому видалення теки блокування або кешу теж відхиляється, з поясненням, що мертве блокування перехоплюється саме.

Цю другу відмову ми б ніколи не написали заздалегідь. Вона прийшла зі спостереження за тим, що агенти справді роблять, коли застрягли, а це джерело огорож краще, ніж уявляти, що вони могли б зробити.

Де це закінчується

Хук специфічний для Claude Code. Решта CLI агентів у флоті бачать лише написане правило. Це відома діра, і ми її приймаємо: огорожа, що покриває більшу частину флоту, краща за жодну, поки немає стандарту хуків, який читають усі CLI.

Сам спільний скрипт нейтральний щодо постачальника, бо це звичайна команда. Будь-який CLI, здатний запустити yarn typecheck, виграє від блокування, змушують його чи ні.

Що з цього винести

Перевірка типів була нашим найгучнішим випадком, а не особливим. Схема пасує будь-якій команді, яка дорога, ідемпотентна на короткому вікні і запускається кожним агентом з тієї самої доброї причини: встановлення залежностей, повний прогін тестів, збірка для продакшену, дев-сервер на фіксованому порту.

Три питання, у цьому порядку, і весь задум у тебе в руках:

  1. Чи можу я перевикористати свіжий результат?
  2. Чи можу я приєднатися до запуску, що вже триває?
  3. Інакше, чи я той, хто запускає його наодинці?

Якщо кілька агентів ділять твою машину, міряти варто не те, скільки їх працює. А те, скільки з них запускають ту саму команду в ту саму хвилину. Саме це число твоя машина й відчуває, і поки ти на нього не подивишся, ти звинувачуватимеш нагрів.

Якщо хочеш загальну картину того, як ми ганяємо кілька агентів на одному репозиторії, не даючи їм наступати одне одному на ноги, вона у статті Запускати кодових агентів паралельно, а сітка безпеки для процесів, які таки стартували, це Process Guard. Спільний скрипт і хук живуть обидва в репозиторії AgentsRoom, тобто там, де того дня по обіді працювали ці сімнадцять агентів.

Завантажити AgentsRoom

Запускайте всіх своїх AI-агентів на всіх своїх проєктах з одного вікна.

БезкоштовноЗавантажити AgentsRoom

Додаток-компаньйон: контролюйте своїх агентів на ходу

Використовуйте свого: Claude, Codex, Antigravity CLI або іншого AI-провайдера.

Отримати розширення
Chrome Web Store

Надсилайте баги та запити прямо у свій публічний беклог.

Погляд на AgentsRoom в дії.

Кілька проектів
Багато постачальників
Кілька агентів
Статус в реальному часі
Різниця файлів і коміт
Мобільний компаньйон
Попередній перегляд в реальному часі
Команди агентів
Автоматизація браузера
Розробка, орієнтована на беклог
Бібліотека підказок
Бібліотека навичок
Переглянути всі функції

Читати далі