Telegram Bot — tmux Session Flow
Each Telegram Forum topic maps to one tmux window running one AI agent session (Claude/Codex/Gemini). The SessionMonitor polls JSONL transcripts every 2 seconds and delivers messages back to the topic. Messages flow through a per-user FIFO queue with automatic merging and MarkdownV2 formatting.
1 Topic = 1 Window = 1 Session Telegram Topic thread_id (e.g. 42) Forum Super Group tmux Window window_id (e.g. @0) session.py · SessionManager Agent Session session_id (uuid) Claude / Codex / Gemini process JSONL Transcript ~/.claude/projects/*/ {session_id}.jsonl thread_bindings session_map.json writes OUTBOUND: User → Agent (via tmux keystrokes) User Message text in topic handlers/text.py Auth + Routing allowed_users check thread_bindings lookup SessionManager resolve window_id session_manager.py Backend Dispatch build CLI command _dispatch_noninteractive() tmux send_keys shell command tmux_manager.py JSONL Output written to file {session}.jsonl INBOUND: Agent → User (via JSONL polling) JSONL File mtime change byte offset tracking SessionMonitor poll every 2s session_monitor.py TranscriptParser JSONL → ParsedEntry transcript_parser.py MessageQueue merge + rate limit handlers/message_queue.py MarkdownV2 format + split 4096 markdown_v2.py Topic safe_send() message_sender.py SessionStart Hook: agent starts → vibex hook (hook.py) → writes session_map.json → SessionMonitor discovers & tracks session
Telegram ↔ Web Chat Bridge
Telegram topics can also bind to web-based conversations (group or single-agent). The text handler checks group_thread_bindings before tmux bindings. Group chats use a persistent bridge task subscribing to GroupChatStore events. Single-agent chats stream the web API’s SSE response per message.
Topic Binding: 1 Topic = 1 Web Conversation (group or single-agent) Telegram Topic thread_id /chats · /newgroup · /setdir group_thread_bindings (user_id, thread_id) → (conv_id, agent_type) session_manager.py · SQLite Web Conversation conversations table (web.db) agent_type: group | claude | codex | gemini Web UI (SSE) Sees all messages Bidirectional with Telegram bind maps to SSE stream text_handler routing: check group_thread_bindings FIRST → if bound, route by agent_type → else fall through to tmux bindings handlers/text.py · group_chat_handler.py · if agent_type == "group" → forward_to_group_chat() · else → forward_to_single_agent() GROUP CHAT FLOW (agent_type = "group") OUTBOUND: Telegram → Agents User Message text in topic forward_to_group_chat() GroupChatStore store.post(sender="user") group_chat_store.py Agent Queues per-agent asyncio.Queue MCP get_new_messages INBOUND: Agents → Telegram (persistent bridge task) Agent posts MCP post_message group_mcp_server.py Store broadcast _broadcast_sse() subscriber queues _bridge_task q.get() → safe_send group_chat_handler.py Bridge Task Lifecycle start_bridge() on bind · stop_bridge() on unbind/close · resume_bridges() on bot restart store.subscribe() returns asyncio.Queue · same queue feeds Web UI SSE · filter sender != "user" Web UI sees everything (bidirectional) Telegram user msg → store.post() → SSE broadcast → Web UI updates live Web user msg → API POST → store.post() → bridge_task → Telegram topic SINGLE-AGENT FLOW (agent_type = claude | codex | gemini) PER-MESSAGE: Send → Stream → Deliver (no persistent bridge) User Message text in topic forward_to_single_agent() httpx POST localhost:{port}/api/ conversations/{id}/messages AgentExecutor run_agent() subprocess agent_executor.py safe_send() to Telegram topic message_sender.py Accumulate text text_delta events send on "completed" SSE Response text_delta stream aiter_lines() Key Difference: No Persistent Bridge Each message is a request-response cycle via httpx SSE streaming. Web UI also sees messages — SSE stream broadcasts to both Telegram & browser. SSE Events Consumed by Bridge text_delta: {text: "..."} → accumulated into response string completed: {usage: {...}} → trigger safe_send() with accumulated text error: {message: "..."} → send error to Telegram topic Web UI sees everything (bidirectional) Same SSE stream drives the web frontend & persists messages to web.db User can continue conversation from either Telegram or Web UI
Module Reference

Telegram Handler Modules

  • handlers/text.py — Routes text: group_thread_bindings → tmux bindings
  • handlers/group_chat_handler.py — /chats, /newgroup, /setdir, bridge task
  • handlers/commands.py — /screenshot, /history, /resume, /stop
  • handlers/topic.py — Topic close: unbind + stop bridge
  • handlers/callbacks/__init__.py — Callback dispatcher (gc: prefix)
  • handlers/status_polling.py — 1s terminal status poll
  • handlers/interactive_ui.py — AskUserQuestion, Permission buttons
  • handlers/directory_browser.py — Directory picker for new topics
  • handlers/voice_handler.py — Voice → STT → text

Web Chat Bridge Components

  • group_chat_handler.py — chats_command, newgroup, setdir, gstop, gunbind
  • forward_to_group_chat() — Direct GroupChatStore.post()
  • forward_to_single_agent() — httpx SSE to web API
  • _bridge_task() — Persistent subscriber for group events
  • start_bridge() / stop_bridge() — Task lifecycle
  • resume_bridges() — Restart on bot startup
  • _ensure_orchestrator() — Auto-start group agents
  • handle_gc_callback() — gc:b: and gc:p: callbacks

State & Persistence

  • group_thread_bindings table — (user_id, thread_id) → (conv_id, agent_type)
  • thread_bindings table — (user_id, thread_id) → window_id (tmux)
  • agent_defaults table — per-user default backend, model, cwd
  • conversations table — web.db: id, title, agent_type, project_dir
  • Both tables in vibex.db (bot DB), loaded into memory on startup
  • Web conversations in web.db, queried on demand

Commands & Flows

  1. /chats [path] — List conversations for project dir
  2. /setdir <path> — Set default project directory
  3. /newgroup [title] — Create group chat, bind, start agents
  4. Tap conversation → bind topic → start bridge (group only)
  5. Type message → routes to group store or web API
  6. /gstop — Stop agents (group only)
  7. /gunbind — Unbind topic, stop bridge
  8. Close topic → auto-unbind + stop bridge