# NousResearch/hermes-agent · Gateway multiplex Telegram responses routed through wrong bot

## Open now

- Nothing open on GitHub.

The members concern the gateway's multiplex mode, where Telegram DM responses may be routed through the wrong bot or adapter, with one bug report citing a chat_id collision. Two items are fix-oriented changes (one described as a salvage of #56854) aimed at keeping responses on the owning profile's bot; their outcomes are not stated in the titles.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #59310. Verify it addresses your case.
- **Recommended:** read PR #59310 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #49634: bug(gateway): multiplex Telegram DM — responses route through wrong bot due to chat_id collision

## Prior & competing PRs

- **#59310: fix(gateway): route multiplex profile responses through the profile's own adapter (#56854 salvage)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `gateway/authz_mixin.py`, `gateway/run.py`, `scripts/release.py`, `tests/gateway/test_notice_rendering.py`, `tests/gateway/test_queue_consumption.py`
  - fix hunks:
  - `gateway/authz_mixin.py`

    ```diff
    @@ -47,10 +47,14 @@ def _authorization_adapter(
             if not platform:
                 return None
             profile_name = (profile or "").strip() or None
    -        if profile_name:
    +        if profile_name and profile_name != "default":
                 profile_adapters = getattr(self, "_profile_adapters", None) or {}
                 if profile_name in profile_adapters:
                     return profile_adapters[profile_name].get(platform)
    +            # Fail closed: a stamped secondary profile with no registry entry
    +            # (e.g. its adapter failed to connect) must NOT fall back to the
    +            # default profile's adapter — that sends replies out the wrong bot.
    +            return None
             adapters = getattr(self, "adapters", None) or {}
             return adapters.get(platform)
     
    ```
  - `gateway/run.py`

    ```diff
    @@ -5201,7 +5201,7 @@ async def _handle_active_session_busy_message(self, event: MessageEvent, session
                             "Approval response via plain text: session=%s verb=%s args=%r",
                             session_key, _verb, _normalized_args,
                         )
    -                    _adapter = self.adapters.get(event.source.platform)
    +                    _adapter = self._adapter_for_source(event.source)
                         if _adapter and _reply:
                             _text, _eph_ttl = _adapter._unwrap_ephemeral(_reply)
                             if _text:
    ```

    ```diff
    @@ -6318,7 +6318,7 @@ async def _drain_startup_restore_queue(self) -> int:
             while queue:
                 event = queue.pop(0)
                 source = getattr(event, "source", None)
    -            adapter = self.adapters.get(source.platform) if source is not None else None
    +            adapter = self._adapter_for_source(source)
                 if adapter is None:
                     logger.debug(
                         "Dropping startup-restore queued message: adapter unavailable for %s",
    ```

    ```diff
    @@ -6424,7 +6424,7 @@ def _schedule_resume_pending_sessions(self, platform=None) -> int:
                     continue
     
                 source = entry.origin
    -            adapter = self.adapters.get(source.platform)
    +            adapter = self._adapter_for_source(source)
                 if adapter is None:
                     logger.debug(
                         "Skipping auto-resume for %s: adapter not ready for %s",
    ```

    ```diff
    @@ -8595,7 +8595,7 @@ def check(
     
         async def _deliver_platform_notice(self, source, content: str) -> None:
             """Deliver a setup/operational notice using platform-specific privacy rules."""
    -        adapter = self.adapters.get(source.platform)
    +        adapter = self._adapter_for_source(source)
             if not adapter:
                 return
     
    ```

    ```diff
    @@ -9070,7 +9070,7 @@ async def _handle_message(self, event: MessageEvent) -> Optional[str]:
                     has_media = bool(getattr(event, "media_urls", None))
                     if not queued_text and not has_media:
                         return "Usage: /queue <prompt>"
    -                adapter = self.adapters.get(source.platform)
    +                adapter = self._adapter_for_source(source)
                     if adapter:
                         queued_event = MessageEvent(
                             text=queued_text,
    ```

    ```diff
    @@ -9091,7 +9091,7 @@ async def _handle_message(self, event: MessageEvent) -> Optional[str]:
                             timestamp=event.timestamp,
                         )
                         self._enqueue_fifo(_quick_key, queued_event, adapter)
    -                depth = self._queue_depth(_quick_key, adapter=self.adapters.get(source.platform))
    +                depth = self._queue_depth(_quick_key, adapter=self._adapter_for_source(source))
                     if depth <= 1:
                         return "Queued for the next turn."
                     return f"Queued for the next turn. ({depth} queued)"
    ```

    ```diff
    @@ -9108,7 +9108,7 @@ async def _handle_message(self, event: MessageEvent) -> Optional[str]:
                     running_agent = self._running_agents.get(_quick_key)
                     if running_agent is _AGENT_PENDING_SENTINEL:
                         # Agent hasn't started yet — queue as turn-boundary fallback.
    -                    adapter = self.adapters.get(source.platform)
    +                    adapter = self._adapter_for_source(source)
                         if adapter:
                             queued_event = MessageEvent(
                                 text=steer_text,
    ```

    ```diff
    @@ -9130,7 +9130,7 @@ async def _handle_message(self, event: MessageEvent) -> Optional[str]:
                             return f"⏩ Steer queued — arrives after the next tool call: '{preview}'"
                         return "Steer rejected (empty payload)."
                     # Running agent is missing or lacks steer() — fall back to queue.
    -                adapter = self.adapters.get(source.platform)
    +                adapter = self._adapter_for_source(source)
                     if adapter:
                         queued_event = MessageEvent(
                             text=steer_text,
    ```

    ```diff
    @@ -9255,7 +9255,7 @@ async def _handle_message(self, event: MessageEvent) -> Optional[str]:
     
                 if event.message_type == MessageType.PHOTO:
                     logger.debug("PRIORITY photo follow-up for session %s — queueing without interrupt", _quick_key)
    -                adapter = self.adapters.get(source.platform)
    +                adapter = self._adapter_for_source(source)
                     if adapter:
                         merge_pending_message_event(adapter._pending_messages, _quick_key, event)
                     return None
    ```

    ```diff
    @@ -9276,7 +9276,7 @@ async def _handle_message(self, event: MessageEvent) -> Optional[str]:
                         time.time() - _started_at,
                         _quick_key,
                     )
    -                adapter = self.adapters.get(source.platform)
    +                adapter = self._adapter_for_source(source)
                     if adapter:
                         if self._busy_input_mode == "queue":
                             self._enqueue_fifo(_quick_key, event, adapter)
    ```

    ```diff
    @@ -9299,7 +9299,7 @@ async def _handle_message(self, event: MessageEvent) -> Optional[str]:
                         return EphemeralReply("⚡ Force-stopped. The agent was still starting — session unlocked.")
                     # Queue the message so it will be picked up after the
                     # agent starts.
    -                adapter = self.adapters.get(source.platform)
    +                adapter = self._adapter_for_source(source)
                     if adapter:
                         merge_pending_message_event(
                             adapter._pending_messages,
    ```

    ```diff
    @@ -9538,7 +9538,7 @@ async def _do_reset():
                     else "Learning a skill from this conversation…"
                 )
                 try:
    -                adapter = self.adapters.get(source.platform)
    +                adapter = self._adapter_for_source(source)
                     if adapter:
                         _ack_meta = self._thread_metadata_for_source(source)
                         await adapter.send(str(source.chat_id), _ack, metadata=_ack_meta)
    ```

    ```diff
    @@ -9592,7 +9592,7 @@ async def _do_reset():
                     _ack = getattr(_blueprint_result, "text", "") or ""
                     if _ack:
                         try:
    -                        adapter = self.adapters.get(source.platform)
    +                        adapter = self._adapter_for_source(source)
                             if adapter:
                                 _ack_meta = self._thread_metadata_for_source(source)
                                 await adapter.send(str(source.chat_id), _ack, metadata=_ack_meta)
    ```

    ```diff
    @@ -10219,7 +10219,7 @@ async def _prepare_inbound_message_text(
                     # while allowing quiet STT for users who only want the agent to
                     # receive the transcription.
                     if _successful_transcripts and self._should_echo_stt_transcripts():
    -                    _echo_adapter = self.adapters.get(source.platform)
    +                    _echo_adapter = self._adapter_for_source(source)
                         _echo_meta = self._thread_metadata_for_source(source, self._reply_anchor_for_event(event))
                         if _echo_adapter:
                             for _tx in _successful_transcripts:
    ```

    ```diff
    @@ -10393,7 +10393,7 @@ async def _prepare_inbound_message_text(
                         allowed_root=_msg_cwd,
                     )
                     if _ctx_result.blocked:
    -                    _adapter = self.adapters.get(source.platform)
    +                    _adapter = self._adapter_for_source(source)
                         if _adapter:
                             await _adapter.send(
                                 source.chat_id,
    ```

    ```diff
    @@ -10634,7 +10634,7 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g
                         and platform_name not in policy.notify_exclude_platforms
                     )
                     if should_notify:
    -                    adapter = self.adapters.get(source.platform)
    +                    adapter = self._adapter_for_source(source)
                         if adapter:
                             if reset_reason == "suspended":
                                 reason_text = "previous session was stopped or interrupted"
    ```

    ```diff
    @@ -11029,7 +11029,7 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g
                                                 "configuration."
                                             )
                                             try:
    -                                            _adapter = self.adapters.get(source.platform)
    +                                            _adapter = self._adapter_for_source(source)
                                                 if _adapter and source.chat_id:
                                                     await _adapter.send(source.chat_id, _warn_msg, metadata=_hyg_meta)
                                             except Exception as _werr:
    ```

    ```diff
    @@ -11053,7 +11053,7 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g
                                                 "check `auxiliary.compression.model` in config.yaml."
                                             )
                                             try:
    -                                            _adapter = self.adapters.get(source.platform)
    +                                            _adapter = self._adapter_for_source(source)
                                                 if _adapter and source.chat_id:
                                                     await _adapter.send(source.chat_id, _aux_msg, metadata=_hyg_meta)
                                             except Exception as _werr:
    ```

    ```diff
    @@ -11210,7 +11210,7 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g
             # event so deferred post-delivery callbacks can be released by the
             # same run that registered them.
             self._bind_adapter_run_generation(
    -            self.adapters.get(source.platform),
    +            self._adapter_for_source(source),
                 session_key,
                 run_generation,
             )
    ```

    ```diff
    @@ -11250,7 +11250,7 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g
     
                 # Stop persistent typing indicator now that the agent is done
                 try:
    -                _typing_adapter = self.adapters.get(source.platform)
    +                _typing_adapter = self._adapter_for_source(source)
                     if _typing_adapter and hasattr(_typing_adapter, "stop_typing"):
                         await _typing_adapter.stop_typing(source.chat_id)
                 except Exception:
    ```

    ```diff
    @@ -11262,7 +11262,7 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g
                         _quick_key or "?",
                         run_generation,
                     )
    -                _stale_adapter = self.adapters.get(source.platform)
    +                _stale_adapter = self._adapter_for_source(source)
                     if getattr(type(_stale_adapter), "pop_post_delivery_callback", None) is not None:
                         _stale_adapter.pop_post_delivery_callback(
                             _quick_key,
    ```

    ```diff
    @@ -11772,7 +11772,7 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g
                 # users see the agent "stop responding without explanation."
                 if agent_result.get("already_sent") and not agent_result.get("failed"):
                     if response:
    -                    _media_adapter = self.adapters.get(source.platform)
    +                    _media_adapter = self._adapter_for_source(source)
                         if _media_adapter:
                             await self._deliver_media_from_response(
                                 response, event, _media_adapter,
    ```

    ```diff
    @@ -11783,7 +11783,7 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g
                     # still surface the runtime metadata on the final reply.
                     if _footer_line:
                         try:
    -                        _foot_adapter = self.adapters.get(source.platform)
    +                        _foot_adapter = self._adapter_for_source(source)
                             if _foot_adapter:
                                 await _foot_adapter.send(
                                     source.chat_id,
    ```

    ```diff
    @@ -11799,7 +11799,7 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g
             except Exception as e:
                 # Stop typing indicator on error too
                 try:
    -                _err_adapter = self.adapters.get(source.platform)
    +                _err_adapter = self._adapter_for_source(source)
                     if _err_adapter and hasattr(_err_adapter, "stop_typing"):
                         await _err_adapter.stop_typing(source.chat_id)
                 except Exception:
    ```

    ```diff
    @@ -12304,7 +12304,7 @@ def _get_goal_manager_for_event(self, event: "MessageEvent"):
     
         async def _send_goal_status_notice(self, source: Any, message: str) -> None:
             """Send a /goal judge status line back to the originating chat/thread."""
    -        adapter = self.adapters.get(source.platform)
    +        adapter = self._adapter_for_source(source)
             if not adapter:
                 logger.debug("goal continuation: no adapter for %s", getattr(source, "platform", None))
                 return
    ```

    ```diff
    @@ -12331,7 +12331,7 @@ async def _defer_goal_status_notice_after_delivery(self, source: Any, message: s
             exactly this boundary; when unavailable, fall back to direct awaited
             delivery rather than silently dropping the notice.
             """
    -        adapter = self.adapters.get(source.platform)
    +        adapter = self._adapter_for_source(source)
             if not adapter:
                 logger.debug("goal continuation: no adapter for %s", getattr(source, "platform", None))
                 return
    ```

    ```diff
    @@ -12429,7 +12429,7 @@ async def _post_turn_goal_continuation(
             # Enqueue via the adapter's FIFO so a user message already in
             # flight preempts the continuation naturally.
             try:
    -            adapter = self.adapters.get(source.platform)
    +            adapter = self._adapter_for_source(source)
                 _quick_key = self._session_key_for_source(source)
                 if adapter and _quick_key:
                     cont_event = MessageEvent(
    ```

    ```diff
    @@ -12462,7 +12462,7 @@ def _get_guild_id(event: MessageEvent) -> Optional[int]:
     
         async def _handle_voice_channel_join(self, event: MessageEvent) -> str:
             """Join the user's current Discord voice channel."""
    -        adapter = self.adapters.get(event.source.platform)
    +        adapter = self._adapter_for_source(event.source)
             if not hasattr(adapter, "join_voice_channel"):
                 return "Voice channels are not supported on this platform."
     
    ```

    ```diff
    @@ -12519,7 +12519,7 @@ async def _handle_voice_channel_join(self, event: MessageEvent) -> str:
     
         async def _handle_voice_channel_leave(self, event: MessageEvent) -> str:
             """Leave the Discord voice channel."""
    -        adapter = self.adapters.get(event.source.platform)
    +        adapter = self._adapter_for_source(event.source)
             guild_id = self._get_guild_id(event)
     
             if not guild_id or not hasattr(adapter, "leave_voice_channel"):
    ```

    ```diff
    @@ -12753,7 +12753,7 @@ async def _send_voice_reply(self, event: MessageEvent, text: str) -> None:
                     logger.warning("Auto voice reply TTS failed: %s", result.get("error"))
                     return
     
    -            adapter = self.adapters.get(event.source.platform)
    +            adapter = self._adapter_for_source(event.source)
     
                 # If connected to a voice channel, play there instead of sending a file
                 guild_id = self._get_guild_id(event)
    ```

    ```diff
    @@ -12931,7 +12931,7 @@ async def _run_background_task(
             media_urls = media_urls or []
             media_types = media_types or []
     
    -        adapter = self.adapters.get(source.platform)
    +        adapter = self._adapter_for_source(source)
             if not adapter:
                 logger.warning("No adapter for platform %s in background task %s", source.platform, task_id)
                 return
    ```

    ```diff
    @@ -13126,7 +13126,7 @@ def run_sync():
     
         async def _get_telegram_topic_capabilities(self, source: SessionSource) -> dict:
             """Read Telegram private-topic capability flags via Bot API getMe."""
    -        adapter = self.adapters.get(source.platform) if getattr(self, "adapters", None) else None
    +        adapter = self._adapter_for_source(source)
             bot = getattr(adapter, "_bot", None)
             if bot is None or not hasattr(bot, "get_me"):
                 return {"checked": False}
    ```

    ```diff
    @@ -13154,7 +13154,7 @@ def _field(name: str):
     
         async def _ensure_telegram_system_topic(self, source: SessionSource) -> None:
             """Create/pin the managed System topic after /topic activation when possible."""
    -        adapter = self.adapters.get(source.platform) if getattr(self, "adapters", None) else None
    +        adapter = self._adapter_for_source(source)
             if adapter is None or not source.chat_id:
                 return
     
    ```

    ```diff
    @@ -13195,7 +13195,7 @@ async def _ensure_telegram_system_topic(self, source: SessionSource) -> None:
     
         async def _send_telegram_topic_setup_image(self, source: SessionSource) -> None:
             """Send the bundled BotFather Threads Settings screenshot when available."""
    -        adapter = self.adapters.get(source.platform) if getattr(self, "adapters", None) else None
    +        adapter = self._adapter_for_source(source)
             if adapter is None or not source.chat_id or not hasattr(adapter, "send_image_file"):
                 return
             image_path = Path(__file__).resolve().parent / "assets" / "telegram-botfather-threads-settings.jpg"
    ```

    ```diff
    @@ -13247,7 +13247,7 @@ async def _rename_telegram_topic_for_session_title(
             # Check the class, not the instance — getattr() on MagicMock
             # auto-creates attributes, so `hasattr(adapter, "_get_dm_topic_info")`
             # would return True for every test double.
    -        adapter = self.adapters.get(source.platform) if getattr(self, "adapters", None) else None
    +        adapter = self._adapter_for_source(source)
             if adapter is not None:
                 get_info = getattr(type(adapter), "_get_dm_topic_info", None)
                 if callable(get_info):
    ```

    ```diff
    @@ -13806,7 +13806,7 @@ async def _request_slash_confirm(
             # cannot race the send_slash_confirm return.
             _slash_confirm_mod.register(session_key, confirm_id, command, handler)
     
    -        adapter = self.adapters.get(source.platform)
    +        adapter = self._adapter_for_source(source)
             metadata = self._thread_metadata_for_source(source, self._reply_anchor_for_event(event))
     
             used_buttons = False
    ```

    ```diff
    @@ -14820,7 +14820,7 @@ async def _dequeue_pending_with_transcription(
                 # Echo raw transcripts back to the user when configured so voice
                 # interrupts feel identical to fresh voice messages.
                 if successful_transcripts and self._should_echo_stt_transcripts():
    -                echo_adapter = self.adapters.get(source.platform)
    +                echo_adapter = self._adapter_for_source(source)
                     echo_meta = {"thread_id": source.thread_id} if source.thread_id else None
                     if echo_adapter:
                         for tx in successful_transcripts:
    ```

    ```diff
    @@ -15642,7 +15642,7 @@ async def _interrupt_and_clear_session(
             if running_agent and running_agent is not _AGENT_PENDING_SENTINEL:
                 running_agent.interrupt(interrupt_reason)
             self._invalidate_session_run_generation(session_key, reason=invalidation_reason)
    -        adapter = self.adapters.get(source.platform)
    +        adapter = self._adapter_for_source(source)
             if adapter and hasattr(adapter, "interrupt_session_activity"):
                 await adapter.interrupt_session_activity(session_key, source.chat_id)
             if adapter and hasattr(adapter, "get_pending_message"):
    ```

    ```diff
    @@ -16207,7 +16207,7 @@ def _run_still_current() -> bool:
             if _streaming_enabled:
                 try:
                     from gateway.stream_consumer import GatewayStreamConsumer, StreamConsumerConfig
    -                _adapter = self.adapters.get(source.platform)
    +                _adapter = self._adapter_for_source(source)
                     if _adapter:
                         _pause_typing_before_finalize = None
                         if source.platform == Platform.TELEGRAM and hasattr(_adapter, "pause_typing_for_chat"):
    ```

    ```diff
    @@ -16258,7 +16258,7 @@ def _pause_typing_before_finalize(
                 stream_task = asyncio.create_task(_stream_consumer.run())
     
             # Send typing indicator
    -        _adapter = self.adapters.get(source.platform)
    +        _adapter = self._adapter_for_source(source)
             if _adapter:
                 try:
                     await _adapter.send_typing(source.chat_id, metadata=_thread_metadata)
    ```

    ```diff
    @@ -16698,7 +16698,7 @@ def voice_ack_callback(call_id, tool_name, args):
             _cleanup_progress = bool(
                 resolve_display_setting(user_config, platform_key, "cleanup_progress")
             )
    -        _cleanup_adapter = self.adapters.get(source.platform) if _cleanup_progress else None
    +        _cleanup_adapter = self._adapter_for_source(source) if _cleanup_progress else None
             if _cleanup_adapter is not None and (
                 type(_cleanup_adapter).delete_message is BasePlatformAdapter.delete_message
             ):
    ```

    ```diff
    @@ -16819,7 +16819,7 @@ def progress_callback(event_type: str, tool_name: str = None, preview: str = Non
                 _code_block_full = None
                 _code_block_short = None
                 try:
    -                _progress_adapter = self.adapters.get(source.platform)
    +                _progress_adapter = self._adapter_for_source(source)
                 except Exception:
                     _progress_adapter = None
                 if (
    ```

    ```diff
    @@ -17009,7 +17009,7 @@ async def send_progress_messages():
                 if not progress_queue:
                     return
     
    -            adapter = self.adapters.get(source.platform)
    +            adapter = self._adapter_for_source(source)
                 if not adapter:
                     return
     
    ```

    ```diff
    @@ -17384,7 +17384,7 @@ def _event_callback_sync(event_type: str, context: dict) -> None:
                     logger.debug("event_callback hook error: %s", _e)
     
             # Bridge sync status_callback → async adapter.send for context pressure
    -        _status_adapter = self.adapters.get(source.platform)
    +        _status_adapter = self._adapter_for_source(source)
             _status_chat_id = source.chat_id
             if source.platform == Platform.FEISHU and source.thread_id and event_message_id:
                 # Feishu topics only keep messages inside the topic when they are
    ```

    ```diff
    @@ -17530,7 +17530,7 @@ def run_sync():
                 if _want_stream_deltas or _want_interim_consumer:
                     try:
                         from gateway.stream_consumer import GatewayStreamConsumer, StreamConsumerConfig
    -                    _adapter = self.adapters.get(source.platform)
    +                    _adapter = self._adapter_for_source(source)
                         if _adapter:
                             _pause_typing_before_finalize = None
                             if source.platform == Platform.TELEGRAM and hasattr(_adapter, "pause_typing_for_chat"):
    ```

    ```diff
    @@ -18698,7 +18698,7 @@ async def monitor_for_interrupt():
                     try:
                         # Re-resolve adapter each iteration so reconnects don't
                         # leave us holding a stale reference.
    -                    _adapter = self.adapters.get(source.platform)
    +                    _adapter = self._adapter_for_source(source)
                         if not _adapter:
                             continue
                         # Check if adapter has a pending interrupt for this session.
    ```

    ```diff
    @@ -18793,7 +18793,7 @@ async def monitor_for_interrupt():
             async def _notify_long_running():
                 if _NOTIFY_INTERVAL is None:
                     return  # Notifications disabled (gateway_notify_interval: 0)
    -            _notify_adapter = self.adapters.get(source.platform)
    +            _notify_adapter = self._adapter_for_source(source)
                 if not _notify_adapter:
                     return
                 # Track the heartbeat message id so we can edit-in-place on
    ```

    ```diff
    @@ -18938,7 +18938,7 @@ def _stream_confirmed_final_delivery(
                         # Backup interrupt check: if the monitor task died or
                         # missed the interrupt, catch it here.
                         if not _interrupt_detected.is_set() and session_key:
    -                        _backup_adapter = self.adapters.get(source.platform)
    +                        _backup_adapter = self._adapter_for_source(source)
                             _backup_agent = agent_holder[0]
                             if (_backup_adapter and _backup_agent
                                     and hasattr(_backup_adapter, 'has_pending_interrupt')
    ```

    ```diff
    @@ -18978,7 +18978,7 @@ def _stream_confirmed_final_delivery(
                         if (not _warning_fired and _agent_warning is not None
                                 and _idle_secs >= _agent_warning):
                             _warning_fired = True
    -                        _warn_adapter = self.adapters.get(source.platform)
    +                        _warn_adapter = self._adapter_for_source(source)
                             if _warn_adapter:
                                 _elapsed_warn = int(_agent_warning // 60) or 1
                                 _remaining_mins = int((_agent_timeout - _agent_warning) // 60) or 1
    ```

    ```diff
    @@ -18998,7 +18998,7 @@ def _stream_confirmed_final_delivery(
                             break
                         # Backup interrupt check (same as unlimited path).
                         if not _interrupt_detected.is_set() and session_key:
    -                        _backup_adapter = self.adapters.get(source.platform)
    +                        _backup_adapter = self._adapter_for_source(source)
                             _backup_agent = agent_holder[0]
                             if (_backup_adapter and _backup_agent
                                     and hasattr(_backup_adapter, 'has_pending_interrupt')
    ```

    ```diff
    @@ -19115,7 +19115,7 @@ def _stream_confirmed_final_delivery(
     
                 # Check if we were interrupted OR have a queued message (/queue).
                 result = result_holder[0]
    -            adapter = self.adapters.get(source.platform)
    +            adapter = self._adapter_for_source(source)
                 
                 # Get pending message from adapter.
                 # Use session_key (not source.chat_id) to match adapter's storage keys.
    ```

    ```diff
    @@ -19246,7 +19246,7 @@ def _stream_confirmed_final_delivery(
                             "queueing message instead of recursing.",
                             _interrupt_depth, session_key,
                         )
    -                    adapter = self.adapters.get(source.platform)
    +                    adapter = self._adapter_for_source(source)
                         if adapter and pending_event:
                             merge_pending_message_event(adapter._pending_messages, session_key, pending_event)
                         elif adapter and hasattr(adapter, 'queue_message'):
    ```

    ```diff
    @@ -19365,7 +19365,7 @@ def _stream_confirmed_final_delivery(
                     # Restart typing indicator so the user sees activity while
                     # the follow-up turn runs.  The outer _process_message_background
                     # typing task is still alive but may be stale.
    -                _followup_adapter = self.adapters.get(source.platform)
    +                _followup_adapter = self._adapter_for_source(source)
                     if _followup_adapter:
                         try:
                             await _followup_adapter.send_typing(
    ```
  - `scripts/release.py`

    ```diff
    @@ -46,6 +46,8 @@
     # Auto-extracted from noreply emails + manual overrides
     AUTHOR_MAP = {
         "[redacted:email]": "derek2000139",  # PR #57838 salvage (desktop/windows: pre-write update marker before quit dwell so the renderer's waitForUpdateToFinish gate parks instead of respawning a backend that re-locks venv .pyd files mid-update)
    +    "AndreasHiltner@users.noreply.github.com": "AndreasHiltner",  # PR #56854 salvage (gateway: route multiplex profile responses through the profile's own adapter — 53-site _adapter_for_source sweep)
    +    "[redacted:email]": "ManniBr",  # PR #57417 partial salvage (gateway: fail-closed adapter resolution for unregistered secondary profiles)
         "[redacted:email]": "s905060",  # PR #57943 salvage (photon: auto-reinstall stale sidecar node_modules when lockfile is newer than npm's install marker; #59169)
         "[redacted:email]": "l0h1nth",  # PR #32210 salvage (mattermost: accept leading-space slash commands from mobile clients; #25184)
         "[redacted:email]": "devatnull",  # PR #58704 salvage (whatsapp: native Baileys polls, clarify-as-poll, location pins, structured quoted replies, PTT/audio split, bridge_helpers extraction)
    ```
  - `tests/gateway/test_notice_rendering.py`

    ```diff
    @@ -107,6 +107,10 @@ def _make_source(platform_value="telegram", chat_id="555", user_id="u1"):
         src.platform = plat
         src.chat_id = chat_id
         src.user_id = user_id
    +    # Real SessionSource.profile is None (single-profile) or a str; a MagicMock
    +    # auto-attribute would read as a truthy "stamped profile" and trip the
    +    # fail-closed path in _adapter_for_source (see AGENTS.md pitfall #17).
    +    src.profile = None
         return src
     
     
    ```
  - `tests/gateway/test_queue_consumption.py`

    ```diff
    @@ -382,7 +382,9 @@ def _make_runner_and_adapter(self):
             return runner, adapter
     
         def _text_event(self, text: str) -> MessageEvent:
    -        source = MagicMock(chat_id="c1", platform=Platform.TELEGRAM)
    +        # profile=None: a MagicMock auto-attribute reads as a truthy stamped
    +        # profile and trips fail-closed adapter resolution (AGENTS.md #17).
    +        source = MagicMock(chat_id="c1", platform=Platform.TELEGRAM, profile=None)
             return MessageEvent(
                 text=text,
                 message_type=MessageType.TEXT,
    ```

    ```diff
    @@ -433,7 +435,7 @@ def test_photo_burst_still_merges_in_head_slot(self):
             runner, adapter = self._make_runner_and_adapter()
             session_key = "telegram:user:burst"
     
    -        source = MagicMock(chat_id="c1", platform=Platform.TELEGRAM)
    +        source = MagicMock(chat_id="c1", platform=Platform.TELEGRAM, profile=None)
             for i in range(3):
                 runner._queue_or_replace_pending_event(
                     session_key,
    ```
- **#49962: fix(gateway): keep multiplex Telegram DMs on the owning bot**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `8a9bc38c2e72a20ed1e8b081917b6a0dd8891573`, `#59310`

## Fix status

- Merged to main in PR #59310 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-06`.
- Subsystem: `gateway`
