8.0 KiB
Telegram Bridge Architecture
Overview
pi-telegram is a session-local pi extension that binds one Telegram DM to one running pi session. The bridge owns four main responsibilities:
- Poll Telegram updates and enforce single-user pairing
- Translate Telegram messages and media into pi inputs
- Stream and deliver pi responses back to Telegram
- Manage Telegram-specific controls such as queue reactions,
/status,/model, and/compact
Runtime Structure
index.ts remains the extension entrypoint and composition layer. Reusable runtime logic is split into flat domain files under /lib rather than into a deep local module tree.
Domain grouping rule: prefer cohesive domain files over atomizing every helper into its own file. A shared domain is allowed only for types or constants that genuinely span multiple bridge domains.
Naming rule: because the repository already scopes this codebase to Telegram, extracted module and test filenames use bare domain names such as api.ts, queue.ts, updates.ts, and queue.test.ts rather than repeating telegram-* in every filename.
Current runtime areas include:
- Telegram API types and local bridge state in
index.ts - Queueing and queue-runtime helpers in
/lib/queue.ts - Reply, preview, preview-finalization, reply-transport, and rendered-message delivery helpers in
/lib/replies.ts - Polling request, stop-condition, and long-poll loop helpers in
/lib/polling.ts - Telegram API/config helpers and lazy bot-token client wrappers in
/lib/api.ts - Telegram turn-building helpers in
/lib/turns.ts - Telegram media/text extraction helpers in
/lib/media.ts - Telegram updates extraction, authorization, flow, execution-planning, direct execute-from-update routing, and runtime helpers in
/lib/updates.ts - Telegram attachment queueing and delivery helpers in
/lib/attachments.ts - Telegram tool, command, and lifecycle-hook registration helpers in
/lib/registration.ts - Setup/token prompt helpers in
/lib/setup.ts - Markdown and Telegram message rendering helpers in
/lib/rendering.ts - Status rendering helpers in
/lib/status.ts - Menu/model-resolution, menu-state construction, pure menu-page derivation, pure menu render-payload builders, menu-message runtime, callback parsing, callback entry handling, callback mutation helpers, full model-callback planning and execution, interface-polished callback effect ports, status-thinking callback handling, and UI helpers in
/lib/menu.ts - Model-switch guard, continuation, and restart helpers in
/lib/model-switch.ts - Telegram API-bound reply transport wiring and broader event-side orchestration in
index.ts - Additional domains can be extracted into
/lib/*.tsas the bridge grows, while keepingindex.tsas the single entrypoint - Mirrored domain regression coverage lives in
/tests/*.test.tsusing the same bare domain naming scheme
Configuration UX
/telegram-setup uses a progressive-enhancement flow for the bot token prompt:
- Show the locally saved token from
~/.pi/agent/telegram.jsonwhen one already exists - Otherwise use the first configured environment variable from the supported Telegram token list
- Fall back to the example placeholder when no real value exists
Because ctx.ui.input() only exposes placeholder text, the bridge uses ctx.ui.editor() whenever a real default value must appear already filled in.
Message And Queue Flow
Inbound Path
- Telegram updates are polled through
getUpdates - The bridge filters to the paired private user
- Media groups are coalesced into a single Telegram turn when needed
- Files are downloaded into
~/.pi/agent/tmp/telegram - A
PendingTelegramTurnis created and queued locally - The queue dispatcher sends the turn into pi only when dispatch is safe
Queue Safety Model
The bridge keeps its own Telegram queue and does not rely only on pi's internal pending-message state.
Queued items now use two explicit dimensions:
kind: prompt vs controlqueueLane: control vs priority vs default
This lets synthetic control actions and Telegram prompts share one stable ordering model while still rendering distinctly in status output. In the pi status bar queue preview, priority prompts are marked with ⬆ while control items keep their own control-specific summary markers such as ⚡.
A dispatched prompt remains in the queue until agent_start consumes it. That keeps the active Telegram turn bound correctly for previews, attachments, abort handling, and final reply delivery.
Dispatch is gated by:
- No active Telegram turn
- No pending Telegram dispatch already sent to pi
- No compaction in progress
ctx.isIdle()being truectx.hasPendingMessages()being false
This prevents queue races around rapid follow-ups, /compact, and mixed local plus Telegram activity.
Abort Behavior
When /stop aborts an active Telegram turn, queued follow-up Telegram messages can be preserved as prior-user history for the next turn. This keeps later Telegram input from being silently dropped after an interrupted run.
Rendering Model
Telegram replies are rendered as Telegram HTML rather than raw Markdown.
Key rules:
- Rich text should render cleanly in Telegram chats
- Real code blocks must remain literal and escaped
- Markdown tables should keep their internal separators but drop the outer left and right borders when rendered as monospace blocks so narrow Telegram clients keep more usable width
- Unordered Markdown lists should render with a monospace
-marker and ordered Markdown lists should render with monospace numeric markers so list indentation stays more predictable on narrow Telegram clients - Nested Markdown quotes should flatten into one Telegram blockquote with added non-breaking-space indentation because Telegram does not render nested blockquotes reliably
- Long replies must be split below Telegram's 4096-character limit
- Chunking should avoid breaking HTML structure where possible
- Preview rendering is intentionally simpler than final rich rendering
The renderer is a Telegram-specific formatter, not a general Markdown engine, so rendering changes should be treated as regression-prone.
Streaming And Delivery
During generation, the bridge streams previews back to Telegram.
Preferred order:
- Try
sendMessageDraft - Fall back to
sendMessagepluseditMessageText - Replace the preview with the final rendered reply when generation ends
Outbound files are sent only after the active Telegram turn completes and must be staged through the telegram_attach tool.
Interactive Controls
The bridge exposes Telegram-side session controls in addition to regular chat forwarding.
Current operator controls include:
/statusfor model, usage, cost, and context visibility, queued as a high-priority control item when needed- Inline status buttons for model and thinking adjustments, applying idle selections immediately while still respecting busy-run restart rules
/modelfor interactive model selection, queued as a high-priority control item when needed and supporting in-flight restart of the active Telegram-owned run on a newly selected model/compactfor Telegram-triggered pi session compaction when the bridge is idle- Queue reactions using
👍and👎
In-Flight Model Switching
When /model is used during an active Telegram-owned run, the bridge can emulate the interactive pi workflow of stopping, switching model, and continuing.
The current implementation does this by:
- Applying the newly selected model immediately
- Queuing or staging a synthetic Telegram continuation turn
- Aborting the active Telegram turn immediately, or delaying the abort until the current tool finishes when a tool call is in flight
- Dispatching the continuation turn after the abort completes
This behavior is intentionally limited to runs currently owned by the Telegram bridge. If pi is busy with non-Telegram work, the bridge still refuses the switch instead of hijacking unrelated session activity.