Операторы знают шлюзы OpenClaw по «счастливым» логам, но продакшен учит словарю кодов выхода. В 2026 году macOS launchd по-прежнему оборачивает рантаймы Node одинаково — хоть на студийном Mac mini, хоть на арендованном в облаке: 137 чаще намекает на давление памяти, 143 часто означает вежливый SIGTERM при перезагрузках, а частые рестарты указывают на ошибки plist, а не на качество модели. Этот гайд сопоставляет коды и сигналы, показывает, как фильтровать unified logging без шума, связывает симптомы с диагностикой doctor для шлюза и объясняет, когда снимать сэмпл в Activity Monitor до расширения параллелизма.
Сочетайте с шаблонами восстановления LaunchAgent для чистых рестартов и с памятью и обрезкой контекста, когда диалоги и транскрипты инструментов растут без границ.
Шпаргалка по кодам выхода
| Код | Типичный смысл | Первые проверки |
|---|---|---|
| 0 | Чистое завершение | Отличить намеренный стоп от watchdog |
| 1 | Общая ошибка Node | Путь stderr; запуск на переднем плане |
| 137 | OOM / SIGKILL | Память, размер stdout инструментов, контекст модели |
| 143 | SIGTERM | Перезагрузка launchd, ручной kill, скрипт деплоя |
Для GUI-приложений macOS показывает Jetsam; демоны оставляют хлебные крошки в unified logging с текстовыми причинами — запомните точное написание bundle id, чтобы сужать предикаты.
Сохраняйте корреляционные идентификаторы запросов в stderr и в отдельном JSONL: когда launchd перезапускает процесс, первые строки нового PID смешиваются со старыми, если вы читаете только хвост файла. Якорь по boot_uuid или счётчику рестарта помогает отличить свежую попытку от «шума» предыдущей жизни процесса.
На Linux systemd интерпретирует сигналы иначе; закрепите внутренний формат «код + сигнал + строка launchd», чтобы не плодить дубликаты между платформенной и прикладной командами.
ThrottleInterval launchd и циклы падений
Если job завершается с ненулевым кодом, launchd применяет откат. При агрессивном KeepAlive и смерти процесса быстрее секунды операторы видят стену одинаковых меток времени, скрывающую первую полезную ошибку. Временно поднимите ThrottleInterval до 10 секунд на время отладки, устраните корень, затем верните более короткое значение для отзывчивости прода.
Зафиксируйте, истинен ли RunAtLoad: ложные срабатывания случаются, когда инженеры днём вручную выгружают агентов, а автоматизация поднимает их через минуты, маскируя реальный триггер.
При смеси системного демона и пользовательского LaunchAgent проверяйте правильный домен (gui/$UID против system) — одинаковые коды могут идти из двух конкурирующих plist.
Читаемые предикаты log show
log show --last 30m --predicate \
'subsystem == "com.apple.xpc.launchd" AND eventMessage CONTAINS[c] "openclaw"'
Ужесточайте фильтр через process == "launchd" и строку label. Экспортируйте JSON для постмортемов, чтобы аудиторы искали через grep без SSH.
Для ночных инцидентов архивируйте log show с явными --start/--end на окно деплоя; режим «last» может отрезать первый вдох, если job перезапускается слишком часто.
Сценарии Console.app
- Создайте избранное «Выходы шлюза», объединяющее подсистему и фильтр по сообщению.
- Запускайте поток до воспроизведения; ставьте на паузу сразу после падения, чтобы не потерять буфер. На удалённом хосте через Screen Sharing держите окно Console на отдельном Space — случайный быстрый клик по очистке стирает полезный контекст.
- sysdiagnose подключайте только при подозрении на ФС или kext — иначе доказательства оставляйте лёгкими.
Общие пресеты фильтров сокращают время до первой полезной строки; версионируйте их в ops-репозитории как код.
Память и веерные вызовы инструментов
Выход 137 часто коррелирует с параллельными вызовами инструментов, каждый из которых буферизует мегабайты stdout. Ограничьте параллелизм тремя на хостах с 8 ГБ унифицированной памяти или уменьшите окна контекста модели ниже пика из Activity Monitor. При включении компрессии памяти задержка растёт раньше жёсткого kill — это ранний индикатор.
Согласуйте политики обрезки с памяткой по памяти: ротируйте транскрипты, ограничивайте глубину JSON и отклоняйте слишком большие вложения на шлюзе, а не позволяйте Node парсить их целиком.
Когда сэмплить процесс Node
Если CPU держит 100 % дольше двух минут без прогресс-логов, снимите сэмпл в Activity Monitor и сохраните рядом с версией plist. Сэмплы показывают узкие циклы в кастомном middleware, которые не видны в stderr.
Гигиена plist: KeepAlive, RunAtLoad
SuccessfulExit внутри KeepAlive включайте только если ноль действительно означает «нездоров». Неверные булевы заставляют launchd рестартить штатные остановки, сжигая CPU-кредиты на облачных Mac. Проверяйте launchctl print gui/$UID/ваш.label и сохраняйте скрин для change management.
Почему Linux CI не воспроизводит
В контейнерах CI нет того же компрессора памяти, жизненного цикла launchd и запросов Keychain. Считайте Linux-тесты линтингом: перед промоушеном шлюза всё равно гоняйте дым на macOS. Аренда Mac mini закрывает разрыв примерно за 16,9 USD в день вместо рассылки ноутбуков.
Дефолтные лимиты дескрипторов тоже отличаются; крупная загрузка может пройти на Linux и упасть на macOS из-за зависших сокетов.
Шаблон постмортема
- Таймлайн от последнего здорового request id до первой строки crash.
- Код выхода, сигнал и текстовая причина launchd.
- Пик RSS и число одновременных инструментов.
- Хэш вывода doctor и diff конфигурации с прошлого деплоя.
- Follow-up: код, plist или мощность.
Ротация stderr и полный диск
Некоторые шлюзы завершаются с 1, когда StandardErrorPath не может дописать из-за заполненного тома. Unified logging крутится, плоские файлы — нет. Следите за свободным местом на /private/var и в каталоге логов; держите минимум 5 ГБ на общих Mac mini, где несколько агентов пишут подробные дампы.
Предпочитайте newsyslog или обёртки в духе logrotate из runbook вместо одного бесконечно растущего файла — разбор хвоста stderr на 12 ГБ под нагрузкой порождает вторичные аварии.
Сигналы в скриптах деплоя
Скрипты blue-green часто шлют SIGTERM, ждут 15 с, затем эскалируют к SIGKILL. Если Node перехватывает SIGTERM, чтобы дренировать HTTP, но вызовы инструментов выходят за окно милосердия, launchd зафиксирует 137, хотя оператор думал о чистом завершении. Удлините окно или срежьте веерные вызовы до смены бинарника.
Эндпоинт /readyz, переходящий в false до SIGTERM, останавливает балансировщики раньше — это снижает принудительные kill сильнее, чем одни только флаги Node.
Добавьте явный таймаут на graceful shutdown в коде шлюза и логируйте, сколько миллисекунд ушло на отмену in-flight задач: если цифра стабильно упирается в лимит деплоя, меняйте скрипт, а не только Node.
Экспортируйте process_start_timestamp_seconds, process_exit_code и rss_bytes_max в Prometheus даже на маленьких инсталляциях. При всплеске кодов эти три ряда покажут, боретесь ли вы с памятью, деплой-churn или хрупкой конфигурацией без ноутбука.
Добавьте счётчик рестартов launchd по label; одновременный рост с HTTP-латентностью чаще указывает на plist, а не на медленную модель.
Телеметрия диска и inode рядом с rss_bytes_max ловит редкие пары «OOM + запись логов», когда процесс пытается сбросить огромный дамп перед убийством и упирается в квоту.
FAQ
Всегда ли 137 — это OOM?
Чаще да на шлюзах; подтверждайте графиками памяти.
Почему 143 после деплоя?
launchd завершил старый процесс при перезагрузке.
Где stderr?
Смотрите StandardErrorPath в LaunchAgent plist.
Когда арендовать Mac mini?
Когда нужны верные launchd и профиль памяти как в проде.
Археология кодов выхода утомительна, но дешевле минут простоя. Физический Mac mini на Apple Silicon воспроизводит backoff launchd, компрессию памяти и дефолты дескрипторов иначе, чем Linux-стейджинг; если шлюз держит локальный кеш моделей и mmap к крупным файлам, профиль RSS на macOS заметно расходится с контейнером — именно поэтому предпрод на Linux часто скрывает выход 137 до первого релиза на «настоящем» macOS. MacHTML сдаёт машины с SSH/VNC, чтобы держать диагностический хост на релизных выходных, снимать потоки Console и выключать всё в понедельник — эластичная мощность без нового CapEx.
Тихое железо также помогает, когда вы зачитываете логи вслух на созвоне с распределённой командой.