Page
Gateway (Telegram)
Choir uses Telegram as its user-facing gateway. choird owns all bot tokens and routes messages between Telegram DMs and agent edge lanes.
Multi-Bot Setup
The gateway supports multiple Telegram bot instances. Each bot is a named gateway in config.json:
"gateways": {
"bot-main": {
"type": "telegram",
"secret": "tg-bot-main-token"
},
"bot-work": {
"type": "telegram",
"secret": "tg-bot-work-token"
}
}
Each gateway references a named secret for its bot token. Set tokens via:
echo '123456:AABBCC...' | choirctl secret set tg-bot-main-token
choirctl secret apply
gateways.<name>.secret is a secret reference, not a raw bot token. choird resolves
the token value from ~/.choir.d/secrets.json when starting each gateway.
choird starts all configured bot instances at startup and connects to the Telegram Bot API via long polling.
At startup, choird also registers Telegram command menus (setMyCommands) so private chats show command suggestions/autocomplete in the bot UI:
- Regular DMs get regular command suggestions.
- Admin DMs get the admin command superset.
- The menu command for update-all is
/update_all(Telegram-safe); choird accepts both/update_alland/update-all.
DM Configuration
Each DM is a named binding between a gateway (bot), a Telegram user ID, and a permission level:
"dms": {
"admin-dm": { "gateway": "bot-main", "user_id": "123456789", "admin": true },
"user-dm": { "gateway": "bot-main", "user_id": "987654321", "admin": false },
"work-dm": { "gateway": "bot-work", "user_id": "123456789", "admin": true }
}
Allowlist
The set of configured DMs for a bot implicitly forms that bot’s allowlist. Messages from Telegram user IDs that do not match any DM entry for the receiving bot are silently ignored.
Admin vs Regular DMs
| Permission | Scope |
|---|---|
Admin ("admin": true) |
Full choirctl-equivalent access. Can manage all agents, system resources, config, workspaces, secrets. |
Regular ("admin": false) |
Commands affect the bound agent only. No cross-agent or system-level operations. |
DM Binding Rules
- Each agent gets exclusive access to its bound DM. One agent per DM at a time.
- DM binding is established at
agent start:- Via gateway
/start: the DM that sends the command is the binding target (if in allowlist). Rejected if the DM is already bound to another agent. - Via
choirctl agent start:--dm=<name>is required. - Default: each agent has a default DM in its
defaultsblock.
- Via gateway
- The binding is released when the agent’s session ends (stop, crash, terminate).
Gateway Commands
Regular DM Commands
Available to all configured DMs. Commands are scoped to the bound agent – no [agent-id] argument is accepted.
| Command | Equivalent choirctl |
Notes |
|---|---|---|
/help |
(no choirctl equivalent) | Show available commands and usage for this DM |
/status |
agent status <bound-agent> |
Lane states, budgets, skill, uptime |
/stop |
agent stop <bound-agent> |
Graceful termination |
/restart |
agent restart <bound-agent> |
Stop + start, same image |
/cores |
session cores <active-session> |
List active core jobs (name, state, step) |
/cancel <name> |
session cancel <active-session> |
Cancel a core job by name |
/compact |
session compact <active-session> |
Trigger working memory compaction |
/events |
session events <active-session> |
Last N events |
/model |
model get <bound-agent> |
Show current LLM model |
/model llm <name> |
model set <bound-agent> --llm=<name> |
Switch text generation model |
/voice |
voice get <bound-agent> |
Show current voice profile |
/voice <name> |
voice set <bound-agent> <name> |
Switch voice profile |
/inject <message> |
(no choirctl equivalent) | Inject message into edge context at next safe point |
/approvals |
approval list |
Lists pending approvals for bound agent |
/approve [id] |
approval approve <id> |
Approve; scoped to bound agent’s approvals |
/reject [id] |
approval reject <id> |
Reject; scoped to bound agent’s approvals |
Admin DM Commands
Admin DMs have access to all regular commands plus the following. Admin commands accept <agent-id> arguments to target any agent.
| Command | Equivalent choirctl |
Notes |
|---|---|---|
/start <agent-id> [key=value ...] |
agent start <agent-id> [flags] |
DM binding is the triggering DM if in allowlist |
/stop <agent-id> |
agent stop <agent-id> |
Stop any agent |
/restart <agent-id> |
agent restart <agent-id> |
Restart any agent |
/update [agent-id] |
agent update <agent-id> |
Build + stop + start with new image |
/update_all (or /update-all) |
agent update-all |
Build + redeploy all agents |
/config load |
config load |
Stage config changes |
/config diff |
config diff |
Show staged vs running diff |
/config apply |
config apply |
Apply hot-reloadable changes |
/workspace list |
workspace list |
List workspaces and lease holders |
/workspace reset <name> |
workspace reset <name> |
Reset workspace |
/secret list |
secret list |
List secret names |
/agent list |
agent list |
List all agents and status |
/agent build <agent-id> |
agent build <agent-id> |
Build agent image |
/approvals |
approval list |
Lists ALL pending approvals |
/approve [id] |
approval approve <id> |
Approve any agent’s approval |
/reject [id] |
approval reject <id> |
Reject any agent’s approval |
When /approve or /reject is sent without an ID and there is exactly one pending approval, it targets that approval. If multiple are pending, the bot replies with a numbered list and waits for selection.
For /update-all, the operator gets a progress summary as each agent is processed.
Approval UX
When an agent submits a REQUEST_APPROVAL:
- choird sends a Telegram message to the agent’s bound DM with the proposal summary and an inline keyboard:
[Approve][Reject]. Callback data encodes theapproval_id. - Approve: choird sends “Approved: [summary]. Executing.” and notifies the agent.
- Reject: choird sends “Rejected: [summary].” and notifies the agent.
- Timeout: Unanswered approvals timeout after 30 minutes (configurable). Automatically rejected with notification.
- Multiple pending approvals each get their own message – no batching.
/approveand/rejectcommands work as a fallback for cases where inline buttons do not render.- Admin DMs can approve/reject any agent’s approvals. Regular DMs are scoped to their bound agent.
File Transfer via .choirtmp
File transfer between the gateway and the agent uses the .choirtmp/ staging area inside /workspace:
Inbound (User -> Agent)
- User sends a file, image, or audio message via Telegram.
- choird stores the file to
/workspace/.choirtmp/recv/<filename>. - A
UserMsgis forwarded to the edge lane with the.choirtmp/recv/<filename>path as an attachment reference.
Outbound (Agent -> User)
- Agent writes a file to
/workspace/.choirtmp/send/(e.g., via TTS tool, file generation). - Tool result contains the path reference.
- choird fetches the file from
.choirtmp/send/on the host bind mount. - choird sends as the appropriate Telegram message type:
| Content Type | Telegram Delivery |
|---|---|
| Text | Text message (chunked at 4096 chars) |
| Documents/files | Document upload |
| Audio (OGG Opus) | Voice message |
| Images | Photo message |
| Videos | Video message |
- choird cleans up files from
.choirtmp/send/after successful delivery and from.choirtmp/recv/after the agent acknowledges receipt.
.choirtmp/ has its own independent lock (choirtmp:X), separate from the workspace lock hierarchy. This allows file transfers to proceed without conflicting with workspace-level tool execution.
The /inject Command
/inject <message> provides an alternative to the normal user message queue. Instead of waiting for the edge lane to return to IDLE (which is when the message queue is drained), /inject inserts a message directly into the edge lane’s context at the next safe point.
This is useful for providing additional instructions to the agent while it is actively processing, without waiting for the current task to complete.
Injection is append-only: it never mutates history or resets budgets.
Design Constraints
- One agent per DM. No multiplexing.
- choird rate-limits outbound messages to respect Telegram API limits.
- Long responses are chunked at the 4096-character Telegram message limit.
- The gateway is a thin adapter; all routing logic lives in choird’s control plane.
- No streaming in v1: all outbound messages are sent as complete blocks.