Telegram
Telegram is Sona's default channel. The adapter long-polls the Bot API, so you don't need a public IP, a tunnel, or a webhook server — Sona pulls updates over an outbound HTTPS connection.
If you ran the Quickstart, you already did most of this. This page covers the same flow in more detail and adds the bits the wizard skips.
Get a bot token
- Open Telegram, search for @BotFather, start a chat.
- Send
/newbot. - Pick a display name — what users will see in the chat header. Anything goes.
- Pick a username — must be globally unique and end in
_bot(e.g.myname_sona_bot). - BotFather replies with the token. It looks like
123456789:ABC-DEF1234ghIkl-zyx57W2v1u123ew11.
Keep the token secret — anyone with it can impersonate your bot.
Optional but useful, still in BotFather:
/setdescription— short blurb users see before the first message/setabouttext— long description on the bot's profile page/setuserpic— bot avatar/setcommands— populate the slash-command menu (see slash commands below)/setjoingroupsand/setprivacy— group behaviour (default: groups disabled, "privacy mode" on so the bot only sees commands and mentions)
Wire it into Sona
Either re-run sona init (which writes the token into ~/.sona/sona.yml for you and verifies it via getMe), or edit the file directly:
# ~/.sona/sona.yml
channels:
telegram:
token: ${TELEGRAM_BOT_TOKEN} # or paste the literal stringThe ${VAR} form makes Sona read from the environment at boot — you keep the token out of the config file but still tie it to the channel config. If you'd rather paste the literal token, that works too.
Restart the daemon:
sona stop
sonaOn a healthy start you should see:
INFO c.t.c.t.TelegramAdapter [] - Telegram adapter started — listening for messagesIf you see Unauthorized in the logs, the token is wrong or revoked — re-issue via BotFather /token and try again.
First message + pairing
Open t.me/<your_bot_username> and send hi.
Sona doesn't trust strangers. The first message from an unknown sender returns a pairing code:
You: hi
Bot: Hi! I don't recognize you yet. Please share this pairing code
with my owner to get access: 483921You are the owner. Approve from another terminal:
sona pair list # lists pending requests
sona pair approve 483921 # by code
# or
sona pair approve telegram:123456789 # by scoped idNow send another message — the bot replies through the LLM.
Pairing is per-(channelId, senderId). Approving telegram:123 does not approve cli:123 or slack:123. This is intentional — channels are tenant boundaries, not just routing labels.
Slash commands
Telegram's slash-command menu helps users discover what a bot can do. Set the menu from BotFather:
/setcommands
@your_bot_username
help - What can this bot do?
new - Start a new conversation
stop - Stop the current taskSona doesn't currently parse slash commands into special behaviours — they flow into the gateway like any other text. If you want a command to do something specific, register a skill whose match() looks for the /help prefix.
Threads, groups, and multi-user chats
By default Telegram bots only see DMs and messages that mention them. To use Sona in a group:
- In BotFather:
/mybots→ pick your bot → Bot Settings → Group Privacy → Turn off. (This lets the bot see every message in the group, not just@mentions.) - Add the bot to the group.
- Anyone in the group can now talk to it — but pairing applies per-user, so you'll need to approve each user that should be allowed to use the LLM budget.
For 1:1 DMs Sona's session boundary is the user's Telegram ID; conversations across days resume from where they left off (until the daemon is restarted with sona uninstall, or you explicitly purge ~/.sona/sessions/).
Media
The adapter accepts inbound photos, documents, and voice notes; they're attached to the UnifiedMessage as Attachment records. What happens next depends on whether your configured LLM is vision-capable:
| Attachment | Vision LLM (Claude Sonnet, GPT-4o, Gemini Vision) | Text-only LLM |
|---|---|---|
| Photo | Encoded inline; passed to the LLM as image input | Logged, not sent to the LLM |
| Document (PDF, txt) | If a tool's wired for it (tnsai-tools PDF / text), the agent can ask for content; otherwise logged | Same |
| Voice note | Not yet wired — tracked in tnsai-tools STT toolkit |
For outbound media, the adapter renders UnifiedResponse.attachments as Telegram media — Sona doesn't ship outbound-media skills today, but the SPI supports it.
Privacy + audit
Sona logs every inbound message + the LLM reply to ~/.sona/sona.log (and the per-session JSON in ~/.sona/sessions/). The Telegram chat metadata (user ID, message ID, timestamps) is in the audit log; the LLM's API call to the provider follows the provider's own retention policy.
If you need a redaction filter on the way in — e.g. strip emails or credit-card numbers before they reach the LLM — see the framework's redaction guide. Wiring it through Sona is one extra config block; no code change.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Unauthorized in logs | Token wrong / revoked | Re-issue via BotFather /token, re-run sona init |
Conflict: terminated by other getUpdates request | Another process is polling the same bot | Stop the other process; only one consumer per token |
| Bot is added to a group but doesn't respond | Group privacy is on | BotFather → bot settings → group privacy → turn off |
| Message reaches Sona logs but no reply | LLM key is invalid, or budget is exhausted | Check sona logs --grep ERROR -i for the provider's response |
| First-message pairing code never arrives | Network drop right after the inbound message | Send again; pairing is per-sender, you'll get a fresh code |
sona pair approve says "no such code" | Daemon restarted between code-issue and approve | Issue a new one by sending a new message |
Removing the bot
To stop using a bot but keep its token alive for future use:
# ~/.sona/sona.yml
channels: {} # empty map = no adapters at bootthen sona stop && sona. The bot exists on Telegram's side until you /deletebot via BotFather.