Rho includes a polling-first Telegram adapter (extensions/telegram) that maps Telegram chats to stable rho sessions.
- Polls Telegram
getUpdateswith durable offset state - Normalizes inbound messages and applies authz gates
- Maps chat -> session deterministically (
dm:<chat_id>,group:<chat_id>) - Runs prompts through rho RPC (
pi --mode rpc) - Transcribes inbound
voice/audio/ audio-document messages via ElevenLabs STT - Supports
/tts <text>to generate and return Telegram voice replies via ElevenLabs TTS - Sends responses back with chunking + retry/backoff
- Exposes operator controls via tool +
/telegramcommand - Follows shared slash RPC contract (
docs/slash-command-contract.md) for classification, execution, and errors
In ~/.rho/init.toml:
[modules.tools]
telegram = true
[settings.telegram]
enabled = true
mode = "polling"
bot_token_env = "TELEGRAM_BOT_TOKEN"
poll_timeout_seconds = 30
rpc_prompt_timeout_seconds = 60
allowed_chat_ids = []
allowed_user_ids = []
require_mention_in_groups = trueSet your token:
export TELEGRAM_BOT_TOKEN="<your-bot-token>"Apply config:
rho syncSet an ElevenLabs API key for voice features:
export ELEVENLABS_API_KEY="<your-elevenlabs-key>"Optional TTS voice override (default is ElevenLabs voice EXAVITQu4vr4xnSDxMaL):
export ELEVENLABS_TTS_VOICE_ID="<voice-id>"
# or legacy alias:
export ELEVENLABS_VOICE_ID="<voice-id>"Behavior:
- Inbound
voice,audio, anddocumentmessages withaudio/*MIME type are transcribed, and that transcript is used as the prompt to rho (reply contains the assistant result, not just raw transcript). /tts <text>generates a playable Telegram voice reply.- Missing key or API failures return actionable in-chat error text instead of crashing the worker.
Telegram polling is owned by the dedicated worker process.
rho telegram start
rho telegram status
rho telegram logs -f
rho telegram stopUse rho start for heartbeat/session daemon lifecycle; use rho telegram ... for Telegram transport lifecycle.
rho telegram onboardThis validates the token, detects chat/user, and can lock allowlists.
When strict allowlists block a sender, they receive a PIN. Operator flow:
rho telegram pending
rho telegram approve --pin 123456
rho telegram reject --pin 123456Slash command:
/telegram status
/telegram check
/telegram allow-chat <chat_id>
/telegram revoke-chat <chat_id>
/telegram allow-user <user_id>
/telegram revoke-user <user_id>
/jobs
/job <job_id>
/cancel <job_id>
Tool action interface (telegram tool):
statuschecksend(chat_id,text, optionalreply_to_message_id)allow/revoke(target=chat|user,id)list_chats
Telegram uses a foreground soft timeout (rpc_prompt_timeout_seconds) to detect long-running work without hard-failing it.
If a prompt hits that timeout, the worker:
- forks the work into a durable job (
/jobs), - rotates the chat onto a fresh main session so follow-up messages stay responsive,
- runs the forked job without a hard timeout,
- posts final completion/failure back in-thread.
Use:
/jobsto list recent jobs/job <job_id>to inspect one job/cancel <job_id>to stop a running job
Tune with:
rpc_prompt_timeout_seconds(default60)
- Default-off transport (
settings.telegram.enabled = false) - Optional allowlists (
allowed_chat_ids,allowed_user_ids) - Group activation gate (
require_mention_in_groups) - Runtime operator allow/revoke persistence in
~/.rho/telegram/config.json
~/.rho/telegram/state.json(poll offset + health)~/.rho/telegram/session-map.json(chat/session mapping)~/.rho/telegram/log.jsonl(audit events)~/.rho/telegram/config.json(runtime allow/revoke overrides)~/.rho/telegram/inbound.queue.json(durable inbound queue)~/.rho/telegram/outbound.queue.json(durable outbound queue)~/.rho/telegram/jobs.json(durable job records: queued/running/completed/failed/cancelled)~/.rho/telegram/background.queue.json(legacy deferred queue, auto-migrated when present)~/.rho/telegram/pending-approvals.json(approval requests)
Run local smoke harness (real Telegram API):
export TELEGRAM_BOT_TOKEN="<token>"
export TELEGRAM_SMOKE_CHAT_ID="<chat-id>"
npm run telegram:smokeExpected output:
sent message_id=... chat_id=...fetched updates=...
- Polling mode only (webhook deferred)
- Text-first outbound rendering (except
/ttsvoice replies) - STT/TTS depend on ElevenLabs availability and API quotas
/ttscurrently uses a single configured/default ElevenLabs voice- Operator controls optimized for single-agent runtime