Skip to content

Gateway Core ​

源码版本v2026.7.20

Responsibility ​

The gateway is the bridge between the agent and the outside world: it receives messages from each platform (Telegram/Discord/Slack/Signal/WeChat...) and converts them into a unified MessageEvent fed to the agent; it turns the agent's streaming output into per-platform send actions. gateway/run.py is the main process of this layer (GatewayRunner), coordinating adapters, sessions, stream consumption, the delivery ledger, hooks, the cron heartbeat, and other subsystems.

Design Motivation ​

Splitting "talking to platforms" from "running the agent loop" means the agent never has to care about Telegram length limits, Discord snowflake IDs, or the Signal signal-cli RPC schema. A unified MessageEvent plus an abstract BasePlatformAdapter lets one agent process host a dozen platforms at once; adding a new platform only touches the adapter. The delivery_ledger sits on top because the gateway is long-lived online and the process can crash at any time — the final reply the agent already produced must not vanish with it, so that responsibility belongs to the gateway, not the agent.

Key Files ​

Data Flow ​

  1. main() (gateway/run.py:22965) starts GatewayRunner.
  2. _start_one_profile_adapter:9389 brings up each platform adapter (see Platform Adapters).
  3. Adapters receive raw platform events and convert them to unified MessageEvents (see gateway/platforms/base.py:1759).
  4. _handle_message (gateway/run.py:9947) dispatches: authz check, session resolution, slash command routing, or handoff to _handle_message_with_agent (gateway/run.py:11956).
  5. The latter calls the agent's run_conversation:588 and wires the streaming callback to GatewayStreamConsumer:83.
  6. Streaming output progressively edits the platform message via the consumer; the final state is recorded by delivery_ledger to prevent loss of the final reply on crash.
  7. Housekeeping and the cron heartbeat run in the background on a 60s cycle (gateway/run.py:22246 / gateway/run.py:22335).

GatewayRunner (gateway/run.py:3029) is a multi-head mixin class:

python
class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, GatewaySlashCommandsMixin):
    """Main gateway controller. Manages the lifecycle of all platform
    adapters and routes messages to/from the agent."""
    _busy_input_mode: str = "interrupt"
    _draining: bool = False
    def __init__(self, config: Optional[GatewayConfig] = None):
        self.adapters: Dict[Platform, BasePlatformAdapter] = {}

Authorization, Kanban watcher, and Slash commands come from three separate mixins; the main class only manages lifecycle and routing — "thin main class + capability mixins" lets each trait be tested in isolation and trimmed as needed.

Inbound messages go through _handle_message. The start of the pipeline runs a few non-skippable steps — reset cross-session ContextVars, detect startup-restore window, gate the scale-to-zero clock, and fire the pre_gateway_dispatch plugin hook:

python
async def _handle_message(self, event: MessageEvent) -> Optional[str]:
    source = event.source
    # Cross-session leak guard: a new task may inherit a sibling session's
    # HERMES_SESSION_* ContextVar, so reset to _UNSET before proceeding.
    try:
        from gateway.session_context import reset_session_vars
        reset_session_vars()
    except Exception:
        logger.debug("reset_session_vars failed at handler entry", exc_info=True)

    if getattr(self, "_startup_restore_in_progress", False) and not getattr(event, "internal", False):
        self._queue_startup_restore_event(event)
        return None

create_task() snapshots the spawning context. If a concurrent message's parent task has called set_session_vars(), the new task inherits the sibling identity, and the subprocess-env bridge would write the wrong session into the environment.

_handle_message_with_agent finds or creates a session, hands the message to the agent, and guards against "illegally reviving a dead session":

python
async def _handle_message_with_agent(self, event, source, _quick_key, run_generation):
    """Inner handler that runs under the _running_agents sentinel guard."""
    session_entry = await self.async_session_store.get_or_create_session(source)
    session_key = session_entry.session_key
    pinned_session_id = str((getattr(event, "metadata", None) or {}).get("gateway_session_id") or "").strip()
    if pinned_session_id and pinned_session_id != session_entry.session_id:
        # Fail closed (#55578): the spawning session may have already /new-reset,
        # don't blindly switch back and revive a session the user closed.
        ...

When an async sub-agent comes back to fill in results, if the spawning session has already been ended, the gateway drops this injection rather than reviving it.

Boundaries and Failure ​

  • Cross-session ContextVar leak: _handle_message runs inside create_task() and inherits the parent task's snapshot. Without a reset_session_vars() call up front, concurrent messages would read a sibling's session identity and the subprocess bridge would write that wrong identity into the environment.
  • Async backfill into a dead session: by the time a sub-agent returns, the spawning session may have already been /new-reset. Blindly switching back would revive a session the user closed on purpose. Fail-closed leaves the result in the delegation records instead.
  • Scale-to-zero clock pollution: internal events (background process completion, startup-restore replay) must not count as traffic, or an idle gateway stays alive forever. The code branches these out explicitly with is_internal.

Summary ​

The gateway core is an "event bus + streaming bridge": the unified MessageEvent abstraction hides platform differences; GatewayStreamConsumer bridges the synchronous agent callback to asynchronous platform delivery; delivery_ledger ensures the final state isn't lost. It is decoupled from the agent — the agent only runs the loop; all delivery details live in the gateway.

Unofficial community learning site. Content based on the MIT-licensed NousResearch/hermes-agent source. · Privacy · Terms · About