Outbound (DurableEvent-driven):
- typing дёргается ДО conv.send (а не только по online-событиям);
рефреш по online — каждые ≥4 с, чтобы не получить 429-спам
(Telegram rate-limit ~250 ms на sendChatAction).
- финальный текст — sendMessage(... ParseMode.HTML) на
DurableEvent.AssistantMessage, без промежуточных editMessage.
- служебные строки ((empty response), ошибки) идут как plain text
(без parseMode=HTML), чтобы 400 can't parse entities не выбил
предупреждение в чат.
Inline markdown → Telegram HTML (MarkdownToTelegram):
- **bold** / __bold__ / *italic* / _italic_ / `code` → <b>/<i>/<code>;
fenced ``` → <pre>; [text](url) → <a href>.
- markdown-таблицы → Unicode-рамка (┌─┬─┐││├─┼─┤└─┴─┘) внутри <pre>
(Telegram Bot API <table> не поддерживает).
- escapeOutsideTags теперь ведёт стек открытых тегов: раньше брал
первый попавшийся закрывающий тег и терял парность (агент вставил
<i> внутри <code> → Telegram 400 can't find end tag для <code>).
- экранирует только вне наших тегов; внутри — уже подготовленный текст.
- сообщения > 4096 режутся по \n вне тегов.
Видимость ошибок:
- kotlin-logging в build.gradle.kts.
- runCatching'и заменены на try/catch + log.error/warn с контекстом
(chatId, convId, htmlLen, превью первых ≤120 символов HTML).
Раньше Telegram 400 на sendMessage с 'can't parse entities' уходил
молча — теперь видно в логе с полным телом запроса.
Тесты: 23 + 12 + 28 = 63 в :integrations-telegram, 132 в :standalone.
:standalone:assemble собирает fatjar.
Новый модуль :integrations-telegram, опциональный «плагин» для standalone:
* TelegramBridgeComponent — Component-имплементация, устанавливается
в agent через agent.install(...) только при заданном
AGENTIK_TELEGRAM_TOKEN. Нет токена — нет polling'а, classpath не
содержит pw.binom.telegram.* (factory возвращает AutoCloseable,
а Component-объект создаётся внутри factory и утекает в
standalone только через agent-api).
* Long-polling getUpdates?timeout=N — не webhook, не нужен публичный URL.
* chatId ↔ ConversationId маппинг в общей SQLite-БД standalone'а
(таблица tg_chat_map); persistent=true переиспользует conv_id,
persistent=false — каждый Telegram-message = свежий temp-диалог.
* Group-чаты и /new стартуют временный диалог, не пишут в мапу.
* Tool-call показывает «⚙️ обрабатываю…» через sendChatAction(typing)
по onlineOutbox.onlineEvents.
* Outbound streaming через editMessageText с guard'ом «message is not
modified»; End с пустым телом → «(empty response)».
* Ошибки conv.send — одной строкой в чат, polling не падает.
* uninstall отменяет scope, polling и stream-джобы корректно
останавливаются.
Конфиг — через env (AppConfig.fromEnv):
* AGENTIK_TELEGRAM_TOKEN (default пусто → модуль не активен)
* AGENTIK_TELEGRAM_POLLING_TIMEOUT (default 30s, ок 25–35)
* AGENTIK_TELEGRAM_PERSISTENT (default true)
Зависимость: наша in-house либа caffeine-mgn/telegramClient
(pw.binom.telegram:telegramClient:1.0.0-SNAPSHOT из mavenLocal).
standalone/Main.kt:
* integrationsScope (SupervisorJob + Dispatchers.Default) создаётся
до server'а; tgClient закрывается shutdown-hook'ом ДО agent.close()
и integrationsScope.cancel() — порядок важен, иначе polling
long-poll не прерывается и JVM висит на выходе.
Тесты (8/8 ✅):
* первый входящий текст → persistent conv + conv.send
* повтор из того же chat → тот же conv
* non-persistent → каждый message = новый temp-conv
* typing-индикатор на каждом сообщении
* outbound streaming → 3 editMessage подряд
* End с пустым телом → draft заменяется на «(empty response)»
* ошибка conv.send → одна строка в чат, polling не падает
* uninstall → polling остановлен, новые push'и не обрабатываются
standalone/README.md — добавлен раздел «Telegram-интеграция» с env-таблицей,
описанием поведения и инструкцией по включению.