A bot that monitors multiple chats simultaneously has a fundamental UX issue: context fragmentation. If someone is having a deep conversation in a DM while group messages arrive, the bot's attention splits. Claude sees interleaved messages from different conversations, leading to confused responses, topic mixing, and wasted context window space.
Focus mode solves this by implementing single-chat attention — the bot concentrates on one conversation at a time, queueing messages from all other chats for later processing.
Focus mode is controlled by a focus_enabled: true config flag and backed by two SQLite tables:
focus_state— A singleton table (enforced byCHECK (id = 1)) that holds the currently focusedchat_id, or NULL for "process all chats"focus_chats— Per-chat cursor tracking. Each row stores acursor_message_id(the last message the bot has "seen" in that chat), plus debounce metadata
When focus is active, the message handling flow changes. Every message is always saved to the database first (via add_message) — nothing is ever lost. The focus decision only controls whether the message also gets forwarded to Claude for processing:
Message arrives → saved to DB (always)
|
v
Is sender Tier 0? (Owner/Queen)
|
+-- YES --> Forward to Claude (Tier 0 bypasses focus)
+-- NO --> Is this the focused chat?
|
+-- YES --> Forward to Claude, update cursor
+-- NO --> Don't forward (queued for later)
Tier 0 bypass is checked in both DM and group message handlers via TIER0_USER_IDS.contains(). This ensures the owner and Queen can always reach the bot regardless of focus state.
Each chat tracked in focus_chats has a cursor_message_id — the last message_id the bot has processed. Messages with IDs above the cursor are "pending." When the bot switches focus to a chat, it:
- Updates the cursor for the old focused chat to its latest message (marking everything as read)
- Queries pending messages for the new focused chat (everything above its cursor)
- Returns those pending messages in XML format as part of the
switch_focustool response - Updates the new chat's cursor to the max returned message_id
The pending message query uses a window function for atomic counting:
WITH all_pending AS (
SELECT *, COUNT(*) OVER() as total_pending
FROM messages WHERE chat_id = ? AND message_id > ?
)
SELECT * FROM (
SELECT * FROM all_pending ORDER BY timestamp DESC LIMIT ?
) ORDER BY timestamp ASCThis returns the N most recent pending messages while also reporting the total count — so the bot knows "showing 50 of 127 pending messages."
To prevent the bot from repeatedly re-injecting the same queued messages, focus_chats tracks:
last_injected_at— timestamp of last injectionlast_pending_count— message count at last injection
Injection only triggers when either 60 seconds have elapsed OR new messages have arrived (content changed). This prevents wasted context window space from redundant notifications.
A subtle problem: when focus is active and other chats have queued messages, the bot might try to stop (go idle) without addressing them. The engine intercepts this:
When the bot sends a stop control action and there are queued messages, the engine rejects the stop up to 3 times, injecting a warning: "You have queued messages in these chats. Consider switching focus or checking them." After 3 rejections, the stop is allowed through to prevent infinite loops.
A similar mechanism handles idle sleep cycles — after 5 consecutive idle sleeps with queued messages, the engine alerts the bot.
Focus mode exposes five MCP tools:
switch_focus— Set focus to a chat (by ID or alias), or clear focus. Returns pending messages as XMLget_focus_state— Show current focus and list all queued chats with pending countspeek_chat— Read recent messages from a chat without switching focuslist_queued_chats— List all non-focused chats that have pending messagesset_chat_alias/remove_chat_alias— Map human-friendly names (like "bot_xona") to chat IDs
Chat aliases make focus switching ergonomic — switch_focus(name: "bot_xona") instead of switch_focus(chat_id: -1003648834056).
Focus and muting are related but distinct:
- Focus mode queues messages for later — the bot will eventually process them when focus shifts
- Mute (
mute_chat/mute_dms) silences a chat for a specified duration — messages are saved to the DB but not injected into context
Both save messages to the database (so nothing is lost), but muted messages don't trigger the queued-message warnings that focus-queued messages do.
Muting is always time-bounded: mute_chat(chat_id, duration_minutes) sets a muted_until timestamp. The health monitor auto-unmutes when the time expires. The bot can also manually unmute early via unmute_chat / unmute_dms. While muted, stats accumulate (messages received, unique users) and are reported in a summary when unmuting — so the bot knows what it missed.