← All Diagrams
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
/chats [path] — List conversations for project dir
/setdir <path> — Set default project directory
/newgroup [title] — Create group chat, bind, start agents
Tap conversation → bind topic → start bridge (group only)
Type message → routes to group store or web API
/gstop — Stop agents (group only)
/gunbind — Unbind topic, stop bridge
Close topic → auto-unbind + stop bridge