Десять агентов запустили одну и ту же проверку типов разом. Решением оказалась папка.
Семнадцать кодовых агентов в одном чекауте, десять процессов 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 проверок одной и отвечает на три случая:
- С прошлого запуска ничего не изменилось, отдаём его результат.
- Запуск уже идёт, ждём его и берём его результат.
- Иначе берём блокировку и остаёмся единственным
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, выигрывает от блокировки, заставляют его или нет.
Что из этого вынести
Проверка типов была нашим самым шумным случаем, а не особенным. Схема подходит любой команде, которая дорога, идемпотентна на коротком окне и запускается каждым агентом по одной и той же хорошей причине: установка зависимостей, полный прогон тестов, сборка для продакшена, дев-сервер на фиксированном порту.
Три вопроса, в этом порядке, и весь замысел у тебя в руках:
- Могу ли я переиспользовать свежий результат?
- Могу ли я присоединиться к уже идущему запуску?
- Иначе, я ли тот, кто запускает его в одиночку?
Если несколько агентов делят твою машину, мерить стоит не то, сколько их работает. А то, сколько из них запускают одну и ту же команду в одну и ту же минуту. Именно это число твоя машина и чувствует, и пока ты на него не посмотришь, ты будешь винить нагрев.
Если хочешь общую картину того, как мы гоняем несколько агентов на одном репозитории, не давая им наступать друг другу на ноги, она в статье Запускать кодовых агентов параллельно, а сетка безопасности для процессов, которые всё-таки стартовали, это Process Guard. Общий скрипт и хук живут оба в репозитории AgentsRoom, то есть там, где в тот день после полудня работали эти семнадцать агентов.
Скачать AgentsRoom
Запускай всех своих ИИ-агентов во всех проектах из одного окна.
Приложение-компаньон: следите за агентами на ходу
Используйте Claude, Codex, Antigravity CLI или другого поставщика AI.
Отправляйте баги и запросы прямо в ваш публичный бэклог.
Взгляд на AgentsRoom в действии.
Читать далее
В сессии Claude Code срабатывает 30 событий хуков. Ответить могут только 3.
Полный список событий хуков в Claude Code: когда срабатывает каждое, какие 15 умеют блокировать и правило stdout, которое молча съедает вывод большинства хуков. Практический справочник, собранный на хуках, работающих в продакшене в тысячах сессий агентов.
Читать статьюМой ИИ-тренер по бегу: Git-репозиторий и агент Claude
Я заканчиваю пробежку, часы синхронизируются, и через три минуты анализ уже записан в мой репозиторий, неделя пересобрана, а тренер оставил комментарий под активностью в Strava. Никакого написанного приложения, никакого сервера, никакого счёта за токены: подписка Claude, AgentsRoom и файлы Markdown. Вот вся сборка целиком, воспроизводимая.
Читать статьюAntigravity CLI держит один вход Google на всю машину. Вот что работает вместо этого.
Почему две подписки Google AI Pro нельзя чередовать в Antigravity CLI, где он на самом деле хранит ваш вход, что переключатели аккаунтов действительно делают с системной связкой ключей, почему семейный тариф не удваивает квоту, и единственный подход, который по-настоящему запускает несколько аккаунтов параллельно.
Читать статью