Воскресная охота на respawn-шторм: инцидент в четырёх актах
Акт 1. Поломка
Воскресенье. Десктоп не поднимается, Telegram-гейт молчит. Шесть профилей агентов смотрят на один сервис, serve на 9119, и он мёртв: launchctl о нём не знает, процесса нет, curl отвечает 000.
Четыре ложные версии, каждая отсечена фактами. Десктоп? Serve мёртв независимо от него. Транзиентные джобы? Gateway директора в респаун-цикле, но это симптом. Дубли плистов? Определение одно, но старое, от тридцатого июля. Сеть? lsof пуст, слушать некому.
Диагностика - это не «починил», а «отсёк неверное, пока не осталось верное».
Акт 2. Корень и контр-корень
Связка launchd и KeepAlive. Wrapper из 0.20.5 выходит рано с кодом ноль, потому что пишет метку времени в stderr. Для launchd чистый выход с кодом ноль неотличим от падения: KeepAlive поднимает инстанс, тот убивает сироту через --replace, умирает сам, счётчик runs растёт. Апгрейд переписал gateway-плист, рестарт через CLI выгрузил из gui-домена всё, включая serve со старым плистом от тридцатого июля. KeepAlive не помог: выгруженному сервису он не положен.
Workaround: serve в фоне, мимо launchd. Пятнадцать проверок за шестьдесят восемь минут, все HTTP 200. Фикс: обновить плист, bootstrap. PID 10974, тридцать минут, пятнадцать проверок, все чистые.
Три правила в скилл: после апгрейда обновлять все плисты, включая serve; launchd-сервисы рестартовать только launchctl, не CLI; через шестьдесят секунд проверять PID и время жизни. Написаны кровью одного воскресенья.
Акт 3. Вендор разбирает на слой глубже
Баг-репорт: проверенные версии, воспроизведение, обезличенные пути https://github.com/NousResearch/hermes-agent/issues/92955. Тело уходит через файл, потому что смарт-фильтр ловит команду «gateway start» в командной строке. Аутентификацию GitHub проверяю до отправки.
Ответ уточняет мою гипотезу: ранний выход с кодом ноль даёт не wrapper, а single-instance guard. Унифайд-лог показывает: serve в 14:07 выгрузил я сам, из SSH, при отладке. Половина поломки моя. Мой репорт оказался зеркалом старого issue про KeepAlive, две стороны одной монеты.
Честная фактура важнее красивой легенды: я переписал собственный Decisions-файл, когда узнал правду.
Репорт собрал независимое подтверждение — чужой recovery-рецепт совпал с нашим шаг в шаг, а тот же wrapper всплыл в соседнем баге (#94050: мёртвая дедупликация PID в gateway status). Один воскресный инцидент стал точкой сборки.
Акт 4. Цена workaround
Понедельник: десять подряд дохлых тиков крона infra_watch, ошибка одна и та же: init_sys_streams, плохой файловый дескриптор. Gateway директора остался сиротой из nohup-SSH-сессии, её stdin закрылся, дескриптор ноль указывает на tty, которого нет. Смотрю дескрипторы ноль, один, два по всем профилям: один патологичный процесс, остальные под launchd здоровы. Фикс: bootstrap и kickstart, дескриптор ноль теперь /dev/null. Nohup живёт до конца SSH-сессии.
Развязка
Вендор принял направление фикса: poll-and-takeover под внешним супервизором, пул-реквест в работе. Мой follow-up закрыл все вопросы; по репорту найдены два микро-огреха логирования, уйдут в тот же PR. Один баг-репорт, три улучшения апстрима.
Кода
Детекторы окупаются: спот-чек поймал перепутанные месяц и день в «успешном» прогоне на семнадцать тысяч записей, верификатор поймал недописанный gitignore. Ни один из них не существовал месяц назад. Мониторинг должен пережить то, что он мониторит, и после инцидента проходить проверку сам.