Перейти к основному содержанию

Тема в Telegram есть, а ответа в ней нет

Персональный агент у меня живёт на своём сервере, а интерфейс к нему — Telegram. Не веб-морда, не терминал, а обычная личка с ботом, в которой включены темы. Это оказалось самой удобной связкой: каждое сообщение в новой теме становится отдельной сессией со своим контекстом, историей и инструментами. Тред равен задаче.

Схему я пробовал на двух клиентах — Hermes и OpenClaw. Подключение к Telegram у них устроено одинаково, и одинаково же ломалось одно и то же место. Разбирательство заняло почти день и закончилось плагином на сервере, комментариями в upstream и пониманием, что виноват не агент.

Что именно ломалось

Иногда я пишу боту в ленте «Все сообщения» — Telegram создаёт под это сообщение новую тему, — но ответ приходит не в неё, а в общий чат. Тема остаётся пустой, вопрос висит отдельно. Иногда тот же сценарий отрабатывает правильно, и именно эта непостоянность мешала искать причину.

В журнале Hermes за утро нашлись два таких случая: в 08:33 создалась тема 79143, но вопрос пришёл в сессию без номера треда; в 10:47 — тема 79151 и снова без треда. Сообщение, с которого начался разбор, пришло уже с номером 79155, и с ним всё работало.

Первое, что я исключил, — настройки: темы у бота включены1, getMe возвращает has_topics_enabled: true и allows_users_to_create_topics: true, отдельный режим /topic в Hermes не активирован (в базе нет ни его состояния, ни привязок). То есть дело не в том, что я что-то не включил.

Первый заход: deepseek-v4.1-flash

Начинал я с deepseek/deepseek-v4.1-flash через OpenRouter. Разбор шёл по кругу: проверить настройки бота, потом настройки клиента, потом версию Hermes, потом снова настройки. Ответы были складные и правдоподобные, но каждый следующий шаг не приближал к причине, а проверка очередной гипотезы её опровергала. Признака, по которому можно было бы отличить «Telegram не прислал» от «агент потерял», так и не появилось.

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

Второй заход: gpt-6.1-sol

Следующую попытку я делал на gpt-6.1-sol. Здесь разбор пошёл от кода, а не от настроек, и быстро выяснилось важное: адаптер Telegram в Hermes принимает тему в приватном чате только при двух условиях сразу — есть message_thread_id и выставлен is_topic_message. Если флага нет, номер темы отбрасывается, и сообщение обрабатывается как сообщение общего чата. Проверка на копии функций маршрутизации это подтвердила: 79155 с флагом остаётся темой, 79155 без флага превращается в None.

Дальше нашлась карточка в трекере Telegram2: при отправке из «Все сообщения» клиент создаёт тему, но само сообщение с текстом доставляет боту без номера темы и без признака принадлежности к ней. В комментариях к карточке упомянуты оба клиента — и OpenClaw, и Hermes, — то есть это не особенность конкретного агента.

В репозитории Hermes к этому моменту уже висело issue #1365673 с описанием ровно нашего сценария, ссылками на PR и подробными выкладками от нескольких независимых инсталляций. На этом этапе причина была понятна, но исправления на сервере не было.

Новый DeepSeek Flash

Тем временем вышла новая версия DeepSeek Flash, и я перевёл Hermes с deepseek/deepseek-v4.1-flash через OpenRouter на канонический deepseek-flash у провайдера напрямую. Следующий заход делался уже на нём — и на этот раз разговор начался не с гипотез, а с вопроса «что именно приходит боту».

Ответ нашёлся в сервисных сообщениях. Когда Telegram создаёт тему, бот получает событие forum_topic_created, и в нём есть номер новой темы. А следом приходит текст сообщения, и в нём нет ни message_thread_id, ни is_topic_message, ни reply_to_message:

forum_topic_created   → message_thread_id = 79143   (номер темы известен)
message (текст)       → message_thread_id = отсутствует, is_topic_message = отсутствует

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

  1. Плагин подписывается на forum_topic_created в приватных чатах и запоминает пару «чат → номер новой темы» на 180 секунд. Темы, созданные самим ботом, игнорируются.
  2. Он же оборачивает обработчик входящих сообщений адаптера.
  3. Первое сообщение без номера темы, пришедшее в это окно, копируется в новую тему через copyMessage4 и перенаправляется туда: у события проставляется thread_id, а якорь ответа указывает на копию. Дальше ответ, прогресс инструментов и продолжение диалога идут уже в тему, и каждая такая тема — отдельная сессия.
  4. Всё падает в исходное поведение, если сервисного сообщения не было, окно истекло, копия не удалась или вылетело исключение.

Проверяли по шагам: двенадцать офлайн-тестов на маршрутизацию и установку патча, затем /restart и строки в журнале о том, что обработчик подменён и наблюдатель зарегистрирован. Первая живая проверка оказалась даже не нужна: тестовая тема 79175 создалась, и сообщение пришло с номером темы само.

Это, кстати, ещё один факт из карточки бага: поведение зависит от клиента. Telegram Desktop номер темы присылает, мобильный клиент из «Все сообщения» — нет. Поэтому баг воспроизводится не каждый раз, а «иногда».

Комментарии в upstream

К этому моменту в репозитории Hermes уже висели несколько PR с почти идентичным решением5, один из них (#136596) был закрыт без мержа. Все они восстанавливали номер темы у «пустого» первого сообщения — и в обсуждении оставался открытый вопрос: что делать с якорем ответа, если само сообщение осталось в общей ленте.

Мы этот вопрос не стали обсуждать теоретически, а измерили двумя пробами Bot API на живом боте (сообщения отправили в тему и удалили):

Якорь ответаРезультат
сообщение из общей лентыok: true, сообщение легло в тему, reply_to_message отсутствует — цитата молча выброшена
сообщение внутри темыok: true, reply_to_message.message_id на месте

Вывод: Bot API не отвергает кросс-топиковый якорь и не возвращает ошибку, поэтому до ветки повторной отправки без якоря дело не доходит. Значит исправления, которые восстанавливают только номер темы, по стороне отправки полны, а дополнительный ретрай в более широком #135959 для этого сценария не нужен.

Комментарий с этими измерениями оставили в issue и в #136581. Автор PR ответил через десять минут: с замерами согласился, сказал, что именно этой части не хватало, а расхождение объяснил тем, что жёсткий отказ Bot API — другая форма отказа, которую его тест и закрепляет.

Что осталось нерешённым

Тема всё равно не самодостаточна: Telegram оставляет исходное сообщение в общей ленте, поэтому без копирования в теме будет только ответ, а вопрос — снаружи. Локально это решает copyMessage, но в upstream это отдельное улучшение со своими издержками — дубль сообщения в двух местах и лишний вызов API на каждый такой ход. Автор PR согласился с такой оценкой и оставил свой патч узким.

Ни один из PR пока не смержен, баг на стороне Telegram и зависит от клиента. Плагин на сервере живёт до тех пор, пока исправление не появится в релизе Hermes, и его придётся перепроверять после обновлений: он держится за внутренние точки, которых в чужом коде никто не обещал сохранять.

Что из этого следует

Три вывода, которые мне кажутся полезнее самого исправления.

Первый: интерфейс в мессенджере — это не «просто бот». Темы, лобби, сервисные сообщения, разные клиенты и разные формы одного и того же события — состояний больше, чем кажется, и половина из них не описана в документации.

Второй: без сырых данных расследование вырождается в перебор гипотез. Пока мы смотрели на уже разобранное агентом событие, «Telegram не прислал» и «агент потерял» были неразличимы. Как только появились обе формы сообщения рядом, вопрос закрылся за один шаг.

Третий, менее приятный: сравнивать модели по этому эпизоду не совсем честно. К третьему заходу баг уже был найден и обсуждён в issue, так что «нашёл и сразу починил» — не чистая победа новой версии, а эффект накопленного контекста. Что действительно отличается, так это привычка доводить до проверяемого факта вместо правдоподобного объяснения, и вот она у моделей разная.

Примечание. Статья написана deepseek-flash по результатам работы на сервере.


  1. BotFather Threads Settings , Bot API: темы в приватном чате включаются на стороне бота. ↩︎

  2. Темы в приватных чатах: первое сообщение новой темы приходит без номера темы , трекер ошибок Telegram. В комментариях упомянуты OpenClaw и Hermes. ↩︎

  3. NousResearch/hermes-agent#136567 , «Telegram DM topics: opening message of a newly created topic is delivered thread-less». ↩︎

  4. copyMessage , Bot API. Копия отправляется от бота, поэтому в теме видно, что вопрос продублирован, а не написан заново. ↩︎

  5. NousResearch/hermes-agent#136581 и #137348 — два независимых PR с одним и тем же исправлением; более широкий вариант — #135959 . ↩︎