Commands, package names, and image names on this page come from the open-source project that Mibyan Desktop is built on, and can differ from the Mibyan Desktop installer. For the supported Mibyan install and update path, see Install and update.
Quick setup (dashboard and desktop app)
The Messaging → Telegram page in the dashboard and the desktop app has a Create with QR button. Scan the code (or open the link) in Telegram; Mibyan creates the bot for you, detects your Telegram user ID, writesTELEGRAM_BOT_TOKEN and TELEGRAM_ALLOWED_USERS into your profile’s .env, and restarts the gateway. If you prefer to create the bot yourself, follow the manual steps below.
Step 1: Create a Bot via BotFather
Every Telegram bot requires an API token issued by @BotFather, Telegram’s official bot management tool.- Open Telegram and search for @BotFather, or visit t.me/BotFather
- Send
/newbot - Choose a display name (e.g., “Mibyan”) — this can be anything
- Choose a username — this must be unique and end in
bot(e.g.,my_mibyan_bot) - BotFather replies with your API token. It looks like this:
Step 2: Customize Your Bot (Optional)
These BotFather commands improve the user experience. Message @BotFather and use:Online/Offline status indicator (Optional)
Telegram bots have no real online/offline presence dot — that green dot is a user-account feature, not something the Bot API exposes for bots. The closest surface is the bot’s short description (the line shown under its name in the bot’s profile). Enablestatus_indicator and Mibyan sets that short description to Online
when the gateway connects and Offline on a clean shutdown:
- The short description is global to the bot (visible to all users), not per-chat. Users see it on the bot’s profile page, not as a live badge inside an open chat.
- Only a clean gateway shutdown (
/stop,disconnect) writes “Offline”. A hard crash leaves the last-known status — the inherent limitation of a profile-text indicator. - Off by default, since it mutates the bot’s global profile.
Cold-boot pending queue (Optional)
By default the adapter drops server-side pending updates on a cold boot (drop_pending_updates=True on the first start_polling). That fits
always-on servers: a restart means “clean up,” and the queue is treated as
stale. It does not fit hosts that turn off (a desktop shut down overnight):
messages sent while the gateway is offline sit in Telegram’s Bot API queue,
and the next boot discards them before Mibyan ever sees them — silently, no
log, no retry.
Set drop_pending_on_cold_boot: false to receive that backlog in order on
startup instead:
- Default is
true: existing behavior is unchanged unless you opt in. - Watcher reconnects (brief network outages with the process still alive) always preserve the queue regardless of this setting.
- Conflict recovery still drops pending updates to terminate the competing
getUpdatessession — that path is unrelated to this knob. - After a crash, a preserved queue can redeliver an update the crashed instance partially processed. Telegram’s offset usually prevents this, but time-sensitive commands sent during a long outage will run on boot.
Repeated inbound updates
Mibyan suppresses repeated Telegramupdate_id values before message batching,
command/media handling, observed group-history writes and plugin observers.
The receiving adapter and numeric bot ID scope this check; it does not deduplicate
by text or message_id. A genuine edit with a new update ID can still be processed.
This is bounded, in-memory protection, not an exactly-once guarantee:
- The adapter remembers the most recent 4096 completed admissions, with no time expiry. Active updates stay claimed until dispatch and its scheduled PTB handler tasks finish, including nonblocking native plugins and registered error callbacks.
- Reconnecting the same adapter retains that history. Eviction, adapter replacement or a process restart can allow an old update through again. Nothing is written to a replay ledger on disk.
- Failed or cancelled preparation releases its claim if nothing has been handed off. Once an update enters a batch/hold queue, gateway dispatch, an observer or a native plugin, a later error does not reopen it. Native plugins own their own partial effects, so entering their update or registered error callback is conservatively treated as handoff. PTB’s own exception logging is not a handoff. Uncached static-sticker vision analysis is also a handoff: cancelling the await cannot undo an auxiliary model request already submitted. Caught preparation errors before any handoff remain retryable; an intentional refusal is terminal.
- Releasing a claim only permits a later delivery; it does not request one from Telegram. Polling acknowledgement is independent of agent completion. This check does not retry failed replies or prevent a downstream component from independently duplicating work.
update_id, update kind, message_id and actual receive time. An edit can reuse
message_id, and the message’s sent timestamp is not its receive time.
Command menu priority and cap (Optional)
Mibyan registers its command menu automatically when the Telegram gateway starts. The menu is built from the central slash-command registry plus eligible plugin/skill commands, then capped so Telegram accepts the payload reliably. The default cap is 60 commands — enough to keep all built-in commands plus common skill commands visible. If you have skill, plugin, or built-in commands that should stay visible in Telegram’s/ picker, prioritize them in ~/.mibyan/config.yaml:
priority_mode controls how your list combines with Mibyan’ built-in priority list:
prepend: put your commands first, then Mibyan defaultsappend: keep Mibyan defaults first, then your commandsreplace: use only your list for priority ordering
priority.
Telegram allows up to 100 BotCommands, but large command payloads can fail. Mibyan defaults to 60 for reliability and clamps configured values to 1..100; use /commands for the full command list.
Inline command picker: search every command (no cap)
The/ menu is capped, but Telegram’s inline mode is not. Once enabled, type @yourbotname followed by a search term in any chat to get a live, searchable picker over every Mibyan command and installed skill — results are computed per keystroke and paginated, so nothing is ever trimmed:
/setinline (pick your bot, set any placeholder text, e.g. Search commands and skills...). Until then, Telegram never delivers inline queries and the picker stays inert.
Results are only served to users who pass your gateway allowlist — unauthorized users get an empty list, so your installed skill catalog is not exposed to strangers (inline queries can be sent from any chat, even ones the bot is not in).
Step 3: Privacy Mode (Critical for Groups)
Telegram bots have a privacy mode that is enabled by default. This is the single most common source of confusion when using bots in groups. With privacy mode ON, your bot can only see:- Messages that start with a
/command - Replies directly to the bot’s own messages
- Service messages (member joins/leaves, pinned messages, etc.)
- Messages in channels where the bot is an admin
How to disable privacy mode
- Message @BotFather
- Send
/mybots - Select your bot
- Go to Bot Settings → Group Privacy → Turn off
Observe group chatter without auto-replying
For OpenClaw/Yuanbao-style group behavior, configure Telegram so the bot can see ordinary group messages but only responds when directly triggered:allowed_chats gates where the bot responds; group_allowed_chats authorizes the shared group session used for observed context, so use the same chat IDs for this mode. A later @botname mention, reply to the bot, or configured mention pattern in that same allowlisted chat/topic can use that observed context. The triggered message is also tagged with [nickname|user_id] and gets a per-turn safety prompt so the model treats prior observed lines as context, not instructions addressed to the bot.
Equivalent environment variable:
Step 4: Find Your User ID
Mibyan uses numeric Telegram user IDs to control access. Your user ID is not your username — it’s a number like123456789.
Method 1 (recommended): Message @userinfobot — it instantly replies with your user ID.
Method 2: Message @get_id_bot — another reliable option.
Save this number; you’ll need it for the next step.
Step 5: Configure Mibyan
Option A: Interactive Setup (Recommended)
Option B: Manual Configuration
Add the following to~/.mibyan/.env:
Start the Gateway
Sending Generated Files from Docker-backed Terminals
If your terminal backend isdocker, keep in mind that Telegram attachments are
sent by the gateway process, not from inside the container. That means the
final MEDIA:/... path must be readable on the host where the gateway is
running.
Common pitfall:
- the agent writes a file inside Docker to
/workspace/report.txt - the model emits
MEDIA:/workspace/report.txt - Telegram delivery fails because
/workspace/report.txtonly exists inside the container, not on the host
- write files inside Docker to
/output/... - emit the host-visible path in
MEDIA:, for example:MEDIA:/home/user/.mibyan/cache/documents/report.txt
docker_volumes: section, add the new mount to the same
list. YAML duplicate keys silently override earlier ones.
Supported MEDIA: file extensions
The gateway extracts MEDIA:/path/to/file tags from agent replies and ships the referenced file as a platform-native attachment. Supported extensions across all gateway platforms:
Anything on this list is delivered as a native attachment on platforms that support it (Telegram, Discord, Signal, Slack, WhatsApp, Feishu, Matrix, etc.); on platforms without native support it falls back to a link or plain-text indicator. The bold categories were added in the last few releases — if you were relying on the model saying
here is the file: /path/to/report.docx instead, swap to MEDIA:/path/to/report.docx for native delivery.
Webhook Mode
By default, Mibyan connects to Telegram using long polling — the gateway makes outbound requests to Telegram’s servers to fetch new updates. This works well for local and always-on deployments. For cloud deployments (Fly.io, Railway, Render, etc.), webhook mode is more cost-effective. These platforms can auto-wake suspended machines on inbound HTTP traffic, but not on outbound connections. Since polling is outbound, a polling bot can never sleep. Webhook mode flips the direction — Telegram pushes updates to your bot’s HTTPS URL, enabling sleep-when-idle deployments.Configuration
Add the following to~/.mibyan/.env:
When
TELEGRAM_WEBHOOK_URL is set, the gateway starts an HTTP webhook server instead of polling. When unset, polling mode is used — no behavior change from previous versions.
Cloud deployment example (Fly.io)
- Add the env vars to your Fly.io app secrets:
- Expose the webhook port in your
fly.toml:
- Deploy:
[telegram] Connected to Telegram (webhook mode).
Proxy Support
If Telegram’s API is blocked or you need to route traffic through a proxy, set a Telegram-specific proxy URL. This takes priority over the genericHTTPS_PROXY / HTTP_PROXY env vars.
Option 1: config.yaml (recommended)
http://, https://, socks5://.
The proxy applies to both the main Telegram connection and the fallback IP transport. If no Telegram-specific proxy is set, the gateway falls back to HTTPS_PROXY / HTTP_PROXY / ALL_PROXY (or macOS system proxy auto-detection).
If the fallback IP discovery path is unhealthy on your host, set mibyan_TELEGRAM_DISABLE_FALLBACK_IPS=true to keep cold connect on the plain api.telegram.org path. You can also bound DNS-over-HTTPS fallback discovery with mibyan_TELEGRAM_FALLBACK_DISCOVERY_TIMEOUT in seconds; the default is 5.
Home Channel
Use the/sethome command in any Telegram chat (DM or group) to designate it as the home channel. Scheduled tasks (cron jobs) deliver their results to this channel.
You can also set it manually in ~/.mibyan/.env:
Cron deliveries in topic mode
If you have topic mode enabled in your bot DM, cron messages delivered to the root chat land in the system-only lobby — replying there opens no session and you see the “main chat is reserved for system commands” notice. Create a dedicated forum topic (e.g.Cron) and set:
TELEGRAM_CRON_THREAD_ID overrides TELEGRAM_HOME_CHANNEL_THREAD_ID for cron deliveries only. Replies in that topic continue the topic’s existing session.
Voice Messages
Incoming Voice (Speech-to-Text)
Voice messages you send on Telegram are automatically transcribed by Mibyan’s configured STT provider and injected as text into the conversation.localusesfaster-whisperon the machine running Mibyan — no API key requiredgroquses Groq Whisper and requiresGROQ_API_KEYopenaiuses OpenAI Whisper and requiresVOICE_TOOLS_OPENAI_KEY
Skipping STT: pass the raw audio file to the agent
If you’d rather have the agent itself handle audio — for diarization, a custom transcription tool, or just archiving the recording — setstt.enabled: false in ~/.mibyan/config.yaml:
.ogg for voice notes, .mp3/.m4a/etc. for audio attachments).
This pairs naturally with the local Bot API server section below, which lifts Telegram’s 20MB getFile ceiling to 2GB — useful when the recordings you want to process are longer than a couple of minutes.
Outgoing Voice (Text-to-Speech)
When the agent generates audio via TTS, it’s delivered as native Telegram voice bubbles — the round, inline-playable kind.- OpenAI and ElevenLabs produce Opus natively — no extra setup needed
- Edge TTS (the default free provider) outputs MP3 and requires ffmpeg to convert to Opus:
config.yaml under the tts.provider key.
Large Files (>20MB) via Local Bot API Server
Telegram’s public Bot API capsgetFile downloads at 20 MB, so any voice note, audio file, video, or document larger than that is silently rejected by Mibyan with a “too large” reply. The documented way around this is to run a local telegram-bot-api daemon — the same server software Telegram uses, but running on your network. A local server raises the file ceiling to 2 GB and Mibyan auto-lifts its own internal cap when it sees a custom base_url configured.
This unlocks workflows like:
- Sending long voice memos (45-minute meetings, podcasts) to the bot
- Uploading large videos for vision-tool processing
- Archiving raw audio for offline pipelines like diarization, alignment, or training data
Step 1: Obtain Telegram API credentials
The local server talks directly to Telegram’s MTProto layer (not the public Bot API), so it needs MTProto credentials:- Visit my.telegram.org/apps and sign in with your Telegram account.
- Create a new application (any name and short description will do).
- Copy the
api_idandapi_hash— both are required.
Step 2: Run the telegram-bot-api server
The community-maintainedaiogram/telegram-bot-api Docker image is the easiest path. A minimal docker-compose.yaml (use --local mode to enable the higher limits):
Step 3: Log the bot out of the public API (one-time)
A bot can only be active on one Bot API server at a time. If your bot was already running againstapi.telegram.org (which it almost certainly was), you must explicitly log it out there before the local server will accept it:
logOut through the new server instead.
Verify the local server can talk to Telegram on the bot’s behalf:
Step 4: Point Mibyan at the local server
Add the URLs underplatforms.telegram.extra in ~/.mibyan/config.yaml:
base_url is set, Mibyan:
- Builds the python-telegram-bot client against the local server
- Auto-lifts its internal document/audio size cap from 20 MB → 2 GB
- Reports the active limit in the “too large” error message (
Maximum: 2048 MB.) so it’s obvious which mode you’re in
Step 5: local_mode — file access on disk
The local server has two ways to deliver files:
- Without
--local(the default): files are served over HTTP at/file/bot<TOKEN>/<path>, same as the public Bot API. The 20MB ceiling stays in effect. Useful as a network-fix only (e.g. whenapi.telegram.orgis unreachable but you can self-host); not what you want for the size lift. - With
--local(set viaTELEGRAM_LOCAL=1above): files are written to the server’s filesystem and thegetFileresponse returns an absolute path instead of an HTTP URL. The 20MB ceiling is lifted. Mibyan must then read the bytes from disk, not over HTTP.
local_mode: true in the config above and make sure the Mibyan process can read the path the server returns. Two scenarios:
- Same machine — telegram-bot-api and Mibyan run on the same host. Bind-mount the data volume to a directory that Mibyan can read (e.g.,
/var/lib/telegram-bot-api), and make sure the file ownership matches. The container drops privileges to its internaltelegram-bot-apiuser (uid varies by image); the simplest fix is to adduser: "<UID>:<GID>"to the compose service so files are owned by a uid Mibyan already runs as. - Different machines — the bot server runs on one host (e.g., a NAS, a separate VM) and Mibyan on another. The server’s data directory must be shared with the Mibyan machine at the same absolute path the server reports (typically
/var/lib/telegram-bot-api). NFS works well for this; CIFS/SMB withuid=mount remapping is friendlier if you don’t want to deal with uid mismatches at the filesystem level.
local_mode: true is set but Mibyan can’t stat the returned file path (permissions or wrong mount), python-telegram-bot silently falls back to an HTTP getFile against the local server — which in --local mode responds with 404 Not Found. The symptom shows up in gateway.log as:
ls -la /var/lib/telegram-bot-api/<TOKEN>/voice/ from the Mibyan host as the user the gateway runs as, and confirm a single file is cat-able without a permission error.
Step 6: Test it
Send the bot a voice note or audio file that’s bigger than 20 MB. Tail the gateway log:[Telegram] Cached user voice at /home/<user>/.mibyan/cache/audio/... line and no “too large” rejection. Combined with stt.enabled: false (above), the path to the original audio file then lands in the agent’s inbound message for downstream processing.
Group Chat Usage
Mibyan works in Telegram group chats with a few considerations:- Privacy mode determines what messages the bot can see (see Step 3)
TELEGRAM_ALLOWED_USERSstill applies — only authorized users can trigger the bot, even in groups- You can keep the bot from responding to ordinary group chatter with
telegram.require_mention: true - With
telegram.require_mention: true, group messages are accepted when they are:- replies to one of the bot’s messages
@botusernamementions/command@botusername(Telegram’s bot-menu command form that includes the bot name)- matches for one of your configured regex wake words in
telegram.mention_patterns
- In groups with multiple Mibyan bots,
telegram.exclusive_bot_mentionskeeps routing deterministic. When a message explicitly mentions one or more Telegram bot usernames, only the mentioned bot profiles process it; other Mibyan bots ignore it before reply and wake-word fallbacks run. This is enabled by default. - Renaming the bot’s
@usernamein BotFather is picked up automatically — Mibyan follows the new handle for mention routing without a gateway restart. Collectible (Fragment) usernames that don’t end inbotare supported too. - Use
telegram.ignored_threadsto keep Mibyan silent in specific Telegram forum topics, even when the group would otherwise allow free responses or mention-triggered replies - If
telegram.require_mentionis left unset or false, Mibyan keeps the previous open-group behavior and responds to normal group messages it can see
Multiple Mibyan bots in one group
If you run several Mibyan profiles in the same Telegram group, create one Telegram bot token per profile and start one gateway per profile. Do not reuse the same bot token in multiple running gateways; Telegram will reject concurrent polling for the same token. Recommended group config:@research_bot @ops_bot summarize this is processed by research_bot and ops_bot only. Other Mibyan bots in the group stay silent, even if the message is a reply to one of their earlier messages or would otherwise match a shared wake word.
Two Mibyan bots that answer each other’s quote-replies can still loop forever with TELEGRAM_ALLOW_BOTS=all, because a reply to the bot always passes the require_mention gate. Setting telegram.bots_require_mention: true (env TELEGRAM_BOTS_REQUIRE_MENTION) closes that path: a message from another bot only triggers a response when it explicitly @mentions this bot, while human replies keep working unchanged.
A bot-to-bot loop guard also meters every chat where bot-authored messages are admitted (TELEGRAM_ALLOW_BOTS set to mentions or all). Once 20 bot messages land in one chat inside 5 minutes, further bot messages in that chat are dropped for 10 minutes and one warning is logged; human messages are never counted or dropped. Settings live in config.yaml:
max_events for that gateway.
Group conversation text and media captions keep every mention when the message names other participants too (@research_bot , @ops_bot are you both listening? reaches research_bot verbatim); when this bot is the only one addressed, its own handle is still stripped so short answers such as @mibyan_bot 2 keep working. Group turns also carry the bot’s own Telegram username in the per-channel context so the model can tell which retained mentions are for it. Slash commands still use the normal command-trigger cleanup.
Set exclusive_bot_mentions: false only for legacy groups where explicit mentions should not override reply and wake-word triggers.
To operate several profiles, run the gateway command once per profile. For example:
mibyan gateway <action> for the default profile and mibyan -p <profile> gateway <action> for each named profile. This is more reliable than assuming a single process-level command controls every named profile on every service manager.
Troubleshooting: works in DMs but not groups
If the bot responds in a private chat but stays silent in a group, check these gates in order:- Telegram delivery: turn off BotFather privacy mode, promote the bot to admin, or mention the bot directly. Mibyan cannot respond to group messages that Telegram never delivers to the bot.
- Rejoin after changing privacy: remove the bot from the group and add it again after changing BotFather privacy settings. Telegram may keep the old delivery behavior for existing memberships.
- Mibyan authorization: make sure the sender is listed in
TELEGRAM_ALLOWED_USERSorTELEGRAM_GROUP_ALLOWED_USERS, or allow the group chat withTELEGRAM_GROUP_ALLOWED_CHATS. - Mention filters: if
telegram.require_mention: trueis set, normal group chatter is ignored unless the message is a slash command, reply to the bot,@botusernamemention, or configuredmention_patternsmatch. - Multi-bot routing: if a group contains several bots, make sure each
Mibyan profile uses a unique bot token and keep
exclusive_bot_mentionsenabled unless you intentionally want legacy shared-trigger behavior.
TELEGRAM_GROUP_ALLOWED_CHATS, not
the sender-user allowlist.
Example group trigger configuration
Add this to~/.mibyan/config.yaml:
chompy, even if they do not use an @mention.
Messages in Telegram topics 31 and 42 are always ignored before the mention and free-response checks run.
Notes on mention_patterns
- Patterns use Python regular expressions
- Matching is case-insensitive
- Patterns are checked against both text messages and media captions
- Invalid regex patterns are ignored with a warning in the gateway logs rather than crashing the bot
- If you want a pattern to match only at the start of a message, anchor it with
^
Private Chat Topics (Bot API 9.4)
Telegram Bot API 9.4 (February 2026) introduced Private Chat Topics — bots can create forum-style topic threads directly in 1-on-1 DM chats, no supergroup needed. This lets you run multiple isolated workspaces within your existing DM with Mibyan.Use case
If you work on several long-running projects, topics keep their context separate:- Topic “Website” — work on your production web service
- Topic “Research” — literature review and paper exploration
- Topic “General” — miscellaneous tasks and quick questions
Configuration
Add topics underplatforms.telegram.extra.dm_topics in ~/.mibyan/config.yaml:
How it works
- On gateway startup, Mibyan calls
createForumTopicfor each topic that doesn’t have athread_idyet - The
thread_idis saved back toconfig.yamlautomatically — subsequent restarts skip the API call - Each topic maps to an isolated session key:
agent:main:telegram:dm:{chat_id}:{thread_id} - Messages in each topic have their own conversation history, memory flush, and context window
Root DM handling
By default, messages sent to the root DM (outside any topic) are processed normally. Setignore_root_dm: true to turn the root DM into a lobby — normal
messages are silently ignored for users who have DM topics configured, while
system commands (/start, /help, /status, etc.) still work.
dm_topics
will have their root DM affected. Users without configured topics are
unaffected.
Skill binding
Topics with askill field automatically load that skill when a new session starts in the topic. This works exactly like typing /skill-name at the start of a conversation — the skill content is injected into the first message, and subsequent messages see it in the conversation history.
For example, a topic with skill: arxiv will have the arxiv skill pre-loaded whenever its session resets (after an explicit /new or /reset).
Multi-session DM mode (/topic)
A ChatGPT-style multi-session DM — one bot, many parallel conversations. Unlike the operator-curated extra.dm_topics above, this mode is user-driven: no config, no pre-declared topic names. The end user flips it on with /topic, then taps the Telegram + button to create as many topics as they want, each one a fully independent Mibyan session.
/topic subcommands
Only authorized users (allowlist via
TELEGRAM_ALLOWED_USERS / platform auth config) can run /topic. An unauthorized sender gets a refusal instead of activation.
DM Topics vs Multi-session DM mode
Both features can coexist on the same bot — you’d run
/topic from a user’s DM, and extra.dm_topics continues to manage operator-declared topics for other chats.
Prerequisites
In @BotFather, open your bot → Bot Settings → Threads Settings:- Turn on Threaded Mode (enables
has_topics_enabled) - Do not disable users creating topics (keeps
allows_users_to_create_topicson)
/topic, Mibyan calls getMe to verify both flags. If either is off, Mibyan sends a screenshot of the BotFather Threads Settings page and explains what to toggle — no activation happens until prerequisites are met.
Activation flow
From the root DM, send:- Check
getMe().has_topics_enabledandallows_users_to_create_topics - If both are true, enable multi-session topic mode for this DM
- Create and pin a System topic for status/commands (best-effort)
- Reply with a list of previous unlinked Telegram sessions the user can restore
/status, /sessions, /usage, /help, etc.) still work in the root.
Creating a new topic (end-user flow)
- Open the bot DM in Telegram
- Tap All Messages at the top of the bot interface, then send any message
- Telegram creates a new topic for that message
- Mibyan responds inside that topic — the topic is now a standalone session
agent:main:telegram:dm:{chat_id}:{thread_id} — identical to the config-driven DM topics isolation.
Auto-renamed topics
When Mibyan generates a session title for a topic (via the auto-title pipeline, after the first exchange), the Telegram topic itself is renamed to match — e.g. “New Topic” becomes “Database migration plan”. The rename is best-effort: failures are logged but don’t break the session. To disable this and keep your manually-chosen topic names untouched, set:mibyan sessions, the TUI, etc.) but never edits the Telegram topic name. Useful when you organise topics by hand under BotFather Threaded Mode and don’t want every first reply to overwrite the title.
/new inside a topic
Resets the current topic’s session (new session ID, fresh history) without touching other topics. Mibyan replies with a reminder that for parallel work, creating another topic (via All Messages) is usually what you want.
Restoring a previous session
Inside a topic, send:- The target session must belong to the same Telegram user
- The target session must not already be bound to another topic
/topic (no argument) in the root DM — Mibyan lists the user’s unlinked Telegram sessions.
/topic inside a topic (no argument)
Shows the current topic’s binding: session title, session ID, and hints for /new vs creating another topic.
Under the hood
- Activation persists to
telegram_dm_topic_mode(profile_name, chat_id, user_id, enabled, ...)instate.db. Primary key is(profile_name, chat_id)so multiplexed / profile-routed bots sharing onestate.dbdo not clobber each other when the same Telegram user DMs multiple bots (privatechat_idis the user id and is identical across bots). - Each topic binding persists to
telegram_dm_topic_bindings(profile_name, chat_id, thread_id, session_id, ...)with PK(profile_name, chat_id, thread_id)andON DELETE CASCADEonsession_id— pruning a session automatically clears its topic binding - The topic-mode SQLite migration is opt-in: it runs on the first
/topiccall, never on gateway startup. Until a user runs/topicin this profile,state.dbis unchanged. Schema v3 addsprofile_name; legacy rows migrate into thedefaultnamespace only - Each inbound DM message looks up its
(profile_name, chat_id, thread_id)binding using the routed profile (source.profile, not the process-global active profile). If present, the lookup routes the message to the bound session viaSessionStore.switch_session()so the session-key-to-session-id mapping stays consistent on disk /newinside a topic rewrites the binding row to point at the new session ID, so the next message stays on the fresh session- Topics declared in
extra.dm_topicsare never auto-renamed — the operator-chosen name is preserved even when multi-session mode is enabled - Set
extra.disable_topic_auto_rename: trueto turn off auto-rename for all topics in the chat (ad-hoc topics created via Threaded Mode included) - The General (pinned top) topic in a forum-enabled DM is treated as the root lobby, regardless of whether Telegram delivers its messages with
message_thread_id=1or with no thread_id - Root-lobby reminders are rate-limited to one message per 30 seconds per (profile, chat) — a user who forgets topic mode is on and types ten prompts in the root won’t get ten replies, and two multiplexed profiles sharing a chat id do not suppress each other’s reminders
- BotFather setup screenshots are rate-limited to one send per 5 minutes per (profile, chat) — repeated
/topicattempts while Threads Settings are still disabled won’t re-upload the same image /bg <prompt>started inside a topic delivers its result back to the same topic; background sessions don’t trigger auto-rename of the owning topic/topicitself is gated by the bot’s user authorization check — unauthorized DMs get a refusal instead of activation
Disabling multi-session mode
Send/topic off in the root DM. Mibyan flips the row off for this profile’s namespace, clears that profile’s (thread_id → session_id) bindings for the chat, and the root DM reverts to a normal Mibyan chat. Existing topics in Telegram aren’t deleted — they just stop being gated as independent sessions. Re-run /topic later to turn it back on.
If you need to clean up by hand (e.g. a bulk reset across many chats), scope rows by profile_name (use default for single-profile installs):
Downgrading Mibyan
If you downgrade to a Mibyan version that predates/topic, the feature simply stops working — the telegram_dm_topic_mode and telegram_dm_topic_bindings tables remain in state.db but are ignored by older code. DMs revert to the native per-thread isolation (each message_thread_id still gets its own session via build_session_key), so your existing Telegram topics keep working as parallel sessions. The root DM is no longer a lobby — messages there go into the agent like they used to. Re-upgrading reactivates multi-session mode exactly where it was.
Group Forum Topic Skill Binding
Supergroups with Topics mode enabled (also called “forum topics”) already get session isolation per topic — eachthread_id maps to its own conversation. But you may want to auto-load a skill when messages arrive in a specific group topic, just like DM topic skill binding works.
Use case
A team supergroup with forum topics for different workstreams:- Engineering topic → auto-loads the
software-developmentskill - Research topic → auto-loads the
arxivskill - General topic → no skill, general-purpose assistant
Configuration
Add topic bindings underplatforms.telegram.extra.group_topics in ~/.mibyan/config.yaml:
How it works
- When a message arrives in a mapped group topic, Mibyan looks up the
chat_idandthread_idingroup_topicsconfig - If a matching entry has a
skillfield, that skill is auto-loaded for the session — identical to DM topic skill binding - Topics without a
skillkey get session isolation only (existing behavior, unchanged) - Unmapped
thread_idvalues orchat_idvalues fall through silently — no error, no skill
Differences from DM Topics
Recent Bot API Features
- Bot API 9.4 (Feb 2026): Private Chat Topics — bots can create forum topics in 1-on-1 DM chats via
createForumTopic. Mibyan uses this for two distinct features: operator-curated Private Chat Topics (config-driven, fixed topic list) and user-driven Multi-session DM mode (activated by/topic, unlimited user-created topics). - Privacy policy: Telegram now requires bots to have a privacy policy. Set one via BotFather with
/setprivacy_policy, or Telegram may auto-generate a placeholder. This is particularly important if your bot is public-facing. - Bot API 9.5 (Mar 2026): Native streaming via
sendMessageDraft. Mibyan supports Telegram’s native streaming-draft API as an opt-in transport for private chats. The default remains the legacyeditMessageTextpath because draft previews can visibly collapse and re-render on some Telegram clients.
Streaming transport (gateway.streaming.transport)
When streaming is enabled (gateway.streaming.enabled: true), Mibyan picks one of four transports:
In
~/.mibyan/config.yaml:
edit (default) — the gateway sends a normal preview message and progressively updates it via editMessageText, avoiding Telegram’s draft-preview collapse/rollback effect.
What you’ll see in DMs with auto or draft — Telegram shows an animated draft preview that updates token-by-token. When the reply finishes, it’s delivered as a regular message and the draft preview clears naturally on the client. Drafts have no message id, so the final answer is what stays in your chat history.
What about groups, supergroups, forum topics? Telegram restricts sendMessageDraft to private chats (DMs). The gateway transparently falls back to the edit-based path for everything else — same UX as before.
What if a draft frame fails? Any failure (transient network error, server-side rejection, older python-telegram-bot install) flips that response back to the edit-based path for the rest of the stream. The next response gets a fresh attempt.
Rendering: Rich Messages, Tables and Link Previews
Rich Messages (Bot API 10.1). Final replies that contain constructs the legacy MarkdownV2 path degrades — tables, task lists, collapsible<details>, and block math — are sent with Telegram’s native sendRichMessage using the agent’s raw markdown, so they render natively with no client-side flattening. In DMs, the default rich_drafts: false keeps the streaming preview plain — it uses Telegram’s ephemeral draft transport with legacy rendering (tables and other rich-only constructs stay as raw markdown in the preview) — then persists the completed response with sendRichMessage. Setting rich_drafts: true makes the live preview use sendRichMessageDraft too. Edit-based streams can finalize an existing preview in place through editMessageText’s rich_message parameter. Ordinary replies (plain prose, bold/italic, simple lists) stay on the MarkdownV2 path for consistent font weight and spacing across clients.
The rich path is skipped automatically when content exceeds the 32,768-character rich text limit, and any rejection from Telegram (unsupported endpoint on an older python-telegram-bot, parser error, oversized blocks/columns) transparently falls back to the MarkdownV2 path — your message is never lost. Transient/network errors are not silently re-sent (no duplicate final message).
MarkdownV2 fallback. When the rich path is unavailable for a message, Mibyan converts markdown to MarkdownV2. Since MarkdownV2 has no native table syntax, pipe tables are normalized:
- Small tables are flattened into row-group bullets — each row becomes a readable bulleted list under the column headings. Good for 2–4 columns and short cells.
- Larger or wider tables fall back to a fenced code block with aligned columns so nothing collapses.
rich_drafts controls whether the DM streaming preview renders rich (sendRichMessageDraft) and stays off by default because Telegram Desktop/macOS can visually overlay rich draft frames until the chat redraws; with it off, the preview streams plain and the final still arrives as a native Rich Message.
CJK text (Chinese, Japanese, Korean, and rare Han extensions) stays on the legacy MarkdownV2 path by default because affected Telegram Desktop/macOS clients have rendered Bot API rich messages with overlapping CJK glyph artifacts. If you use an unaffected client and prefer native rich tables/task lists/details/math for CJK payloads, set allow_cjk_rich_messages: true alongside rich_messages: true to opt in to that client-side risk.
If you only want the legacy “always code-block” table behavior while keeping rich messages enabled, disable table normalization by setting telegram.pretty_tables: false in config.yaml (default: true).
Link previews. Telegram auto-generates link previews for URLs in bot messages. If you’d rather suppress those (long /tools output, agent reply that mentions ten links, etc.):
LinkPreviewOptions(is_disabled=True) to every outgoing message and falls back to the legacy disable_web_page_preview parameter on older python-telegram-bot versions.
Long replies and flood control. A reply longer than Telegram’s 4,096-character limit is sent as numbered parts ((1/3), (2/3), …). Sends to one chat are delivered one reply at a time, so a scheduled report and a DM answer landing together cannot interleave their parts, and a file upload cannot land between two parts of the text it accompanies. If Telegram’s flood control refuses a part mid-way, Mibyan resumes from the refused part once the penalty passes instead of re-sending the parts already on screen, and while a chat is inside a known penalty window further sends to it fail closed locally (no extra requests that would lengthen the penalty). A penalty longer than the gateway’s inline wait cap is handed to the delivery ledger, which redelivers the reply with a “part of it may already have arrived above” note.
Group Allowlisting
Telegram groups and forum chats have two orthogonal gates you can configure:- Sender user IDs (
group_allow_from/TELEGRAM_GROUP_ALLOWED_USERS) — sender-scoped allowlist that applies only to group/forum messages. Use this when you want specific users to be able to invoke the bot in groups without adding them toTELEGRAM_ALLOWED_USERS(which would also give them DM access). - Chat IDs (
group_allowed_chats/TELEGRAM_GROUP_ALLOWED_CHATS) — chat-scoped allowlist. Any member of these groups/forums can interact with the bot. Useful for team/support bots where group membership itself is the access signal.
TELEGRAM_ALLOWED_USERScovers all chat types (DMs, groups, forums).TELEGRAM_GROUP_ALLOWED_USERSonly authorizes the listed senders in groups/forums. They still can’t DM the bot unless listed inTELEGRAM_ALLOWED_USERS.- A chat in
TELEGRAM_GROUP_ALLOWED_CHATSauthorizes every member of that chat, regardless of sender. - Use
*in any of these to allow any sender/chat. - This layers on top of existing mention/pattern triggers and on top of
group_topics+ignored_threads.
Migration from before PR #17686
Prior to this split,TELEGRAM_GROUP_ALLOWED_USERS was the only knob and users put chat IDs in it. For backward compatibility, chat-ID-shaped values (starting with -) in TELEGRAM_GROUP_ALLOWED_USERS are still honored as chat IDs and a deprecation warning is logged once. Migration:
Guest @mention bypass (guest_mode)
In a typical setup, group_allowed_chats is a hard gate: messages from groups outside the list are silently dropped, even if a member explicitly @mentions the bot. That’s the right default for support / team bots.
For more casual setups — friend group chats where you want the bot mostly silent but occasionally available on explicit ping — enable guest_mode:
false.
With guest_mode: true, a message from a non-allowlisted group is processed only if it explicitly @mentions the bot. The mention is required every turn — there’s no session stickiness for guest interactions, so the bot never auto-engages in a friend group thread it isn’t pinged into.
DMs and allowlisted groups behave exactly as before.
Slash Command Access Control
By default, every allowed user can run every slash command. To split your allowlist into admins (full slash command access) and regular users (only commands you explicitly enable), addallow_admin_from and user_allowed_commands to the platform’s extra block:
- A user listed in
allow_admin_fromfor a scope (DM or group) can run every registered slash command — built-in commands AND plugin-registered ones — through the live registry. - A user in
allow_frombut not inallow_admin_fromcan only run commands listed inuser_allowed_commands, plus the always-allowed floor:/helpand/whoami. - Plain chat (non-slash messages) is unaffected. Non-admin users can still talk to the agent normally, they just can’t trigger arbitrary commands.
- Backward compat: if
allow_admin_fromis not set for a scope, slash command gating is disabled for that scope. Existing installs keep working with no changes. - DM admin status does not imply group admin status. Each scope has its own admin list.
- If only
group_allow_admin_fromis set, DM scope stays in unrestricted (backward-compat) mode.
/whoami to see the active scope, your tier (admin / user / unrestricted), and which slash commands you can run.
Interactive Model Picker
When you send/model with no arguments in a Telegram chat, Mibyan shows an interactive inline keyboard for switching models:
- Provider selection — buttons showing each available provider with model counts (e.g., “OpenAI (15)”, ”✓ Anthropic (12)” for the current provider).
- Model selection — paginated model list with Prev/Next navigation, a Back button to return to providers, and Cancel.
DNS-over-HTTPS Fallback IPs
In some restricted networks,api.telegram.org may resolve to an IP that is unreachable. The Telegram adapter includes a fallback IP mechanism that transparently retries connections against alternative IPs while preserving the correct TLS hostname and SNI.
How it works
- If
TELEGRAM_FALLBACK_IPSis set, those IPs are used directly. - Otherwise, the adapter automatically queries Google DNS and Cloudflare DNS via DNS-over-HTTPS (DoH) to discover alternative IPs for
api.telegram.org. - Known IPv4 Telegram API IPs are tried before the dual-stack
api.telegram.orghostname. A blackholed IPv6 path can sit inconnect()without erroring, which used to pin the event loop so the 30s init deadline never fired. - If DoH is also blocked or times out, a hardcoded IPv4 seed list (
149.154.166.110,149.154.167.220) is used as that IPv4-first list. The hostname remains last resort. - Once a path succeeds, it becomes “sticky” — subsequent requests use it directly. The hostname is kept as a last resort for IPv6-only networks.
Configuration
~/.mibyan/config.yaml:
Proxy Support
If your network requires an HTTP proxy to reach the internet (common in corporate environments), the Telegram adapter automatically reads standard proxy environment variables and routes all connections through the proxy.Supported variables
The adapter checks these environment variables in order, using the first one that is set:HTTPS_PROXYHTTP_PROXYALL_PROXYhttps_proxy/http_proxy/all_proxy(lowercase variants)
Configuration
Set the proxy in your environment before starting the gateway:~/.mibyan/.env:
This covers the custom fallback transport layer that Mibyan uses for Telegram connections. The standard
httpx client used elsewhere already respects proxy env vars natively.Message Reactions
The bot can add emoji reactions to messages as visual processing feedback:- 👀 when the bot starts processing your message
- 👍 when the response is delivered successfully
- 👎 if an error occurs during processing
config.yaml:
Unlike Discord (where reactions are additive), Telegram’s Bot API replaces all bot reactions in a single call. The transition from 👀 to 👍/👎 happens atomically — you won’t see both at once.
Per-Channel Prompts
Assign ephemeral system prompts to specific Telegram groups or forum topics. The prompt is injected at runtime on every turn — never persisted to transcript history — so changes take effect immediately.- Message in topic
42inside group-1001234567890→ uses topic42’s prompt - Message in topic
99(no explicit entry) → falls back to group-1001234567890’s prompt - Message in a group with no entry → no channel prompt applied
Troubleshooting
Exec Approval
When the agent tries to run a potentially dangerous command, it asks you for approval in the chat:⚠️ This command is potentially dangerous (recursive delete). Reply “yes” to approve.Reply “yes”/“y” to approve or “no”/“n” to deny.
Interactive Prompts (clarify)
When the agent calls theclarify tool — to ask which approach you prefer, get post-task feedback, or check before a non-trivial decision — Telegram renders the question with inline keyboard buttons:
❓ Which framework should I use for the dashboard? [1. Next.js] [2. Remix] [3. Astro] [✏️ Other (type answer)]Tap a button to answer, or tap Other to type a free-form response (the next message you send becomes the answer). Open-ended
clarify calls (no preset choices) skip the buttons and just capture your next message.
Configure the response timeout via agent.clarify_timeout in ~/.mibyan/config.yaml (default 3600 seconds). If you don’t respond within the timeout, the agent unblocks with "outcome": "timed_out" and adapts rather than hanging. Reply skip to skip a question.
If Telegram cannot render the button card (the Bot API rejects it, or the send fails after its 15-second acknowledgement window), Mibyan re-asks the same question as a plain numbered-list message and your typed reply (a number or the option text) is taken as the answer. When even that cannot be delivered, the agent is released at once with [clarify prompt could not be delivered] instead of waiting out the timeout and mistaking the silence for you not answering.
Push notification volume
Telegram fires a push notification on every message the bot sends. For long agent turns that emit tool-progress bubbles, streaming updates, and status callbacks, this gets noisy fast. The Telegram adapter has two notification modes:
Configure in
~/.mibyan/config.yaml:
important.
Status messages edited in place
The Telegram adapter routes recurring agent status callbacks (e.g. “Compressing context…”, “Calling tool…”) throughsend_or_update_status(), which keeps a {(chat_id, status_key) → message_id} cache and edits the existing bubble on subsequent emits instead of appending a new one each time. Distinct status_key values get their own messages; distinct chats never collide. If the edit fails (e.g. the user deleted the message, or it’s older than Telegram allows for edits), the cache entry is dropped and the next emit posts a fresh message and re-caches its ID. No config required — this is the default Telegram behavior. Other adapters that don’t implement send_or_update_status fall through to plain send() unchanged.
Pin incoming user message during agent turn
When a user sends a message that triggers an agent turn, the Telegram adapter pins that incoming message for the duration of the turn and unpins it when the response is finished — a lightweight visual indicator that the bot is actively working on the message rather than ignoring it. The pin usesdisable_notification=true to avoid extra pings. No config required.
Security
Never share your bot token publicly. If compromised, revoke it immediately via BotFather’s/revoke command.
For more details, see the Security documentation. You can also use DM pairing for a more dynamic approach to user authorization.
