How AstrBot Handles Multi-Platform Message Sessions with the UMO System

AstrBot unifies conversations across Telegram, Discord, Slack, Kook, and other platforms using a Unified Message Origin (UMO) string as the single source of truth for session identity, enabling isolated, configurable multi-platform messaging.

The AstrBot repository (AstrBotDevs/AstrBot) implements a Unified Message Origin (UMO) system to treat every conversation as a single logical session regardless of whether it originates from Telegram, Discord, or any other supported platform. This architecture allows a single AstrBot instance to simultaneously serve multiple messaging platforms while maintaining strict session isolation and per-session configuration.

Understanding the UMO Format

Every UMO follows a strict canonical form that encodes the platform, message type, and session identifier into a single string:


<platform_id>:<message_type>:<session_id>

  • platform_id – The unique identifier for the platform adapter (e.g., telegram, discord, slack).
  • message_type – The conversation context (group, private, friend).
  • session_id – A platform-specific identifier for the chat (group ID, user ID, or channel ID).

This format ensures that a Telegram private chat with user 12345 (telegram:private:12345) never collides with a Discord group channel 987654321 (discord:group:987654321).

How UMOs Are Created and Assigned

When a platform adapter receives a message, it constructs an AstrMessageEvent containing a MessageSession object. The UMO is generated automatically through the unified_msg_origin property in astrbot/core/platform/astr_message_event.py:

@property
def unified_msg_origin(self) -> str:
    """统一的消息来源字符串。格式为 platform_name:message_type:session_id"""
    return str(self.session)

The corresponding setter reconstructs the session from a UMO string:

@unified_msg_origin.setter
def unified_msg_origin(self, value: str) -> None:
    self.new_session = MessageSession.from_str(value)
    self.session = self.new_session

This bidirectional conversion ensures that every event carries a canonical identifier that any component can use for routing or storage.

Session Storage and Database Indexing

The conversation persistence layer uses the UMO as the primary key. In astrbot/dashboard/routes/session_management.py, the database queries distinct user_id values—which are stored as UMO strings—to list all active sessions:

result = await session.execute(
    select(ConversationV2.user_id).distinct().order_by(ConversationV2.user_id)
)
umos = [row[0] for row in result.fetchall()]

All subsequent operations—history retrieval, deletion, and status checks—filter by this UMO column, ensuring complete isolation between conversations from different platforms.

Configuration Routing with UMOP

AstrBot supports per-session configuration overrides through the UMOP (UMO-to-Config-File) router. The UmopConfigRouter class in astrbot/core/umop_config_router.py maintains a mapping of UMO patterns to configuration file IDs:

class UmopConfigRouter:
    def __init__(self, sp: SharedPreferences) -> None:
        self.umop_to_conf_id: dict[str, str] = {}
        self.sp = sp

Pattern matching supports wildcards using fnmatch. The _is_umo_match method determines if a stored pattern covers a specific UMO:

def _is_umo_match(self, p1: str, p2: str) -> bool:
    # p2 is logically contained in p1 (wildcards allowed)

    return all(p == "" or fnmatch.fnmatchcase(t, p) for p, t in zip(p1_ls, p2_ls))

When processing a message, get_conf_id_for_umop() traverses the routing table to return the appropriate configuration, enabling global defaults, per-platform rules, or specific session overrides.

Managing Active Event Lifecycles

To prevent resource leaks and handle session resets, AstrBot tracks running events in active_event_registry (astrbot/core/utils/active_event_registry.py). Each event registers using its UMO as the lookup key:

def register(self, event: AstrMessageEvent) -> None:
    self._events[event.unified_msg_origin].add(event)

When a session requires termination—such as during a reset command—the registry stops all active runners for that specific UMO:

def stop_all(self, umo: str, exclude: AstrMessageEvent | None = None) -> int:
    for event in list(self._events.get(umo, [])):
        if event is not exclude:
            event.stop_event()

This ensures that long-running LLM requests or plugin tasks do not persist after a session has been invalidated.

End-to-End Message Processing Flow

The UMO system orchestrates the entire message lifecycle from reception to response:

  1. Platform Adapter receives a raw message and constructs an AstrMessageEvent with a MessageSession.
  2. UMO Generation occurs automatically via unified_msg_origin, producing the canonical <platform>:<type>:<id> format.
  3. Event Registration adds the event to active_event_registry under its UMO key.
  4. Configuration Lookup queries UmopConfigRouter to determine which configuration file applies to this specific UMO (supporting wildcards).
  5. Pipeline Execution runs the processing stages—whitelist checks, provider selection, LLM invocation—all filtered by the UMO.
  6. Response Routing sends the reply back through the same platform adapter, using the original UMO to ensure correct destination.

This unified flow allows AstrBot to maintain distinct conversation contexts across dozens of platform adapters while sharing the same core logic.

Practical Code Examples

Manually Constructing a UMO

When developing custom adapters or plugins, you can generate UMO strings programmatically:

from astrbot.core.platform.message_session import MessageSession

# platform "telegram", private chat, user id "12345"

umo = MessageSession(platform_id="telegram",
                    message_type="private",
                    session_id="12345").to_str()
print(umo)   # → telegram:private:12345

Retrieving Configuration for a Session

Query the UMOP router to determine which configuration file governs a specific conversation:

conf_id = await core_lifecycle.umop_config_router.get_conf_id_for_umop(
    "discord:group:987654321"
)
print(conf_id)   # e.g. "default_config.yml"

Terminating Active Session Runners

Force-stop all processing events for a specific UMO during session reset:

from astrbot.core.utils.active_event_registry import active_event_registry

stopped = active_event_registry.stop_all("slack:group:C01ABCD2EFG")
print(f"Terminated {stopped} active runners")

Updating UMOP Mappings via Dashboard API

Configure per-platform routing rules through the REST API:

curl -X POST http://localhost:3000/api/umop/update \
     -H "Content-Type: application/json" \
     -d '{
           "umo": "telegram:private:*",
           "conf_id": "my_telegram_private.yml"
         }'

Summary

  • AstrBot uses a Unified Message Origin (UMO) string in the format <platform_id>:<message_type>:<session_id> to uniquely identify every conversation across all supported platforms.
  • The unified_msg_origin property in astrbot/core/platform/astr_message_event.py automatically generates UMOs from MessageSession objects, ensuring consistent identification.
  • All conversation storage in ConversationV2 tables indexes sessions by UMO, enabling isolated context management across Telegram, Discord, Slack, and other adapters.
  • The UMOP router (astrbot/core/umop_config_router.py) supports wildcard pattern matching to map UMOs to specific configuration files, allowing per-platform or per-session behavior overrides.
  • The active_event_registry tracks running processes by UMO, enabling precise termination of active runners when sessions reset or close.

Frequently Asked Questions

How does AstrBot prevent session collisions between different messaging platforms?

AstrBot prevents collisions by encoding the platform identifier into the first segment of every UMO string. Because the canonical format requires <platform_id>:<message_type>:<session_id>, a Telegram private chat (telegram:private:12345) and a Discord group channel (discord:group:987654321) produce distinct keys even if the numeric IDs coincidentally match. All database tables and in-memory registries use this full string as the primary lookup key.

Can I configure different AI providers for different platforms using the UMO system?

Yes. The UMOP (UMO-to-Config-File) router in astrbot/core/umop_config_router.py supports wildcard patterns that allow you to map entire platforms or specific session types to different configuration files. For example, setting the UMO pattern telegram:* to telegram_config.yml and discord:* to discord_config.yml routes platform traffic to distinct provider pipelines. The _is_umo_match method uses fnmatch.fnmatchcase to evaluate these patterns at runtime.

What happens to active conversations when a session is reset?

When a session reset occurs, AstrBot invokes active_event_registry.stop_all(umo) from astrbot/core/utils/active_event_registry.py. This method iterates through all registered AstrMessageEvent instances matching the specified UMO and calls stop_event() on each runner, immediately terminating any pending LLM requests or long-running plugin tasks. This ensures that stale processing contexts do not interfere with the new session state.

Where is the UMO string actually generated when a message arrives?

The UMO is generated in astrbot/core/platform/astr_message_event.py within the unified_msg_origin property getter. When a platform adapter constructs an AstrMessageEvent, it populates the session property with a MessageSession object. The getter converts this object to its string representation using str(self.session), which produces the canonical <platform_id>:<message_type>:<session_id> format. The corresponding setter uses MessageSession.from_str(value) to reconstruct the session object from a UMO string.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →