messenger/telegram/
June 13, 2026 · View on GitHub
Telegram interface layer (aiogram): handlers, middleware, callback routing, streaming UX, startup lifecycle.
For the Matrix transport equivalent, see matrix.md. For shared messenger protocols and the transport registry, see messenger.md.
Files
messenger/telegram/app.py:TelegramBotclass, handler registration, callback routing, group management commandsmessenger/telegram/startup.py: startup sequence (orchestrator creation, bus wiring, recovery, sentinels)messenger/telegram/callbacks.py: shared selector callback helpers (SelectorResponseediting)messenger/telegram/middleware.py:AuthMiddleware,SequentialMiddleware, queue controls, quick-command bypassmessenger/telegram/message_dispatch.py: shared streaming/non-streaming execution pathsmessenger/telegram/handlers.py: command helper handlers (/new,/stop, generic command path)messenger/telegram/chat_tracker.py: persisted group chat activity (chat_activity.json) for/whereand group auditsmessenger/telegram/topic.py: topic/session key helpers + topic-name cachemessenger/telegram/file_browser.py,messenger/telegram/sender.py,messenger/telegram/media.py,messenger/telegram/welcome.py,messenger/telegram/formatting.py,messenger/telegram/typing.pymessenger/telegram/transport.py: Telegram transport adapter forMessageBus
Command ownership
Bot-level handlers:
/start,/help,/info,/showfiles,/stop,/stop_all,/restart,/new,/reset,/session,/sessions,/tasks,/agent_commands- main-agent only:
/agents,/agent_start,/agent_stop,/agent_restart - hidden but supported:
/where,/leave(not in Telegram command popup)
Immediate middleware-handled command path (pre-lock, no normal dispatch):
/interrupt(and bare-word interrupt triggers likeesc,interrupt)
Orchestrator-routed commands:
/status,/memory,/model,/cron,/diagnose,/upgrade,/sessions,/tasks,/reset
Middleware behavior
AuthMiddleware
- private chats: requires
user_id in allowed_user_ids - groups/supergroups: requires both
group_id in allowed_group_idsuser_id in allowed_user_ids
- optional rejected-group callback feeds
ChatTracker
group_mention_only is not auth. It is applied later as message-content gating.
Media messages in groups also respect group_mention_only:
- when
group_mention_only=false: media is always processed in authorized groups (no mention required) - when
group_mention_only=true: media requires a mention or reply addressing the bot (same gate as text messages)
This is checked in _resolve_text() via is_media_addressed(message, bot_id, bot_username).
SequentialMiddleware
Flow order:
- interrupt/abort checks before lock (
/interrupt,/stop_all,/stop, abort phrases) - quick-command bypass
- dedupe by
chat_id:message_id - queue indicator when lock is busy (
mq:<entry_id>cancel callback) - lock by
SessionKey.lock_key(topic-aware)
Quick commands:
/status,/memory,/cron,/diagnose,/model,/showfiles,/sessions,/tasks,/where,/leave
Queue APIs:
is_busy(chat_id)has_pending(chat_id)cancel_entry(chat_id, entry_id)drain_pending(chat_id)
Topic support
get_session_key(message) returns:
SessionKey(chat_id, topic_id=message_thread_id)for forum topic messagesSessionKey(chat_id, topic_id=None)otherwise
Implications:
- forum topics are fully isolated sessions
- locks are topic-aware
/newand/modeloperate per topic when called inside a topic/new @topicnameresets a specific topic session by cached name lookup
TopicNameCache is seeded from persisted sessions at startup and updated from forum topic create/edit events.
Group management (/where, /leave, audits)
ChatTracker stores activity in ~/.ductor/chat_activity.json.
- join/reject/leave events are persisted for both groups and channels
/whereshows active/rejected/left tracked chats, including channels/leave <group_id>lets an authorized user force leave- startup and periodic (24h) audits auto-leave groups no longer in
allowed_group_idsand channels no longer inallowed_channel_ids - auth hot-reload (
allowed_group_ids) triggers immediate audit task allowed_channel_idsis supported by the bot runtime and audit path, but config hot-reload does not currently emit that field, so channel allowlist changes still need restart in practice
Reply context (build_reply_prompt, prepend_reply_to_media)
When a user uses Telegram's native reply gesture, _resolve_text() prepends the
cited message to the prompt. Both helpers share _cited_reply_text(), which
prefers the user-selected quote fragment (Bot API 7.0+, message.quote) over the
full replied-to body (reply_to_message.text or .caption).
Text replies (build_reply_prompt) wrap both parts with explicit labels:
The user is replying to this quoted message:
> <quoted text>
The user's message:
<user input>
Media replies (prepend_reply_to_media) — where the reply is a voice / photo /
video / ... attachment instead of text — prepend the quote plus an attachment-type
note ahead of the [INCOMING FILE] block, so the agent knows the voicemail/image
is the reply to the quote (and can transcribe it with that context):
The user is replying to this quoted message:
> <quoted text>
Their reply is a voice message (the attached file below).
[INCOMING FILE]
...
Both return the body unchanged when the message is not a reply or the cited message has no text (forum-topic service messages).
Message dispatch (message_dispatch.py)
Non-streaming
run_non_streaming_message():
- typing context
orchestrator.handle_message()send_rich()
Streaming
run_streaming_message():
- stream editor +
StreamCoalescer - optional
ReactionTrackeron the original Telegram message whenscene.status_reaction=true - forward text/tool/system callbacks
- finalize editor
- fallback rules:
- stream fallback or empty stream ->
send_rich(full_text) - otherwise send only extracted files from final text
- stream fallback or empty stream ->
Reaction behavior:
ReactionTrackermaps thinking/system/tool stages to Telegram-safe emoji and dedups consecutive identical states- reactions are best-effort and are always cleared at the end of the turn
- when
scene.status_reaction=falseandscene.seen_reaction=true, the app falls back to the older one-shot seen reaction instead
Callback routing
Special callback namespaces:
mq:*queue cancelupg:*upgrade flowms:*model selectorcrn:*cron selectornsc:*session selectortsc:*task selectorns:*named-session follow-up callbackssf:*/sf!file browser
Selector callbacks use shared helpers in messenger/telegram/callbacks.py and selector response types from orchestrator/selectors/models.py.
Observer and task integration
The bot no longer owns fragmented deliver_* handlers.
Current model:
- startup wires observer outputs to
MessageBusviaorch.wire_observers_to_bus(...) - async inter-agent and task callbacks convert results through
bus/adapters.py MessageBushandles lock/injection/delivery using sharedLockPool
Webhook wake path:
- acquires lock for target chat
- runs orchestrator message flow
- submits final wake result envelope for delivery
Current limitation:
- Telegram startup wires the webhook wake handler
- Matrix startup currently does not, so webhook
wakeis effectively Telegram-only right now
Startup lifecycle (messenger/telegram/startup.py)
Startup performs, in order:
- orchestrator creation
- topic cache seeding and resolver wiring
- restart/upgrade sentinel handling
- observer-to-bus wiring
- startup-kind detection and startup notification policy
- recovery planner actions
- command sync + restart marker watcher
- group audit startup + periodic audit loop
Notification routing during startup is transport-owned:
- startup/restart notices use
notifications.startup_targetswhen configured, otherwise they broadcast to allowed users - upgrade-available notices use
notifications.upgrade_targetswhen configured, otherwise they broadcast normally
File safety
Outbound file sends enforce file_access via files.allowed_roots.resolve_allowed_roots(...).
messenger/telegram/sender.py uses shared MIME/tag helpers from files/.