LifeOSAI — Channels Architecture · sequential view
Two provider subsystems (Baileys WhatsApp, grammy Telegram) sit above a shared channels/ layer. Inbound paths converge on runAgentDispatch(); outbound goes through mcp__channels__message → action-runner → plugin → provider.
CHANNEL NETWORKS
WhatsApp Network
multi-device protocol · reached via Baileys WS
QR / pairing-code login as a real account
Telegram Bot API
official Bot API · reached via grammy
long-poll updates · bot token auth · /commands
WHATSAPP SUBSYSTEM · apps/api/src/whatsapp/
socket.ts Baileys WS · reconnect 30-min watchdog
pairing.ts pairing code · isPaired multi-file auth state
pipeline.ts parse · group meta debounce 500 ms
dispatch.ts per-session queue → runAgentDispatch
delivery.ts StreamSink · buffered typing indicator
lid + contacts JID/LID ↔ phone + directory lookup
also: contacts · group-history · reactions · receipt-tracker · media · permissions · channel-router
TELEGRAM SUBSYSTEM · apps/api/src/telegram/
bot.ts grammy Bot (singleton) bot-token auth
polling.ts long-poll runner no webhook needed
pipeline.ts /commands + msg debounce 500 ms
bot-message-dispatch.ts per-session queue → runAgentDispatch
streaming/ StreamSink · edit-in-place throttled editMessageText
agent-selector /agents picker agent-router routes
also: bot-message · group-history · tools · permissions · lifecycle · routes
SHARED CHANNELS LAYER · apps/api/src/channels/
message-debounce.ts 500 ms coalesce per chat used by both pipelines
session-store.ts SessionEntry · DmScope main · per-peer · per-ch.
identity-links + chat-key merges WA + TG by name buildAgentChatKey()
registry.ts plugin + alias map "wa"→whatsapp, "tg"→tg
system-prompt.ts buildChannelSystemPrompt + user context prefix
dispatch.runAgentDispatch() the harness invocation · used by BOTH creates streaming session · token-pool rotation
plugins/whatsapp.ts ChannelPlugin impl (WA) delegates to whatsapp/
plugins/telegram.ts ChannelPlugin impl (TG) delegates to telegram/
mcp__channels__message + action-runner unified MCP tool · send/reply/react/poll/… 30 msgs/min/target · strips <think>
RUNTIME + OUTBOUND PATH
Claude Code SDK harness
createStreamingSession · SDKMessage
• content_block_delta → sink.append
• result → capture session_id
• rate-limit → rotate token (5 retries)
StreamSink (transport-specific)
two implementations of one interface
WhatsApp delivery:
buffered send-once + typing indicator
Telegram StreamEngine:
edit-in-place via throttled editMessageText
Outbound (agent reply)
when agent calls mcp__channels__message
1. message-tool: rate-limit + clean
2. action-runner: resolve channel + target
3. ChannelPlugin.send / react / …
4. provider delivers · messageId returned
PERSISTENCE
channel sessions ~/.lifeosai/sessions/
identity-links.json cross-channel merging
Baileys auth multi-file creds + keys
media uploads GCS / filestore
LID cache JID ↔ phone
voice transcripts ElevenLabs (audio in)
group history recent N per chat
runs + events heartbeatRun · logs
STATE THE CHANNELS LAYER WRITES
Session JSON + identity links are channel-layer state.
Baileys auth + LID cache are WhatsApp-specific.
Media + transcripts + run-events shared with orchestration.
inbound
inbound
→ runAgentDispatch
→ runAgentDispatch
createStreamingSession()
agent → MCP tool