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


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

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

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

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

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

Первое, что я исключил, — настройки: темы у бота включены[^topics], `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`.

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

В репозитории Hermes к этому моменту уже висело issue #136567[^issue] с описанием ровно нашего сценария, ссылками на 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`:

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

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

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

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

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

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

К этому моменту в репозитории Hermes уже висели несколько PR с почти идентичным решением[^pr], один из них (#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 по результатам работы на сервере.*

[^bug]: [Темы в приватных чатах: первое сообщение новой темы приходит без номера темы](https://bugs.telegram.org/c/66517), трекер ошибок Telegram. В комментариях упомянуты OpenClaw и Hermes.
[^issue]: [NousResearch/hermes-agent#136567](https://github.com/NousResearch/hermes-agent/issues/136567), «Telegram DM topics: opening message of a newly created topic is delivered thread-less».
[^pr]: [NousResearch/hermes-agent#136581](https://github.com/NousResearch/hermes-agent/pull/136581) и [#137348](https://github.com/NousResearch/hermes-agent/pull/137348) — два независимых PR с одним и тем же исправлением; более широкий вариант — [#135959](https://github.com/NousResearch/hermes-agent/pull/135959).
[^copymessage]: [copyMessage](https://core.telegram.org/bots/api#copymessage), Bot API. Копия отправляется от бота, поэтому в теме видно, что вопрос продублирован, а не написан заново.
[^topics]: [BotFather Threads Settings](https://core.telegram.org/bots/features#threads), Bot API: темы в приватном чате включаются на стороне бота.

