- BrainTools - https://www.braintools.ru -
Три утра подряд мой крон отчитывался в логе: “завершено, код 0”. Три утра подряд главная работа оставалась несделанной. Лог при этом был полон жизни: цифры, счетчики, время выполнения. С отчетностью у этой системы было все в порядке, с работой – никак. Мой опыт [1] руководителя проектов заставляет меня погрузиться глубже и задать контрольные вопросы.
Пара слов для тех, кто не в теме. Крон – будильник для программ: в назначенное время он запускает скрипт сам, без человека. Код возврата – число, которым программа на прощание сообщает, как все прошло; ноль принято читать как “все хорошо”. У меня получился сотрудник, который каждое утро приходит вовремя, ставит галочку в табеле и уходит домой.
Я строю процессы разработки и личную автоматизацию с AI-агентами Claude Code и Codex: скрипты по расписанию, зеркала почты и чатов, тесты, отправка писем. Агенты пишут скрипты быстро, скриптов становится много, и каждый где-то докладывает об успехе. Доложить об успехе – самое дешевое, что умеет код. Проверить, был ли успех, стоит дороже, и на этой разнице живет то, что я называю тихим отказом: проверка отвечает одинаково, когда все исправно и когда сломано.
Таких историй у меня набралось на пять повторяющихся форм. У каждой свой контрольный вопрос, и вопросы, замечу, годятся для любого отчета – от лога крона до статуса подрядчика в трекере.
Бытовой аналог – ребенок, который на вопрос “уроки сделал?” отвечает “да” раньше, чем вопрос дозвучал. Ответ есть, он бодрый, и он одинаковый при любом состоянии уроков.
Мой утренний скрипт был устроен по обычной схеме: сделать главное, собрать статистику, отчитаться. Сначала главный шаг падал из-за протухшего токена доступа – цифрового пропуска, по которому программа ходит во внешний сервис. Затем команда claude не находилась в PATH крона: у него свой, урезанный список каталогов, где он ищет программы. Статистика при этом собиралась исправно и печатала настоящие цифры.
Настоящие цифры – самое обидное в этой истории. Лог сломанного прогона выглядел живее исправного: там что-то считалось, суммировалось, завершалось. Чем подробнее отчет, тем убедительнее ложный успех. Это правило у меня даже записано в инструкциях для агентов. Скрипт с этой ловушкой все равно был написан и три дня работал.
Подвела последняя строка. В командной оболочке переменная $? хранит код последней выполненной команды. К моменту итогового отчета последней была статистика, она отработала, и ее успех достался всему заданию. Главный шаг упал выше, и об этом никто не спросил. Лечится тем, что код главного шага запоминается сразу после него:
главная_команда
RC=$?
if [ $RC -ne 0 ]; then
echo "Главный шаг не выполнен: код $RC" >> "$LOG"
fi
# статистика, отчет
exit $RC
Сохраненный код позволяет явно выбрать итоговый статус задания, а ошибку [2] главного шага при этом отдельно записать в лог: без этой записи провал растворится в общем “завершено”. Признак, по которому это ловится глазами при чтении чужого скрипта: между главной командой и итоговым $? стоит хотя бы одна другая команда. Принцип “думай о коде возврата” меня не удержал. Признак “между ними что-то есть” удерживает.
Та же форма в почте. Отправка письма возвращает строку вроде Message ID: .... Она подтверждает, что сервер письмо принял. Что именно он принял, из нее не следует. Однажды получателю ушло письмо недельной давности: скрипт взял тело из файла с переиспользуемым именем, а там лежал прошлый текст. Осмотр краев тут молчит: первая строка, подпись и длина у двух писем одного автора обычно совпадают. Перечитывать надо целиком, и в отчете писать, с каким файлом сверял и что вышло. Отметка “перечитал” без результата сравнения – тот же “код 0”, только в прозе.
Аналог – примерка обуви сидя. Сидя ботинки сидят прекрасно. Жмут они на прогулке, а прогулка в проверку не входила.
Я проверял удаленный вызов: команда уходит по ssh на другую машину и выполняется там. Проверил однословным запросом, получил правильный ответ, успокоился. Любой настоящий запрос из нескольких слов падал. ssh склеивает аргументы в одну строку, а оболочка на той стороне режет ее заново по пробелам. Однословному запросу распадаться было нечему. Я убрал из пробы ровно то свойство, которое ломало боевой сценарий, и проба, разумеется, прошла.
Второй случай смешнее. Скрипт перед отправкой сверял адресата: входит ли имя нужного чата в заголовок открытого окна. Он отрапортовал успех со строкой чат ''. Нужный чат открыт не был. Пустая строка входит в любую строку:
print("" in "Любое другое окно") # True
Скрипт нашел пустое имя в произвольном заголовке и с чистой совестью пошел дальше. След отказа лежал прямо в отчете, две кавычки после слова “чат”, и я прочитал их как успех. Пустое искомое стоит считать отдельным отказом еще до проверки на вхождение.
Есть и совсем тихий вариант: регулярное выражение, то есть шаблон для поиска в тексте, написанное по тому, как данные выглядят в голове автора. В файле они выглядели иначе. Такой шаблон с ошибкой не падает, он просто ничего не находит, а снаружи такой результат неотличим от “искомого нет”. Дважды за одну сессию. Контрольный вопрос здесь простой: какое свойство настоящих данных в пробе отсутствовало. Если не могу сказать, откуда взял границы шаблона, открываю боевой файл и смотрю.
Аналог – проверять, дома ли подросток, по свету в его окне. Свет горит. Обычно это даже совпадает, поэтому такой замер живет годами.
MCP – способ дать агенту внешние инструменты: сервер предлагает набор функций вроде “прочитать почту” или “создать черновик письма”. Однажды агент не нашел у себя создание черновиков Gmail на сервере workspace-mcp и объявил, что такой возможности у сервера нет. Накануне он сам ею пользовался. В истории сессии висели уведомления о переподключении сервера, и версия сложилась сама: связь моргнула, инструменты потерялись. Меня попросили перезапустить сессию. Я перезапустил. Инструмент не появился.
Причина нашлась позже и случайно. Сервер был запущен с урезанным набором инструментов, флагом --tool-tier core, и черновиков в этом наборе нет ни в какой сессии. Статус сервера все это время исправно показывал Connected. Он отвечал на вопрос “жив ли сервер”, а я по нему судил, есть ли функция. Строка запуска – факт, читается одной командой из конфига. Переподключение – гипотеза, и совпадение по времени ее не подтверждает: в длинной сессии дисконнекты совпадут с чем угодно.
Та же форма проявилась в общении с заказчиком. В одном проекте он подолгу молчал в привычном нам канале. В заметках появился риск “заказчик пропадает”, а за ним управленческие выводы про напоминания и дедлайны. Зеркало переписки – локальная копия чата, которую скрипт регулярно докачивает, – в проекте было. В нем лежали реплики самого заказчика о том, что наши сообщения в этом канале остаются незамеченными. Лежали до того, как кто-то задумался. Свидетельство было собрано, сохранено и проигнорировано: причину искали в человеке, труба осталась вне подозрений. Смена канала восстановила общение.
Подвела асимметрия. У меня тот же мессенджер разложен по папкам, и из этой позиции не вообразить, что собеседник в нем тонет. Молчание – наблюдаемый факт, мотивация [3] – гипотеза, и она дороже проверки. Проверка приземленная: спросить там, где он точно читает, дошло ли конкретное сообщение. Общее “видишь мои сообщения?” получит “да” и тогда, когда пропущено именно одно конкретное письмо или вложение.
А 28 сентября та же ошибка нашлась в общении с самым доступным мне получателем. Отчеты кронов приходили в Telegram-Избранное. Они лежали на месте, доставка работала. Уведомлений не было: сообщение самому себе Telegram уведомлением не сопровождает. Отложенное сообщение, наоборот, приходит напоминанием со звуком. Я построил систему оповещений, которая бережно охраняла мою тишину, особенно в те моменты, когда я рассчитывал узнать, что что-то сломалось. “Отчет доставлен” и “я отчет заметил” – два разных слоя, и проверял я первый.
Аналог – два одинаковых будильника на тумбочке. Я неделю настраиваю один, а звонит по утрам второй. Настроен первый безупречно.
Сервис существовал в двух копиях: репозиторий, где я правлю код, и байт-в-байт снимок в домашнем каталоге, развернутый раньше. Точка входа, по которой сервис запускался, вела в снимок. Полчаса правок, 291 зеленый тест, пять исправленных дефектов – и ноль изменений в поведении [4] бота, который звал снимок. При голосовании бот проиграл бы с разгромным счетом. Прав был бот.
Коварство в том, что ошибки нет нигде. Правки корректны, тесты честные, оба факта наблюдаемы, и оба ничего не говорят о работающей системе. Разговор при этом уезжает в “правка не помогла”, хотя правка просто не доехала. Признак формы: тесты зеленые, поведение [5] прежнее. Прежде чем обсуждать, почему не помогло, я смотрю, откуда исполняющийся процесс берет код: куда ведет точка входа, какой модуль загружается. Диагностическую команду при этом надо запускать тем же интерпретатором и в том же окружении, что у работающего сервиса, и вне каталога репозитория, иначе Python найдет модуль в текущем каталоге и покажет ту копию, которую я и так успешно проверял.
Копии бывают и у состояния. Codex из моей оболочки отвечал 401 Missing bearer, а codex login status – Not logged in. Вместе это читалось как потерянный логин, и агент предложил перелогиниться. Обе команды говорили правду, только про пустой домашний каталог, который им достался: переменная с настоящим домом инструмента до неинтерактивной оболочки не доходила. Авторизация была на месте, ее искали в другой квартире. Опровергать вывод пришлось мне.
Та же форма бывает на стороне получателя, с именем вложения в письме. Сначала длинное имя резалось на пронумерованные куски заголовка, которые часть почтовых клиентов не склеивает, и получатель видел вместо имени сырую строку. Потом переход на одну стандартную форму имени сломал отображение в одной версии Apple Mail, при том что веб-почта показывала имя верно. Помогли две формы одного и того же имени в обоих заголовках, с проверкой транспортных байтов письма до отправки. Как имя отобразится у получателя, проверяется только его клиентом, и это остаточный риск.
Аналог – “молоко есть, я вчера покупал”. Покупка настоящая. Про холодильник сейчас она ничего не говорит, особенно если дома живет кто-то еще.
Здесь проверки нет вовсе: ее место заняла память [6] о том, что я сам делал с объектом. Я вел работу двумя агентами на разных машинах, и за один разбор набралось пять таких случаев. Каждый раз утверждение о чужом рабочем дереве или общей странице было верным на момент прошлого взгляда. С тех пор сосед внес правку, а агент отвечал по своей последней картине, потому что повода посмотреть заново не видел. Ни проглоченной ошибки, ни негодного признака, ни второй копии: состояние было наблюдено верно и просто устарело. И каждый раз ошибку замечал собеседник, у которого объект был перед глазами. Совместную работу я включил, последствия совместности учел частично.
От остальных форм эта отличается тем, что лечится повторным взглядом. У крона можно перечитать ложный ноль сколько угодно, у второй копии снова прогнать честные тесты – ничего не изменится. Здесь достаточно открыть объект сейчас. Смотреть нечем – тогда датированное допущение: “в 10:51 было так, с тех пор не смотрел”. Для ответа в разговоре это полноценно. Для необратимого действия это означает “сперва посмотри”.
Общее у этих историй одно: у меня были все основания чувствовать себя спокойно. Логи заполнялись, тесты зеленели, сообщения доставлялись, статус горел Connected. Добавить в такую систему еще один общий статус “все хорошо” ничего не стоит, и код делает это охотнее всего.
Пять вопросов выясняют, за что этот статус отвечает: что попадет в отчет при провале главной работы, какие данные выдержала проба, что измеряет признак, какую копию проверяли, когда последний раз смотрели. Все пять сводятся к одному, который я задаю любой проверке, от крона до собственной уверенности:
Если бы это было сломано прямо сейчас, сказала бы она что-нибудь другое?
Если ответ “нет”, проверка декоративна. Она хуже отсутствия проверки: отсутствие оставляет неуверенность, а такая выдает уверенность в том, чего не проверяла. Ритуал на каждый чих из этого делать незачем. Вопрос нужен в двух местах: когда пишешь проверку и когда собираешься на нее опереться.
Для начала хватит одного своего задания по расписанию. Откройте его итоговый отчет и мысленно сломайте главный шаг. Если отчет от этого не изменится, вы нашли свой крон с кодом 0.
Автор: dewil
Источник [7]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/36252
URLs in this post:
[1] опыт: http://www.braintools.ru/article/6952
[2] ошибку: http://www.braintools.ru/article/4192
[3] мотивация: http://www.braintools.ru/article/9537
[4] поведении: http://www.braintools.ru/article/9372
[5] поведение: http://www.braintools.ru/article/5593
[6] память: http://www.braintools.ru/article/4140
[7] Источник: https://habr.com/ru/articles/1088110/?utm_campaign=1088110&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.