PunchIran executes three men linked to anti-government protestsThe Jerusalem PostIran executes three men accused of working for Israel, US, taking part in anti-government protestsInquirerIlocos Norte, Ilocos Sur among areas that have recovered from ASF한겨레최근 한 달간 수도권 아파트 매맷값 상승률 10위권에 비규제지역 3곳גיקטייםעל המדף: אמנון שעשוע מחפש רוכשים לבנק ONE ZEROCNN TürkTrump'ın en yakınındaki o isme dikkat! Savaş daha yeni mi başlıyor?ZDF heuteEntdecken Sie das ZDF-NachrichtenstudioThe Sydney Morning HeraldKhawaja doubles down in extraordinary social media videosSportstarIndia U-19 vs Pakistan U-19 LIVE score: Arjun score hundred; Dravid falls for ten, Sehwag dismissed for 22; IND 145/5 (31)Sudinfo« Cette météo aura néanmoins un avantage… » : Luc Trullemans a de bonnes et… de moins bonnes nouvelles ce matin !Het Laatste NieuwsOud-klasgenote omschrijft hem als “stille jongen” die “verontrustende tekeningen” maakte: dit weten we al over dader schietpartij KrakauWirtualna PolskaSąsiadka o sprawcy ataku w Krakowie. "Cichy, wycofany, mało rozmowny"
The Daily Newsstand · Free, Always
Sunday, November 8, 2026

Агент закончил работу, а Telegram недоступен: как я сделал очередь доставки ответов

Translate

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

Эту ситуацию пришлось учесть при разработке agent-hub - приложения на Rust, которое запускает AI-агентов на компьютере и позволяет общаться с ними через темы Telegram-группы. В прошлой статье я рассказывал, как запрос агента превращается в кнопку «Разрешить». С тех пор в хабе появилась очередь доставки финальных ответов.

Казалось бы, достаточно сохранить текст и повторить отправку при ошибке. Но ответ может состоять из нескольких сообщений, Telegram может принять одно из них и не прислать подтверждение, а приложение - перезапуститься между отправкой и записью результата. Расскажу, что именно сохраняется в очереди, как она продолжает работу после сбоя и почему дубликаты все равно возможны.

Задача выполнена, но ответа нет

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

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

Поэтому я разделил получение результата и отправку. Для успешно завершенного хода хаб сохраняет финальный текст и ставит его в очередь до обращения к Telegram. Отдельная задача занимается отправкой и повторами. После перезапуска она читает сохраненное состояние и продолжает доставку без нового вызова модели.

В очередь попадает не вся переписка. Хаб сохраняет последовательные текстовые блоки после последнего отображаемого вызова инструмента. Если таких вызовов не было, сохраняется весь текст хода. Вложения, промежуточные сообщения, вопросы и кнопки разрешений автоматически повторно не доставляются. Ошибка, остановка или ход без текстового ответа не заменяют предыдущий сохраненный результат.

Сначала запись на диск, потом отправка

Результат и запись очереди хранятся вместе в локальном файле topics.results.json. В нем же хаб хранит последний ответ для команды /last. Одна запись содержит исходную группу и тему, ID хода, текст, подготовленные части и прогресс их доставки.

Состояние доставки хранится в такой структуре:

pub struct Pending {
    result: SavedResult,
    chunks: Vec<Chunk>,
    next: usize,
    failures: u8,
    revision: u32,
    due: i64,
    state: State,
}

next указывает на следующую неподтвержденную часть, failures считает неудачные попытки, а due задает время следующей. Поле revision помогает не применить устаревшее действие к уже изменившейся записи. Варианты состояния - ожидание, резервирование на время отправки и остановка доставки.

Хаб сохраняет уже подготовленные части, а не только исходный Markdown. Это важно при обновлении приложения: если после перезапуска изменится преобразование разметки, границы сообщений в уже поставленном в очередь ответе останутся прежними. Иначе сохраненный номер следующей части мог бы указывать уже на другой кусок текста.

В начало каждой части добавляются ID хода и ее номер:

📨 Ход <id> · часть 1/3
...

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

Что происходит при перезапуске

Отправитель очереди работает отдельно от сессии агента. Упрощенно цикл выглядит так:

  1. Найти готовую к отправке запись для текущей группы.

  2. Зарезервировать ее и сохранить это состояние на диске.

  3. Отправить текущую часть в исходную тему.

  4. Сохранить подтверждение или состояние ошибки.

  5. Перейти к следующей части только после сохранения прогресса.

Резервирование действует 60 секунд. Оно нужно на случай, если процесс оборвется во время сетевого запроса: после перезапуска запись не остается занятой навсегда. Сам запрос к Telegram ограничен 20 секундами. Один отправитель посылает не больше одной части в секунду.

Сохраненная группа не меняется вместе с настройками. Если переключить хаб на другую группу, старые ответы не переедут в нее. Когда исходная группа снова станет текущей, ее очередь сможет продолжить работу.

После успешной отправки каждой части хаб записывает новый индекс. Если подтверждена последняя часть, запись удаляется из очереди. Сам последний ответ при этом остается доступен через /last.

Почему возможны дубликаты

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

Похожая ситуация возникает, если Telegram подтвердил прием, но хаб не успел сохранить подтверждение на диск до остановки. Следующая попытка снова начнется с этой части. Поэтому я не стал обещать доставку ровно один раз: повтор может принести дубликат с тем же ID хода и номером части.

хаб отправил часть 2/3
        ↓
Telegram принял сообщение
        ↓
подтверждение потерялось или не сохранилось
        ↓
хаб повторяет часть 2/3

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

Ошибки бывают разными

При временной ошибке отправитель делает паузы между попытками: 5, 15, 30, 60, 120, 300 и 600 секунд. После восьми неудачных попыток подряд доставка останавливается до ручного повтора. Успешно отправленная часть сбрасывает счетчик неудач.

Отдельный случай - RetryAfter, когда Telegram сам указывает, сколько ждать. Эту паузу хаб сохраняет для очереди группы отдельно от конкретного ответа. Ручной повтор, удаление ответа или перезапуск приложения не должны сокращать уже сохраненное время ожидания. Если записать паузу на диск не удалось, она остается в памяти; после перезапуска восстановится только то, что успело сохраниться.

Другая проблема - разметка. Агенты пишут Markdown, а хаб преобразует его в HTML для Telegram. Если Telegram отклонил HTML-часть, хаб пробует отправить ее обычным текстом. Если и обычный текст отклонен, доставка останавливается и ждет ручного вмешательства.

С ошибкой диска сложнее. Если сохранить ответ и запись очереди не получилось, хаб продолжает обычную отправку с предупреждением, но рассчитывать на автоматический повтор уже нельзя. Начатая запись при этом может завершиться позже тайм-аута, поэтому и в этом случае возможен повтор с теми же метками.

Как посмотреть и восстановить ответ

В теме есть две команды для разных ситуаций.

/last показывает последний сохраненный текстовый ответ этой темы. Модель при этом не вызывается. Вместе с текстом хаб показывает время, агента, исходный каталог и ID хода. Это пригодится, например, если после ответа уже сменился проект: сохраненный результат остается привязан к теме и своим исходным метаданным.

/outbox показывает ожидающие доставки ответы текущей темы, их состояние и прогресс. Если автоматические попытки закончились, кнопка «Повторить» продолжит доставку с первой неподтвержденной части. Кнопка «Удалить» уберет запись из очереди, но не удалит ответ из /last. Во время активной отправки удалить запись нельзя; устаревшие кнопки тоже не применяются повторно.

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

Где заканчиваются гарантии

Очередь ограничена, чтобы локальное хранилище не росло бесконечно:

  • до 50 ожидающих ответов и 8 МиБ содержимого очереди;

  • до 7 дней хранения записи очереди;

  • до 64 КиБ UTF-8 на сохраняемый финальный текст;

  • для /last - по одному ответу на тему, до 100 тем и 30 дней хранения.

Если очередь заполнена, старые записи не вытесняются. Новый ответ все еще сохраняется для /last, а его обычная отправка сопровождается предупреждением: автоматического повтора для него не будет. Ответ больше 64 КиБ не сохраняется для восстановления, но хаб продолжает обычную отправку полного текста.

Содержимое хранится локально; на Unix файл создается с правами 0600. В файле лежат ответы агента, поэтому стоит помнить, кто имеет доступ к компьютеру и резервным копиям. Если файл поврежден, хаб не перезаписывает его молча: работа бота продолжается, а операции сохранения и чтения сообщают об ошибке.

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

Как проверял

Для очереди добавлены тесты с подменой Telegram и временным файловым хранилищем. Среди сценариев - восстановление после недоступности сети, перезапуск после частичной отправки, потеря записи подтверждения, RetryAfter, ошибка HTML, заполнение очереди и смена группы. Отдельный тест проверяет, что после перезапуска оставшиеся части доставляются без нового запроса к агенту и без команды /last.

Эти проверки выполнялись на Linux с тестовыми заменами, без реального сбоя Telegram. Внезапное отключение питания отдельно не моделировалось, поэтому здесь речь о проверенном восстановлении после перезапуска процесса, а не о гарантии при любом отказе оборудования.

Итого

Теперь, когда агент заканчивает работу, хаб сначала сохраняет ответ и только потом отправляет его в Telegram. Если связь пропадет или приложение перезапустится, доставка продолжится с первой неподтвержденной части.

Ответ все еще может прийти дважды, очередь может заполниться, а запись на диск - завершиться ошибкой. Поэтому у каждой части есть метка, очередью можно управлять вручную, а последний сохраненный ответ - получить командой /last.

Код открыт под MIT: github.com/aprazdnikov/agent-hub-rs.

Как вы решаете проблему доставки результата в своих инструментах, если задача уже закончилась, а получатель временно недоступен?

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.