Channel plugin · listing tai42/channel-telegramInstall
Permissions
Provides
telegram
Channel — Deliver ask questions to a Telegram chat and bridge the typed reply back.
Bridge a Telegram bot to an agent, or deliver ask questions into a chat. A
message runs an agent turn and the answer comes back in the same chat.
Enable it
manifest.yml
Configuration
TheCHANNEL_TELEGRAM_ env group. CHANNEL_TELEGRAM_REDIS_URL (and the
inherited connection-tuning fields) point the plugin-owned correlation store.
CHANNEL_TELEGRAM_BOT_TOKEN, CHANNEL_TELEGRAM_WEBHOOK_SECRET and
CHANNEL_TELEGRAM_PUBLIC_BASE_URL are required, and so is at least one of
CHANNEL_TELEGRAM_DEFAULT_RECIPIENT and CHANNEL_TELEGRAM_ALLOWED_RECIPIENTS: the
startup hook refuses to boot without them, naming the missing variable.
Webhook registration is automatic
Unlike the other channels, Telegram’s webhook is registered automatically: on startup the plugin callssetWebhook (idempotent) to point Telegram at
{PUBLIC_BASE_URL}/api/channels/telegram/inbound with the secret token. You do
not configure it by hand. A failed setWebhook call, including an unreachable Bot
API, aborts startup.
Create a bridge route
A Telegram route’sour_identity is the bot’s numeric id — the digits before
the : in the bot token. client_address is the numeric chat id, so each chat
holds its own conversation:
Rich messages
A notification’s rich shapes map onto native Bot API sends: tappable options render as an inline keyboard (reply options as callback buttons, link options as url buttons), sections degrade to grouped buttons with each title as a text header (Telegram has no native sectioned list), a media header rides the body as its caption when it fits (else a separate message), a footer is a trailing italic line, file media sends by kind (sendPhoto/sendDocument/sendVideo/sendAudio),
and a location is sendLocation (or sendVenue with a name and address). Telegram
has no vendor templates and no ask-less form notifications, so the channel
declines those capabilities rather than silently dropping them.
A bridged tap on an option with an author-set id carries that id back as
params.reply_id (and a sectioned row’s description as params.reply_description) —
opaque enrichment a tool target can read from the turn’s params.
Entry params are bounded at the door (at most 16 keys, each value at most 512 characters,
the whole set at most 2048 bytes). Because Telegram redelivers a failed webhook, this door
drops rather than refuses: an over-cap value is dropped on its own, an over-bound set is
dropped whole, and the participant’s message still bridges. Every drop is logged at
WARNING (naming the door and the bound, never a value) and counted on
tai_entry_params_dropped_total{door="telegram"}.
Inbound media
A message carrying media is fetched from Telegram and ingested into the platform’s served media. A photo (largest size), document, audio, voice, video, video_note, animation, or sticker has its bytes pulled — viagetFile and the file host — through the ingestion seam, which caps, sniffs, stores,
and mints a served reference. The message then bridges as one turn whose text is the
caption when present, else a non-blank placeholder ([voice message],
[document: <name>], or the bracketed kind), carrying BOTH a typed attachment (the
served MediaItem a tool consumer reads as structured content) and the parity media_*
entry-params: media_kind (image/document/audio/video/sticker), media_id
(the served media reference, not the raw Telegram file_id), media_mime_type (the
sniffed-and-agreed type), media_size (the actual bytes stored), and, where they apply,
media_filename (the sanitised display name), media_voice (a voice note), and
sticker_animated. The placeholder label and media_filename both use the sanitised
name, never the raw vendor filename.
A media message that replies to a pending question answers it just as a text reply
does: the ask is resolved with the caption (or the placeholder) as the answer text and the
media_* parameters, while the typed attachment rides only a fresh-turn bridge (the answer
carries text and parameters). A media reply that cannot be received leaves the ask pending.
If the media cannot be received, the participant gets one generic notice and a platform
event records why — too_large (over the operator’s per-kind cap), unsupported_type
(a disallowed or mismatched type), or could_not_receive (a gone/expired file, a store
outage, or an unfetchable source). A caption on a rejected media is still bridged as a
text-only turn, so the participant’s words are never lost. A transient failure (a
timeout, a 5xx, or a torn read) is not acked, so Telegram redelivers the update — the
update id dedupes a redelivery that later succeeds.
Content Telegram can send that this channel does not map to a media kind — a poll,
dice, venue, contact, location, or game — is not bridged: the
participant gets one generic “unsupported” notice and a platform event records the drop,
so nothing is silently lost.
Message ids and addresses
The conversation bridge records every message this channel sends to a participant. These are the ids and addresses it records:- An inbound message passes its
message_idas the medium message id. resolve_addressreturns the bot’s numeric id and the target as the caller names it, which may be an@username. Telegram resolves the target only when it sends: a completed send reports the numericchat.idfrom the Bot API response, the id an inbound message from that chat carries, and themessage_idof each message sent. A send to an@usernameis therefore recorded in the thread of its numeric chat.
Replies
A message sent as a reply carriesreply_to_message.message_id, the id of the message it
quotes in the same chat. The channel hands that id to the platform beside the message, and
the platform resolves it against the conversation’s own records into a typed reply_to
({message_id, status, text}): a tool target reads it in its payload, an agent target
through start_expr. An inline-keyboard tap is a reply to the message holding the keyboard.
A message posted in a forum topic, whose reply_to_message Telegram fills with the topic’s
first message, is a reply only when the participant replied to another message. A reply to a
message of another chat (external_reply) names no message of this chat, so it quotes none.
When the reply answers a pending question, the asker receives the same reply_to beside the
unchanged answer, and the reply’s entry parameters (a tap’s reply_id / reply_description,
a media reply’s media_*) as its params.
Working-on-it signal
While a message runs its turn, the platform keeps Telegram’s typing action alive: it re-issuessendChatAction "typing" ahead of the action’s ~5 s lifetime
until the reply is sent, so the chat shows the bot typing for the whole wait rather
than for a single burst at receipt. The refresh is bounded by the
CONVERSATIONS_WORKING_SIGNAL_MAX_SECONDS ceiling (0 disables it entirely), with
CONVERSATIONS_WORKING_SIGNAL_REFRESH_MARGIN_SECONDS the margin it refreshes ahead
of each lapse.
The bot token stays out of logs
The Bot API carries the token in every request path (/bot<id>:<secret>/…), and
httpx logs each request line at INFO. Loading the channel registers a token mask
with the kit’s transport-log redaction (tai42_kit.logging.register_transport_redaction),
so every httpx/httpcore record shows /bot<redacted>/… before any handler
formats it.
See also
- Client conversations — the bridge model and delivery outcomes.
- Conversation bridge — every setting and the authorization model.

