Как мы поймали ложную встречу в SalesMachine
Разбираем сбой в SalesMachine: ложное подтверждение, сломанный Google OAuth и починку Telegram-уведомлений.
В SalesMachine мы поймали случай, который хорошо показывает, почему в автоматизации нельзя доверять только одному признаку состояния. Сначала система зафиксировала подтверждённую встречу на 20.07 в 15:00 МСК, потом выяснилось, что это была ложная классификация. Параллельно вскрылись две отдельные поломки: Google Calendar не принимал событие из-за проблем с refresh_token, а Telegram-уведомления падали на мёртвом токене. В итоге мы получили не один баг, а целую цепочку, где каждый следующий слой маскировал предыдущий.
Снаружи это выглядело как обычный рабочий инцидент: есть лид, есть ответ, есть статус booking_ready. Но когда мы пошли по следам, стало видно, что ошибка сидит не в одном месте. Ошибка была в логике разбора письма, в хранении данных о подтверждениях и в двух каналах доставки уведомлений. Для такого случая важнее не «починить всё разом», а аккуратно разделить: что было ложным срабатыванием, что действительно сломалось, и что нам нужно оставить как явный ручной шаг.
Как мы заметили проблему
Сценарий всплыл не из отдельного алерта, а случайно при проверке другой задачи. Это типичная история для небольшого портфеля: баг может долго лежать в системе, пока мы не коснёмся соседнего процесса. В confirmed_bookings.json уже была запись о подтверждённой встрече, а дальше мы увидели, что автоматические каналы уведомления тоже отработали не так, как ожидалось.
Первым красным флагом был Google Calendar. Создание события упало с http_401, а попытка refresh_token тоже завершилась ошибкой HTTP 400 Bad Request. Это важная деталь: речь была не просто о просроченном access-token. Если refresh_token не обновляется, значит проблема глубже — токен отозван, невалиден или сломана сама цепочка OAuth. Файл google_calendar_token.json был датирован 2026-06-05, то есть сама история уже успела пожить отдельно от текущего состояния системы.
Вторым красным флагом оказался Telegram. Уведомление тоже вернуло http_401 Unauthorized. Причина была уже другого класса: TELEGRAM_BOT_TOKEN в SalesMachine/.env оказался мёртвым. То есть у нас одновременно умерли два независимых канала доставки, и это особенно неприятно, потому что по отдельности такие ошибки легко принять за случайные сбои.
Что было на самом деле
Когда мы попросили показать переписку, выяснилось, что встреча на 20.07 вообще не была согласована. Лид ответил на предложение слота словами «нет не подходит». Но наш keyword-fallback классификатор смотрел не только на сам ответ, а на весь текст письма целиком, включая процитированную переписку. А внутри цитаты оставался наш же вопрос с фразой «подойдет 20 июля в 15:00?». Из-за этого в тексте одновременно присутствовали и отрицание, и слова из booking_tokens, и система по ошибке поставила статус booking_ready.
Это хороший пример того, как легко ломается текстовая классификация, если не отделить ответ пользователя от процитированного контекста. По сути, мы учили модель и запасной keyword-слой не на том объекте. Вместо свежего ответа лида мы кормили их смесью нового сообщения и старой переписки. В результате система нашла знакомые слова там, где их не должно было быть.
ROOT CAUSE оказался довольно приземлённым, но важным. В analyze_replies.py мы добавили strip_quoted_reply(), которая обрезает текст по первому вхождению ” > ”. Здесь письма приходят уже со схлопнутыми переносами строк, поэтому обычная проверка по префиксу строки не сработала бы. Мы применили эту очистку и к keyword-fallback, и к промпту Ollama. Заодно добавили «не подходит» в negative_tokens, чтобы явный отказ не конкурировал с фразами о готовности к встрече.
После этого мы проверили реальный текст письма снова. Теперь он стал классифицироваться как not_interested, а не booking_ready. Это было важно не только для этой конкретной записи, но и для всей дальнейшей логики, которая опирается на interest_status.
Что мы исправили в данных и коде
Дальше нам пришлось привести в порядок уже сохранённые данные. По явному подтверждению Олега мы удалили ложную запись из confirmed_bookings.json: bookings снова стали пустыми для этого случая. В replies_real.csv мы поменяли interest_status у этой строки с booking_ready на not_interested, чтобы она не портила статистику дневных отчётов.
Это отдельный урок: если баг уже успел попасть в данные, одной правки кода мало. Старые записи продолжают жить в отчётах, дашбордах и ручных проверках. Поэтому мы всегда смотрим, не надо ли исправить и сам факт, и его следы.
Telegram-часть мы починили сразу в той же сессии. Скрипты analyze_replies.py, send_nurture_followups.py и notify_telegram.py перевели с мёртвого TELEGRAM_BOT_TOKEN и WARROOM_GROUP_ID на рабочий общий WarRoom-бот через OLEHOVAI_TOKEN и OWNER_ID. Логика стала такой же, как уже использует daily_report.py в этом же проекте: прямой чат с Олегом вместо зависания на неработающей групповой конфигурации. Мы проверили это реальной отправкой — тестовое сообщение дошло.
Что мы оставили вручную
Google Calendar OAuth мы не смогли закрыть сами. Когда refresh_token сломан на уровне Google OAuth, агент не может пройти consent самостоятельно. Для этого нужен личный повторный логин Олега в Google. До этого момента любая новая подтверждённая встреча не будет попадать в календарь автоматически. Телеграм-уведомление теперь работает, но календарь остаётся ручной точкой входа.
Здесь важно не делать вид, что всё можно обойти кодом. Если внешний провайдер требует заново подтвердить доступ, это не баг в бизнес-логике, а ограничение интеграции. Мы можем только зафиксировать состояние, сделать временный обходной канал и попросить человека выполнить ручное действие.
Отдельно мы ещё прояснили контекст с двумя копиями SalesMachine: на Timeweb и на Windows. Олег думал, что Timeweb-копия стоит на паузе, но на деле она не была остановлена. Таймеры salesmachine-reply-watch.timer и salesmachine-daily.timer были активны и реально работали. Это тоже полезное напоминание: когда проект живёт в двух местах, очень легко ошибиться в том, какая копия сейчас основная и какие фоновые задачи вообще исполняются.
Если нам нужно было бы вынести из этой истории один практический вывод, он был бы простым: в автоматизации продаж нельзя считать одно письмо, один статус и один канал уведомлений достаточной защитой. Нужны раздельная обработка цитат, явные отрицательные токены, проверка внешних токенов и понятный ручной fallback для критичных интеграций.
Как мы перестали терять лиды и автоматизировали их обработку
Если вам близка такая же связка из лидов, писем, календаря и ручных исключений, посмотрите SalesMachine. Там же видно, как мы собираем эти процессы в одну рабочую схему без лишней магии.
В этой задаче для нас было важно не просто исправить один неправильный статус, а разложить по полкам весь путь от входящего письма до уведомления и календаря. Именно так мы обычно и работаем с портфелем: сначала отделяем ложный сигнал от настоящего, потом чиним источник ошибки, а затем проверяем, что данные и автоматические действия снова совпадают.
Рассылка новых статей
Выберите, куда присылать новые статьи и разборы — без спама, только новые материалы.
Чтобы получать статьи на email, сначала привяжите email в личном кабинете.
Чтобы получать статьи в Telegram, войдите через Telegram-кнопку на странице входа.
Получайте новые статьи и разборы
Подпишитесь через email — без спама, только новые материалы.