The Jerusalem PostIran is renewing progress on its nuclear program, Netanyahu tells UAE President MBZ - reportBollywood HungamaEXCLUSIVE: CBFC orders 45-50 cuts for Dhurandhar The Revenge’s TV Premiere; U/A 16+ version is 10 minutes shorterDaily MaverickPOLITICALLY AWEH: Inside the Gathering 2026 — South Africa’s cities crisis collides with chaotic election preparationPunchKebbi warns civil servants against lateness, absenteeismInquirerMarcos institutionalizes DBM’s Compass for transparent gov’t spendingCNN TürkMezitli’de 65 yaş üstü vatandaşlara göz sağlığı desteğiESPNTransfer rumors, news: USMNT defender Richards to move to Inter?UOLSão Paulo tem alerta de perigo para tempestade e calor de 33°C nesta quarta (30)Daily MailBrooklyn Beckham and Nicola Peltz walk hand in hand as they arrive in the French capital for Paris Fashion Week - just days before estranged mum Victoria's showVilaWebEl primer ministre britànic obre la porta al retorn del Regne Unit a la UEHet Laatste NieuwsStormloop op tickets eerste stadionconcert Clouseau: tweede show aangekondigdIl Fatto Quotidiano“Emanuela Orlandi? Quello che è scritto nei verbali è tutto inventato. Sono 40 anni che sopporto, adesso posso fare come mi pare”: le dichiarazioni choc di Bruno Bosco a Filorosso
The Daily Newsstand · Free, Always
Wednesday, September 30, 2026

Крон три дня писал «код 0» и ничего не делал: пять форм тихого отказа в автоматизации

Translate

Три утра подряд мой крон отчитывался в логе: “завершено, код 0”. Три утра подряд главная работа оставалась несделанной. Лог при этом был полон жизни: цифры, счетчики, время выполнения. С отчетностью у этой системы было все в порядке, с работой - никак. Мой опыт руководителя проектов заставляет меня погрузиться глубже и задать контрольные вопросы.

Пара слов для тех, кто не в теме. Крон - будильник для программ: в назначенное время он запускает скрипт сам, без человека. Код возврата - число, которым программа на прощание сообщает, как все прошло; ноль принято читать как “все хорошо”. У меня получился сотрудник, который каждое утро приходит вовремя, ставит галочку в табеле и уходит домой.

Я строю процессы разработки и личную автоматизацию с AI-агентами Claude Code и Codex: скрипты по расписанию, зеркала почты и чатов, тесты, отправка писем. Агенты пишут скрипты быстро, скриптов становится много, и каждый где-то докладывает об успехе. Доложить об успехе - самое дешевое, что умеет код. Проверить, был ли успех, стоит дороже, и на этой разнице живет то, что я называю тихим отказом: проверка отвечает одинаково, когда все исправно и когда сломано.

Таких историй у меня набралось на пять повторяющихся форм. У каждой свой контрольный вопрос, и вопросы, замечу, годятся для любого отчета - от лога крона до статуса подрядчика в трекере.

1. Успех по умолчанию. Что окажется в отчете, если работа не сделана?

Бытовой аналог - ребенок, который на вопрос “уроки сделал?” отвечает “да” раньше, чем вопрос дозвучал. Ответ есть, он бодрый, и он одинаковый при любом состоянии уроков.

Мой утренний скрипт был устроен по обычной схеме: сделать главное, собрать статистику, отчитаться. Сначала главный шаг падал из-за протухшего токена доступа - цифрового пропуска, по которому программа ходит во внешний сервис. Затем команда claude не находилась в PATH крона: у него свой, урезанный список каталогов, где он ищет программы. Статистика при этом собиралась исправно и печатала настоящие цифры.

Настоящие цифры - самое обидное в этой истории. Лог сломанного прогона выглядел живее исправного: там что-то считалось, суммировалось, завершалось. Чем подробнее отчет, тем убедительнее ложный успех. Это правило у меня даже записано в инструкциях для агентов. Скрипт с этой ловушкой все равно был написан и три дня работал.

Подвела последняя строка. В командной оболочке переменная $? хранит код последней выполненной команды. К моменту итогового отчета последней была статистика, она отработала, и ее успех достался всему заданию. Главный шаг упал выше, и об этом никто не спросил. Лечится тем, что код главного шага запоминается сразу после него:

главная_команда
RC=$?
if [ $RC -ne 0 ]; then
    echo "Главный шаг не выполнен: код $RC" >> "$LOG"
fi
# статистика, отчет
exit $RC

Сохраненный код позволяет явно выбрать итоговый статус задания, а ошибку главного шага при этом отдельно записать в лог: без этой записи провал растворится в общем “завершено”. Признак, по которому это ловится глазами при чтении чужого скрипта: между главной командой и итоговым $? стоит хотя бы одна другая команда. Принцип “думай о коде возврата” меня не удержал. Признак “между ними что-то есть” удерживает.

Та же форма в почте. Отправка письма возвращает строку вроде Message ID: .... Она подтверждает, что сервер письмо принял. Что именно он принял, из нее не следует. Однажды получателю ушло письмо недельной давности: скрипт взял тело из файла с переиспользуемым именем, а там лежал прошлый текст. Осмотр краев тут молчит: первая строка, подпись и длина у двух писем одного автора обычно совпадают. Перечитывать надо целиком, и в отчете писать, с каким файлом сверял и что вышло. Отметка “перечитал” без результата сравнения - тот же “код 0”, только в прозе.

2. Проверка упрощенным входом. На каких данных это проверяли?

Аналог - примерка обуви сидя. Сидя ботинки сидят прекрасно. Жмут они на прогулке, а прогулка в проверку не входила.

Я проверял удаленный вызов: команда уходит по ssh на другую машину и выполняется там. Проверил однословным запросом, получил правильный ответ, успокоился. Любой настоящий запрос из нескольких слов падал. ssh склеивает аргументы в одну строку, а оболочка на той стороне режет ее заново по пробелам. Однословному запросу распадаться было нечему. Я убрал из пробы ровно то свойство, которое ломало боевой сценарий, и проба, разумеется, прошла.

Второй случай смешнее. Скрипт перед отправкой сверял адресата: входит ли имя нужного чата в заголовок открытого окна. Он отрапортовал успех со строкой чат ''. Нужный чат открыт не был. Пустая строка входит в любую строку:

print("" in "Любое другое окно")  # True

Скрипт нашел пустое имя в произвольном заголовке и с чистой совестью пошел дальше. След отказа лежал прямо в отчете, две кавычки после слова “чат”, и я прочитал их как успех. Пустое искомое стоит считать отдельным отказом еще до проверки на вхождение.

Есть и совсем тихий вариант: регулярное выражение, то есть шаблон для поиска в тексте, написанное по тому, как данные выглядят в голове автора. В файле они выглядели иначе. Такой шаблон с ошибкой не падает, он просто ничего не находит, а снаружи такой результат неотличим от “искомого нет”. Дважды за одну сессию. Контрольный вопрос здесь простой: какое свойство настоящих данных в пробе отсутствовало. Если не могу сказать, откуда взял границы шаблона, открываю боевой файл и смотрю.

3. Проверка не того слоя. Что на самом деле измеряет этот признак?

Аналог - проверять, дома ли подросток, по свету в его окне. Свет горит. Обычно это даже совпадает, поэтому такой замер живет годами.

MCP - способ дать агенту внешние инструменты: сервер предлагает набор функций вроде “прочитать почту” или “создать черновик письма”. Однажды агент не нашел у себя создание черновиков Gmail на сервере workspace-mcp и объявил, что такой возможности у сервера нет. Накануне он сам ею пользовался. В истории сессии висели уведомления о переподключении сервера, и версия сложилась сама: связь моргнула, инструменты потерялись. Меня попросили перезапустить сессию. Я перезапустил. Инструмент не появился.

Причина нашлась позже и случайно. Сервер был запущен с урезанным набором инструментов, флагом --tool-tier core, и черновиков в этом наборе нет ни в какой сессии. Статус сервера все это время исправно показывал Connected. Он отвечал на вопрос “жив ли сервер”, а я по нему судил, есть ли функция. Строка запуска - факт, читается одной командой из конфига. Переподключение - гипотеза, и совпадение по времени ее не подтверждает: в длинной сессии дисконнекты совпадут с чем угодно.

Та же форма проявилась в общении с заказчиком. В одном проекте он подолгу молчал в привычном нам канале. В заметках появился риск “заказчик пропадает”, а за ним управленческие выводы про напоминания и дедлайны. Зеркало переписки - локальная копия чата, которую скрипт регулярно докачивает, - в проекте было. В нем лежали реплики самого заказчика о том, что наши сообщения в этом канале остаются незамеченными. Лежали до того, как кто-то задумался. Свидетельство было собрано, сохранено и проигнорировано: причину искали в человеке, труба осталась вне подозрений. Смена канала восстановила общение.

Подвела асимметрия. У меня тот же мессенджер разложен по папкам, и из этой позиции не вообразить, что собеседник в нем тонет. Молчание - наблюдаемый факт, мотивация - гипотеза, и она дороже проверки. Проверка приземленная: спросить там, где он точно читает, дошло ли конкретное сообщение. Общее “видишь мои сообщения?” получит “да” и тогда, когда пропущено именно одно конкретное письмо или вложение.

А 28 сентября та же ошибка нашлась в общении с самым доступным мне получателем. Отчеты кронов приходили в Telegram-Избранное. Они лежали на месте, доставка работала. Уведомлений не было: сообщение самому себе Telegram уведомлением не сопровождает. Отложенное сообщение, наоборот, приходит напоминанием со звуком. Я построил систему оповещений, которая бережно охраняла мою тишину, особенно в те моменты, когда я рассчитывал узнать, что что-то сломалось. “Отчет доставлен” и “я отчет заметил” - два разных слоя, и проверял я первый.

4. Проверка не того экземпляра. Тот ли объект проверяли, который работает?

Аналог - два одинаковых будильника на тумбочке. Я неделю настраиваю один, а звонит по утрам второй. Настроен первый безупречно.

Сервис существовал в двух копиях: репозиторий, где я правлю код, и байт-в-байт снимок в домашнем каталоге, развернутый раньше. Точка входа, по которой сервис запускался, вела в снимок. Полчаса правок, 291 зеленый тест, пять исправленных дефектов - и ноль изменений в поведении бота, который звал снимок. При голосовании бот проиграл бы с разгромным счетом. Прав был бот.

Коварство в том, что ошибки нет нигде. Правки корректны, тесты честные, оба факта наблюдаемы, и оба ничего не говорят о работающей системе. Разговор при этом уезжает в “правка не помогла”, хотя правка просто не доехала. Признак формы: тесты зеленые, поведение прежнее. Прежде чем обсуждать, почему не помогло, я смотрю, откуда исполняющийся процесс берет код: куда ведет точка входа, какой модуль загружается. Диагностическую команду при этом надо запускать тем же интерпретатором и в том же окружении, что у работающего сервиса, и вне каталога репозитория, иначе Python найдет модуль в текущем каталоге и покажет ту копию, которую я и так успешно проверял.

Копии бывают и у состояния. Codex из моей оболочки отвечал 401 Missing bearer, а codex login status - Not logged in. Вместе это читалось как потерянный логин, и агент предложил перелогиниться. Обе команды говорили правду, только про пустой домашний каталог, который им достался: переменная с настоящим домом инструмента до неинтерактивной оболочки не доходила. Авторизация была на месте, ее искали в другой квартире. Опровергать вывод пришлось мне.

Та же форма бывает на стороне получателя, с именем вложения в письме. Сначала длинное имя резалось на пронумерованные куски заголовка, которые часть почтовых клиентов не склеивает, и получатель видел вместо имени сырую строку. Потом переход на одну стандартную форму имени сломал отображение в одной версии Apple Mail, при том что веб-почта показывала имя верно. Помогли две формы одного и того же имени в обоих заголовках, с проверкой транспортных байтов письма до отправки. Как имя отобразится у получателя, проверяется только его клиентом, и это остаточный риск.

5. Состояние по памяти о своем действии. Когда последний раз смотрели своими глазами?

Аналог - “молоко есть, я вчера покупал”. Покупка настоящая. Про холодильник сейчас она ничего не говорит, особенно если дома живет кто-то еще.

Здесь проверки нет вовсе: ее место заняла память о том, что я сам делал с объектом. Я вел работу двумя агентами на разных машинах, и за один разбор набралось пять таких случаев. Каждый раз утверждение о чужом рабочем дереве или общей странице было верным на момент прошлого взгляда. С тех пор сосед внес правку, а агент отвечал по своей последней картине, потому что повода посмотреть заново не видел. Ни проглоченной ошибки, ни негодного признака, ни второй копии: состояние было наблюдено верно и просто устарело. И каждый раз ошибку замечал собеседник, у которого объект был перед глазами. Совместную работу я включил, последствия совместности учел частично.

От остальных форм эта отличается тем, что лечится повторным взглядом. У крона можно перечитать ложный ноль сколько угодно, у второй копии снова прогнать честные тесты - ничего не изменится. Здесь достаточно открыть объект сейчас. Смотреть нечем - тогда датированное допущение: “в 10:51 было так, с тех пор не смотрел”. Для ответа в разговоре это полноценно. Для необратимого действия это означает “сперва посмотри”.

Главный контрольный вопрос

Общее у этих историй одно: у меня были все основания чувствовать себя спокойно. Логи заполнялись, тесты зеленели, сообщения доставлялись, статус горел Connected. Добавить в такую систему еще один общий статус “все хорошо” ничего не стоит, и код делает это охотнее всего.

Пять вопросов выясняют, за что этот статус отвечает: что попадет в отчет при провале главной работы, какие данные выдержала проба, что измеряет признак, какую копию проверяли, когда последний раз смотрели. Все пять сводятся к одному, который я задаю любой проверке, от крона до собственной уверенности:

Если бы это было сломано прямо сейчас, сказала бы она что-нибудь другое?

Если ответ “нет”, проверка декоративна. Она хуже отсутствия проверки: отсутствие оставляет неуверенность, а такая выдает уверенность в том, чего не проверяла. Ритуал на каждый чих из этого делать незачем. Вопрос нужен в двух местах: когда пишешь проверку и когда собираешься на нее опереться.

Для начала хватит одного своего задания по расписанию. Откройте его итоговый отчет и мысленно сломайте главный шаг. Если отчет от этого не изменится, вы нашли свой крон с кодом 0.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.