Telegram Multi-Fleet Hub
Experimental. Steer every running fleet on your machine from one Telegram bot. Each fleet gets its own forum topic in a private group. The General topic is the Control channel: it talks to the control plane and runs hub commands.
The hub runs inside the control plane (agents-fleet --control-plane) and uses the control-plane bot. It replaces the "one bot per fleet" setup from Telegram Remote Steering, although that setup still works.
How it works
Telegram forum group ("Fleet Hub")
├─ General = Control ── /fleets /pending /stop … + chat with the control plane
├─ 🟢 api-server ── fleet A: prompts in, replies / asks / task completions out
├─ 🟡 web-frontend ── fleet B (waiting on an ask)
└─ ⚫ data-pipeline ── fleet C (stopped)
▲
│ one bot token (control plane)
agents-fleet --control-plane ──(per-fleet control socket, token-gated)──► agents-fleet --hub- Fleets opt in. A fleet joins only when it starts with
--hub(orAGENTS_FLEET_TELEGRAM_HUB_ATTACH=1, or"telegramHub": {"attach": true}in~/.fleet/config.json). Opting in turns on the fleet's control socket. - You approve every fleet once (TOFU). Before it reads any fleet token or opens a connection, the hub validates the fleet's discovery descriptor and asks you in General. This includes fleets the control plane started itself (
start_fleet): they are not trusted automatically. Approve once per session or per folder. - Only you can act. The hub checks the sender's
from.idon every message and every button tap, not just the chat. Other group members are ignored. A button tap counts only in the chat where the hub posted that button. - Topic text goes straight to the fleet. No LLM sits between the topic and the fleet. Text typed in a topic becomes a prompt for that fleet. Forwarded messages are refused everywhere in the hub (topics, General and your DM): type or paste the text instead.
Setup runbook
1. Create the control-plane bot
In Telegram, talk to @BotFather, send
/newbotand copy the token.Use a different bot from any fleet's
--telegrambot. The control plane refuses to start when the two tokens are equal.Export the token on the control-plane host:
bashexport AGENTS_FLEET_CONTROL_PLANE_TELEGRAM_TOKEN='<token>' # PowerShell: $env:AGENTS_FLEET_CONTROL_PLANE_TELEGRAM_TOKEN='<token>' agents-fleet --control-plane
2. Pair with the bot in a DM
agents-fleet bot pair --control-planeThe command prints /start <token> once. Open a private chat with the bot and send that line within 5 minutes. The bot answers ✅ Paired!. This DM pairing makes you the operator. Pairing only works in a DM; the bot ignores /start in groups.
3. Create the forum group
- Create a new group and add the bot.
- In the group settings, turn on Topics. This converts the group into a forum supergroup.
- Promote the bot to administrator with the Manage Topics right.
- In your own admin settings for this group, turn Remain Anonymous off. An anonymous admin (or a post "as channel") arrives as
GroupAnonymousBot, not as you, so the hub ignores it.
4. Bind the group
Send /hubbind in the group. If the bot is not an admin yet and still has privacy mode on, Telegram only delivers commands addressed to it: send /hubbind@<botname> instead.
The bot then DMs you: "Bind group '…' as your fleet hub?". Tap ✅ Bind. The prompt expires after 5 minutes. A /hubbind from anyone who is not DM-paired gets no reply.
5. Verify
Send /hub in General. A correct setup shows:
mode=forum · chat=-100… · operator=<your Telegram id>mode=fallback means a right is missing: Topics are off, or the bot is not an admin with Manage Topics. The hub posts the fix in General and re-checks every 10 minutes. It also re-checks when the bot's rights change.
6. Start fleets with --hub
cd ~/src/api-server && agents-fleet --hubOr persist the opt-in in that fleet's ~/.fleet/config.json (or its FLEET_HOME):
{ "telegramHub": { "attach": true } }Fleets started by the control plane (start_fleet) opt in automatically. They still never receive any Telegram token, and they still need your approval (step 7).
7. Approve each fleet once
For each new fleet, including the ones the control plane started, a prompt appears in General with its name, folder, FLEET_HOME, pid and start time:
| Button | Effect |
|---|---|
| ✅ Approve session | Trust this folder for this session (bound to the session id; the name is only shown). A different session in the same folder asks again, even if it reuses the name. |
| 📁 Approve folder | Trust any session in this folder and FLEET_HOME. |
| ⛔ Deny | Ignore this fleet. Revoke the decision with /hub trust revoke <n>. |
The hub connects only after you approve. It then creates the fleet's topic, named after the session name or else the folder name.
Using the hub
In a fleet topic
| You do | What happens |
|---|---|
| Type text | Sent to that fleet as a prompt. If the fleet is not running, you get ⚫ fleet not running — message not delivered. |
| Tap an ask / confirm button | Answers that fleet's pending request. Many requests can be pending at once; each tap resolves only its own request. |
| Reply (Telegram "Reply") to a freeform ask | The reply text is the answer. |
/verbose on|off | Also post tool calls, hub activity and terminal input (⌨️ terminal: …) for this topic. |
/hub | Hub diagnostics. |
Any other /command | Sent to the fleet as typed. Prefix // to send a command the hub would otherwise handle (//verbose sends /verbose). Dangerous fleet commands are blocked (see below). |
Commands blocked from Telegram. These fleet slash commands change configuration, secrets, worktrees, loops, approvals or the process itself, so the hub refuses them in a topic (also with the // prefix): /mcp, /config, /bot-set-token, /bot-clear-token, /worktree, /loop, /loop-target, /approve, /exit, /quit, /q. Run them in the fleet's terminal. To stop a fleet from Telegram, use /stop <fleet> in General.
The fleet enforces this itself for anything the hub sends: it resolves the command to its real name first, so aliases, other casing and a @bot suffix are refused too (/wt, /WORKTREE, /worktree@bot). Commands that need a confirmation tap on the regular Telegram bot are also refused from the hub, because the hub has no confirm step for them: for example /root, /compete, /commit, /team-repo, /restart and skill/crew workflow commands. Run those in the fleet's terminal as well. You get 🚫 /<command> is blocked from Telegram — run it in the fleet's terminal.
Some read-only commands also have subcommands that change things. From Telegram only their read-only forms run (the subcommand must be typed exactly as shown; anything else is refused with the same 🚫 message):
| Command (and aliases) | Allowed from Telegram |
|---|---|
/crew | /crew list (use /crews for the overview) |
/wf | no argument, status, help |
/workflows | no argument, list, status, help |
/learn (/intelligence) | no argument, overview, steering, shadows |
/fleet-intel (/intel) | no argument, summary, agents, failures, tokens, stats, skills, search, insights |
/skill (/sk), /skills | no argument, list (/skills takes none) |
/team | no argument, list |
Only text that starts with / is treated as a command. Text with a leading space ( /do x) reaches the fleet as a plain prompt and cannot run a command. The same policy applies when the control-plane assistant drives a fleet (drive_fleet_prompt): it cannot run blocked or confirmation-required commands or mutating subcommands either.
Fleet replies, asks, confirms, errors and task completions are posted into the fleet's topic. If you answer an ask elsewhere first (terminal, another channel), the topic message is edited to ↪ resolved elsewhere.
The topic title shows the fleet status: ⚫ stopped > 🟡 waiting for you > 🔴 error > 🟢 working > ⚪ idle. Title edits are debounced.
In General (Control)
| Command | Description |
|---|---|
/fleets | One line per fleet: status, workers, tasks, tokens, context %, topic link. |
/pending | Every unanswered ask/confirm across fleets, oldest first, with links. |
/stop <fleet> | Stop a fleet. Requires 3 taps within 60 s. Control-plane fleets stop through the supervisor; others get SIGTERM. On Windows this is a hard terminate. |
/pause <fleet> / /resume <fleet> | Pause / resume task dispatch (/pause-dispatch). |
/verbose <fleet> on|off | Verbosity for a fleet's topic. |
/use <fleet> / /use / /use none | Fallback mode only: send your plain text to a fleet, show the selection, or clear it. |
/hub | Mode, operator, discovery stats, invalid descriptors with reasons, last probe, queue depth and oldest item per class, unauthorized counters. |
/hub trust / /hub trust revoke <n> | List fleet approvals / revoke one (the fleet detaches and asks again). |
/hub unbind | Unbind the group. Topics are kept, closed. |
| Anything else | Goes to the control-plane LLM, as in a DM with the bot. |
Your DM with the bot keeps working as the control-plane chat. Fleet traffic goes only to the group.
Behaviour to know
- Stale messages are not executed. Telegram re-delivers messages that were sent while the hub was down. Prompts,
/stopand Control commands older thanmaxInboundAgeMs(default 10 min) are rejected with⌛ stale … not delivered. - Order is kept. Two quick messages in the same chat reach the fleet in the order you sent them. Button taps are handled at once and never wait behind a message.
- Everything posted is redacted. Secrets and PII are masked in all hub output: messages, edits, button labels and topic names.
- Stale links are retired. When a fleet's pid is reused or its descriptor goes stale, the hub drops the old link and treats the next start as a new fleet.
- Rate limits. Telegram allows about 20 messages per minute per group. All topics and General share one queue (
AGENTS_FLEET_TELEGRAM_HUB_RATE_PER_MIN, default 20) with round-robin fairness. Asks, confirms and errors go first and are never dropped. Replies are coalesced. Status and verbose posts are sent silently (no notification). Control-plane replies in your DM are not part of this budget. - Stopping the control plane. On a clean stop the control plane waits up to 5 s for messages it already received to finish routing. A message Telegram already handed over can be lost if the process is killed hard.
- Restarts never repost history. After a control-plane or fleet restart, the hub reuses the same topic and approval. It posts only what is new. An ask that was posted before the restart keeps working through its original button.
- Fleet also running
--telegram. Its asks appear in both its own bot DM and the hub topic. The first answer wins, and the other copy shows↪ resolved elsewhere. The fleet prints a warning at startup. - The control-plane hub dies, fleets keep running. A
--hubfleet keeps its token-gated control socket open, and its descriptor keeps advertisinghubAttach. A restarted control plane re-attaches without prompting. To close the socket, restart the fleet without--hub. - Topics. If you delete a fleet's topic, the hub creates a new one on the next post. If you close a topic, the hub only posts asks and confirms there, and reopens it to do so.
- Attachments. Files are accepted only in General (the control plane downloads them). In a fleet topic they are not delivered.
Turning it off
| Goal | How |
|---|---|
| Disable the hub (legacy control-plane bot) | AGENTS_FLEET_TELEGRAM_HUB=0, or "enabled": false in <control-plane home>/hub.json. |
| Detach one fleet | Restart it without --hub (or AGENTS_FLEET_TELEGRAM_HUB_ATTACH=0), or /hub trust revoke <n>. |
| Unbind the group | /hub unbind in General or your DM. |
| Revoke your pairing | Remove it from <control-plane home>/.fleet/telegram.json (bot revoke has no --control-plane variant yet). Pairings are re-read on every update. |
All settings are listed in Configuration.
Troubleshooting
| Symptom | Fix |
|---|---|
/hubbind gets no answer | Pair in a DM first (bot pair --control-plane). If the bot is not an admin yet, use /hubbind@<botname>. |
| Your messages in the group are ignored | Turn off Remain Anonymous and don't post "as channel". The hub DMs you a hint once. |
/hub shows mode=fallback | Turn on Topics; promote the bot to admin with Manage Topics. |
| A fleet never shows up | Start it with --hub. Check /hub for invalid descriptors (with the reason) or a pending approval in General. A denied fleet needs /hub trust revoke <n>. |
| A fleet asks for approval again | It runs a new session and was approved per session. Use 📁 Approve folder to stop this. |
A fleet is listed under "invalid" in /hub | The registry entry failed validation (dead pid, paths outside its FLEET_HOME, socket path mismatch …). The hub never connects to it. The group only gets one summary notice (⚠️ N invalid fleet descriptor(s) ignored — see /hub) at most every 10 minutes; details are only in /hub. A planted descriptor is expected to end up here. |
| A forwarded message is refused | Forwards are refused in hub chats. Type or paste the text. |
/mcp (or another command) is refused in a topic | It is on the blocked list. Run it in the fleet's terminal. |
authentication failed (token rotated?) | The fleet's daemon.token changed. Restart the fleet; the hub does not retry. |
Constant 429 | Lower the volume (/verbose off) or AGENTS_FLEET_TELEGRAM_HUB_RATE_PER_MIN. |
Security notes
- The hub reads a fleet's
daemon.tokenonly after the descriptor passes validation and you approve the fleet. The token is never written to hub state or logs. - On Windows, named pipes carry no ACL, so the token file is the only boundary. A local process that squats a fleet's pipe name before the fleet starts could receive the token in the
helloframe. Known v1 limitation: the control-sockethellosends the raw token; a challenge-response handshake is tracked in #833. See the Telegram threat model andSECURITY.md. - v1 has one operator per group.