# NousResearch/hermes-agent · Telegram update queue preservation on reconnect

## Open now

- Nothing open on GitHub.

These issues and PRs address Telegram gateway behavior around dropped or preserved pending updates during reconnects and startup, including related adapter interface fixes and configuration toggles for catch-up behavior.

## TL;DR

- 9 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 3 merged PR(s), 4 PR(s) closed without merging.
- A fix was merged to main in PR #52844. Verify it addresses your case.
- **Recommended:** read PR #52844 (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: `partially_resolved` · deciding PR(s): `pr-52844`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.20`: 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

- #41193: feat: add env-var/config toggle for Telegram pending-updates catch-up on startup
- #46621: Telegram gateway discards the server-side update queue on watcher reconnect after a prolonged outage (start_polling drop_pending_updates=True)

## Prior & competing PRs

- **#52844: fix(telegram): preserve Bot API update queue on watcher reconnect (#46621)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/platforms/api_server.py`, `gateway/platforms/base.py`, `gateway/platforms/bluebubbles.py`, `gateway/platforms/msgraph_webhook.py`, `gateway/platforms/signal.py`, `gateway/platforms/webhook.py`, `gateway/platforms/weixin.py`, `gateway/platforms/whatsapp_cloud.py`, `gateway/platforms/yuanbao.py`, `gateway/run.py`, `plugins/platforms/dingtalk/adapter.py`, `plugins/platforms/discord/adapter.py`, `plugins/platforms/email/adapter.py`, `plugins/platforms/feishu/adapter.py`, `plugins/platforms/google_chat/adapter.py`, `plugins/platforms/homeassistant/adapter.py`, `plugins/platforms/irc/adapter.py`, `plugins/platforms/line/adapter.py`, `plugins/platforms/matrix/adapter.py`, `plugins/platforms/mattermost/adapter.py` (+45 more)
  - fix hunks:
  - `gateway/platforms/api_server.py`

    ```diff
    @@ -4372,7 +4372,7 @@ async def _sweep_orphaned_runs(self) -> None:
         # BasePlatformAdapter interface
         # ------------------------------------------------------------------
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             """Start the aiohttp web server."""
             if not AIOHTTP_AVAILABLE:
                 logger.warning("[%s] aiohttp not installed", self.name)
    ```
  - `gateway/platforms/base.py`

    ```diff
    @@ -2634,10 +2634,21 @@ def set_session_store(self, session_store: Any) -> None:
             self._session_store = session_store
         
         @abstractmethod
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             """
             Connect to the platform and start receiving messages.
    -        
    +
    +        Args:
    +            is_reconnect: False on a cold first boot (the gateway is
    +                starting this platform for the first time); True when the
    +                reconnect watcher is re-establishing a platform that was
    +                previously running and dropped after an outage. Adapters
    +                that buffer a server-side update queue (e.g. Telegram's Bot
    +                API) should preserve that queue when ``is_reconnect`` is
    +                True so messages sent during the outage are delivered rather
    +                than silently discarded. Adapters with no such queue may
    +                ignore the flag.
    +
             Returns True if connection was successful.
             """
             pass
    ```
  - `gateway/platforms/bluebubbles.py`

    ```diff
    @@ -232,7 +232,7 @@ async def _api_post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
         # Lifecycle
         # ------------------------------------------------------------------
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             if not self.server_url or not self.password:
                 [redacted:secret-assignment]
                     "[bluebubbles] BLUEBUBBLES_SERVER_URL and BLUEBUBBLES_PASSWORD are required"
    ```
  - `gateway/platforms/msgraph_webhook.py`

    ```diff
    @@ -136,7 +136,7 @@ def set_notification_scheduler(self, scheduler: Optional[NotificationScheduler])
         def _source_allowlist_required_but_missing(self) -> bool:
             return is_network_accessible(self._host) and not self._allowed_source_networks
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             if self._client_state is None:
                 logger.error(
                     "[msgraph_webhook] Refusing to start without extra.client_state configured"
    ```
  - `gateway/platforms/signal.py`

    ```diff
    @@ -338,7 +338,7 @@ def __init__(self, config: PlatformConfig):
         # Lifecycle
         # ------------------------------------------------------------------
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             """Connect to signal-cli daemon and start SSE listener."""
             if not self.http_url or not self.account:
                 logger.error("Signal: SIGNAL_HTTP_URL and SIGNAL_ACCOUNT are required")
    ```
  - `gateway/platforms/webhook.py`

    ```diff
    @@ -153,7 +153,7 @@ def __init__(self, config: PlatformConfig):
         # Lifecycle
         # ------------------------------------------------------------------
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             # Load agent-created subscriptions before validating
             self._reload_dynamic_routes()
     
    ```
  - `gateway/platforms/weixin.py`

    ```diff
    @@ -1261,7 +1261,7 @@ def _coerce_list(value: Any) -> List[str]:
                 return [str(item).strip() for item in value if str(item).strip()]
             return [str(value).strip()] if str(value).strip() else []
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             if not check_weixin_requirements():
                 message = "Weixin startup failed: aiohttp and cryptography are required"
                 self._set_fatal_error("weixin_missing_dependency", message, retryable=False)
    ```
  - `gateway/platforms/whatsapp_cloud.py`

    ```diff
    @@ -347,7 +347,7 @@ def _is_dm_allowed(self, sender_id: str) -> bool:
             return super()._is_dm_allowed(sender_id)
     
         # ------------------------------------------------------------------ lifecycle
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             if not check_whatsapp_cloud_requirements():
                 self._set_fatal_error(
                     "whatsapp_cloud_deps_missing",
    ```
  - `gateway/platforms/yuanbao.py`

    ```diff
    @@ -5114,7 +5114,7 @@ def enforces_own_access_policy(self) -> bool:
             """Yuanbao gates DM/group access at intake via dm_policy/group_policy."""
             return True
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             """Connect to Yuanbao WS gateway and authenticate.
     
             Delegates to ConnectionManager.open().
    ```
  - `gateway/run.py`

    ```diff
    @@ -3086,13 +3086,24 @@ def _platform_connect_timeout_secs(self) -> float:
                     return max(0.0, timeout)
             return _PLATFORM_CONNECT_TIMEOUT_SECS_DEFAULT
     
    -    async def _connect_adapter_with_timeout(self, adapter, platform) -> bool:
    -        """Connect an adapter without allowing one platform to block others."""
    +    async def _connect_adapter_with_timeout(
    +        self, adapter, platform, *, is_reconnect: bool = False
    +    ) -> bool:
    +        """Connect an adapter without allowing one platform to block others.
    +
    +        ``is_reconnect`` is forwarded to ``adapter.connect()`` so platform
    +        adapters can distinguish a cold first boot (drop any stale
    +        server-side queue) from a watcher reconnect after a prolonged outage
    +        (preserve the queue so messages sent during the outage are delivered
    +        rather than silently dropped — #46621).
    +        """
             timeout = self._platform_connect_timeout_secs()
             if timeout <= 0:
    -            return await adapter.connect()
    +            return await adapter.connect(is_reconnect=is_reconnect)
             try:
    -            return await asyncio.wait_for(adapter.connect(), timeout=timeout)
    +            return await asyncio.wait_for(
    +                adapter.connect(is_reconnect=is_reconnect), timeout=timeout
    +            )
             except asyncio.TimeoutError as exc:
                 raise TimeoutError(
                     f"{platform.value} connect timed out after {timeout:g}s"
    ```

    ```diff
    @@ -6718,7 +6729,12 @@ async def _platform_reconnect_watcher(self) -> None:
                         adapter.set_topic_recovery_fn(self._recover_telegram_topic_thread_id)
                         adapter._busy_text_mode = self._busy_text_mode
     
    -                    success = await self._connect_adapter_with_timeout(adapter, platform)
    +                    # Reconnect after an outage: preserve the platform's
    +                    # server-side update queue so messages sent while the bot
    +                    # was offline are delivered rather than dropped (#46621).
    +                    success = await self._connect_adapter_with_timeout(
    +                        adapter, platform, is_reconnect=True
    +                    )
                         if success:
                             self.adapters[platform] = adapter
                             self._sync_voice_mode_state_to_adapter(adapter)
    ```
  - `plugins/platforms/dingtalk/adapter.py`

    ```diff
    @@ -239,7 +239,7 @@ def __init__(self, config: PlatformConfig):
     
         # -- Connection lifecycle -----------------------------------------------
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             """Connect to DingTalk via Stream Mode."""
             if not DINGTALK_STREAM_AVAILABLE:
                 logger.warning(
    ```
  - `plugins/platforms/discord/adapter.py`

    ```diff
    @@ -859,7 +859,7 @@ async def _notify() -> None:
     
             asyncio.create_task(_notify())
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             """Connect to Discord and start receiving events."""
             if not DISCORD_AVAILABLE:
                 logger.error("[%s] discord.py not installed. Run: pip install discord.py", self.name)
    ```
  - `plugins/platforms/email/adapter.py`

    ```diff
    @@ -548,7 +548,7 @@ def _connect(*, ipv4_only: bool = False) -> smtplib.SMTP:
                 # Retry with IPv4 only.
                 return _connect(ipv4_only=True)
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             """Connect to the IMAP server and start polling for new messages."""
             # Validate up front so a missing host surfaces as an actionable config
             # error instead of IMAP4_SSL("") raising the cryptic
    ```
  - `plugins/platforms/feishu/adapter.py`

    ```diff
    @@ -1641,7 +1641,7 @@ def _build_event_handler(self) -> Any:
                 .build()
             )
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             """Connect to Feishu/Lark."""
             if not FEISHU_AVAILABLE:
                 logger.error("[Feishu] lark-oapi not installed")
    ```
  - `plugins/platforms/google_chat/adapter.py`

    ```diff
    @@ -761,7 +761,7 @@ async def _resolve_bot_user_id(self) -> Optional[str]:
         # ------------------------------------------------------------------
         # Connection lifecycle
         # ------------------------------------------------------------------
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             """Validate config, authenticate, start Pub/Sub pull, resolve bot id."""
             # First call into the heavy google-cloud stack — trigger the lazy
             # import. ``_load_google_modules()`` is idempotent and rebinds the
    ```
  - `plugins/platforms/homeassistant/adapter.py`

    ```diff
    @@ -98,7 +98,7 @@ def _next_id(self) -> int:
         # Connection lifecycle
         # ------------------------------------------------------------------
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             """Connect to HA WebSocket API and subscribe to events."""
             if not AIOHTTP_AVAILABLE:
                 logger.warning("[%s] aiohttp not installed. Run: pip install aiohttp", self.name)
    ```
  - `plugins/platforms/irc/adapter.py`

    ```diff
    @@ -152,7 +152,7 @@ def name(self) -> str:
     
         # ── Connection lifecycle ──────────────────────────────────────────────
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             """Connect to the IRC server, register, and join the channel."""
             if not self.server or not self.channel:
                 logger.error("IRC: server and channel must be configured")
    ```
  - `plugins/platforms/line/adapter.py`

    ```diff
    @@ -740,7 +740,7 @@ def __init__(self, config, **kwargs):
         # Connection lifecycle
         # ------------------------------------------------------------------
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             if not self.channel_access_token or not self.channel_secret:
                 [redacted:secret-assignment]
                     "config_missing",
    ```
  - `plugins/platforms/matrix/adapter.py`

    ```diff
    @@ -1135,7 +1135,7 @@ async def _verify_device_keys_on_server(self, client: Any, olm: Any) -> bool:
         # Required overrides
         # ------------------------------------------------------------------
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             """Connect to the Matrix homeserver and start syncing."""
             from mautrix.api import HTTPAPI
             from mautrix.client import Client
    ```
  - `plugins/platforms/mattermost/adapter.py`

    ```diff
    @@ -256,7 +256,7 @@ async def _upload_file(
         # Required overrides
         # ------------------------------------------------------------------
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             """Connect to Mattermost and start the WebSocket listener."""
             import aiohttp
     
    ```
  - `plugins/platforms/ntfy/adapter.py`

    ```diff
    @@ -183,7 +183,7 @@ def __init__(self, config: PlatformConfig):
     
         # -- Connection lifecycle -----------------------------------------------
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             """Connect to ntfy by starting the streaming subscription task."""
             if not HTTPX_AVAILABLE:
                 logger.warning("[%s] httpx not installed. Run: pip install httpx", self.name)
    ```
  - `plugins/platforms/photon/adapter.py`

    ```diff
    @@ -334,7 +334,7 @@ def _clean_mention_text(self, text: str) -> str:
     
         # -- Connection lifecycle ---------------------------------------------
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             if not HTTPX_AVAILABLE:
                 self._set_fatal_error(
                     "MISSING_DEP", "httpx not installed", retryable=False
    ```
  - `plugins/platforms/raft/adapter.py`

    ```diff
    @@ -468,7 +468,7 @@ def __init__(self, config: PlatformConfig):
         def runtime_session(self) -> str:
             return self._runtime_session
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             if not self._bridge_token:
                 [redacted:secret-assignment] = secrets.token_hex(32)
                 logger.info("[raft] Auto-generated bridge token")
    ```
  - `plugins/platforms/simplex/adapter.py`

    ```diff
    @@ -208,7 +208,7 @@ def __init__(self, config: PlatformConfig, **kwargs):
         # Lifecycle
         # ------------------------------------------------------------------
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             """Connect to the simplex-chat daemon and start the WebSocket listener."""
             try:
                 import websockets  # noqa: F401
    ```
  - `plugins/platforms/slack/adapter.py`

    ```diff
    @@ -829,7 +829,7 @@ async def _send_slash_ephemeral(
             # Non-fatal — the user saw the initial ack already.
             return SendResult(success=True, message_id=None)
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             """Connect to Slack via Socket Mode."""
             if not SLACK_AVAILABLE:
                 logger.error(
    ```
  - `plugins/platforms/sms/adapter.py`

    ```diff
    @@ -86,7 +86,7 @@ def _basic_auth_header(self) -> str:
         # Required abstract methods
         # ------------------------------------------------------------------
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             import aiohttp
             from aiohttp import web
     
    ```
  - `plugins/platforms/teams/adapter.py`

    ```diff
    @@ -709,7 +709,7 @@ def __init__(self, config: PlatformConfig):
             # Used to send cards with the correct conversation type (personal/group/channel).
             self._conv_refs: Dict[str, Any] = {}
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             # Lazy-install the Teams SDK on demand (parity with Slack/Discord/etc.),
             # then re-check the module globals it rebinds.
             check_teams_requirements()
    ```
  - `plugins/platforms/telegram/adapter.py`

    ```diff
    @@ -2232,7 +2232,7 @@ async def _setup_dm_topics(self) -> None:
                                 self.name, topic_name, seed_err,
                             )
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             """Connect to Telegram via polling or webhook.
     
             By default, uses long polling (outbound connection to Telegram).
    ```

    ```diff
    @@ -2240,6 +2240,14 @@ async def connect(self) -> bool:
             instead.  Webhook mode is useful for cloud deployments (Fly.io,
             Railway) where inbound HTTP can wake a suspended machine.
     
    +        ``is_reconnect`` distinguishes a cold first boot (False — drop any
    +        stale Bot API queue) from a watcher reconnect after a prolonged
    +        outage (True — preserve the updates Telegram queued while the bot
    +        was offline, otherwise every message sent during the outage is
    +        silently lost). The in-process network-error ladder and the
    +        409-conflict handler already pass ``drop_pending_updates=False``
    +        for the same reason; bootstrap follows suit on the reconnect path.
    +
             Env vars for webhook mode::
     
                 TELEGRAM_WEBHOOK_URL    Public HTTPS URL (e.g. https://app.fly.dev/telegram)
    ```

    ```diff
    @@ -2476,7 +2484,11 @@ def _with_limits(httpx_kwargs: Optional[dict] = None) -> dict:
                         webhook_url=webhook_url,
                         secret_token=[redacted:secret-assignment]
                         allowed_updates=Update.ALL_TYPES,
    -                    drop_pending_updates=True,
    +                    # Webhooks are push-based — Telegram does not hold a
    +                    # server-side getUpdates queue, so this flag is a no-op
    +                    # in practice. Mirror the polling path's reconnect
    +                    # semantics for consistency.
    +                    drop_pending_updates=not is_reconnect,
                     )
                     self._webhook_mode = True
                     logger.info(
    ```

    ```diff
    @@ -2509,7 +2521,10 @@ def _polling_error_callback(error: Exception) -> None:
     
                     await self._app.updater.start_polling(
                         allowed_updates=Update.ALL_TYPES,
    -                    drop_pending_updates=True,
    +                    # On a cold first boot drop the stale Bot API queue; on a
    +                    # watcher reconnect after an outage preserve it so messages
    +                    # sent while the bot was offline are delivered (#46621).
    +                    drop_pending_updates=not is_reconnect,
                         error_callback=_polling_error_callback,
                     )
                 
    ```
  - `plugins/platforms/wecom/adapter.py`

    ```diff
    @@ -198,7 +198,7 @@ def __init__(self, config: PlatformConfig):
         # Connection lifecycle
         # ------------------------------------------------------------------
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             """Connect to the WeCom AI Bot gateway."""
             if not AIOHTTP_AVAILABLE:
                 message = "WeCom startup failed: aiohttp not installed"
    ```
  - `plugins/platforms/whatsapp/adapter.py`

    ```diff
    @@ -411,7 +411,7 @@ def _coerce_float_extra(self, key: str, default: float) -> float:
                 return float(default)
             return parsed
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             """
             Start the WhatsApp bridge.
             
    ```
  - `tests/gateway/relay/stub_connector.py`

    ```diff
    @@ -40,7 +40,7 @@ def __init__(self, descriptor: CapabilityDescriptor) -> None:
             # absent/expired capability or a tenant mismatch on the connector side.
             self.next_follow_up_result: Dict[str, Any] = {"success": True, "message_id": "f1"}
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             self.connected = True
             return True
     
    ```
  - `tests/gateway/relay/test_relay_going_idle.py`

    ```diff
    @@ -422,7 +422,7 @@ async def test_adapter_go_dormant_noop_on_stub_transport():
         )
     
         class _StubTransport:
    -        async def connect(self):
    +        async def connect(self, *, is_reconnect: bool = False):
                 return True
     
             def set_inbound_handler(self, h):
    ```
  - `tests/gateway/restart_test_helpers.py`

    ```diff
    @@ -15,7 +15,7 @@ def __init__(self):
             self.sent: list[str] = []
             self.sent_calls: list[tuple[str, str, object]] = []
     
    -    async def connect(self):
    +    async def connect(self, *, is_reconnect: bool = False):
             return True
     
         async def disconnect(self):
    ```
  - `tests/gateway/test_active_session_text_merge.py`

    ```diff
    @@ -65,7 +65,7 @@ def _make_event(
     
     
     class _DummyAdapter(BasePlatformAdapter):  # type: ignore[misc]
    -    async def connect(self):
    +    async def connect(self, *, is_reconnect: bool = False):
             pass
     
         async def disconnect(self):
    ```
  - `tests/gateway/test_base_topic_sessions.py`

    ```diff
    @@ -20,7 +20,7 @@ def __init__(self):
             self.typing = []
             self.processing_hooks = []
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             return True
     
         async def disconnect(self) -> None:
    ```
  - `tests/gateway/test_cancel_background_drain.py`

    ```diff
    @@ -19,7 +19,7 @@
     
     
     class _StubAdapter(BasePlatformAdapter):
    -    async def connect(self):
    +    async def connect(self, *, is_reconnect: bool = False):
             pass
     
         async def disconnect(self):
    ```
  - `tests/gateway/test_command_bypass_active_session.py`

    ```diff
    @@ -29,7 +29,7 @@
     class _StubAdapter(BasePlatformAdapter):
         """Concrete adapter with abstract methods stubbed out."""
     
    -    async def connect(self):
    +    async def connect(self, *, is_reconnect: bool = False):
             pass
     
         async def disconnect(self):
    ```
  - `tests/gateway/test_duplicate_reply_suppression.py`

    ```diff
    @@ -37,7 +37,7 @@ def __init__(self):
             super().__init__(PlatformConfig(enabled=True, token="fake"), Platform.DISCORD)
             self.sent = []
     
    -    async def connect(self):
    +    async def connect(self, *, is_reconnect: bool = False):
             return True
     
         async def disconnect(self):
    ```
  - `tests/gateway/test_ephemeral_reply.py`

    ```diff
    @@ -39,7 +39,7 @@
     class _NoDeleteAdapter(BasePlatformAdapter):
         """Adapter that does NOT override delete_message (silent degrade)."""
     
    -    async def connect(self):
    +    async def connect(self, *, is_reconnect: bool = False):
             pass
     
         async def disconnect(self):
    ```

    ```diff
    @@ -59,7 +59,7 @@ def __init__(self, *a, **kw):
             super().__init__(*a, **kw)
             self.deleted: list[tuple[str, str]] = []
     
    -    async def connect(self):
    +    async def connect(self, *, is_reconnect: bool = False):
             pass
     
         async def disconnect(self):
    ```
  - `tests/gateway/test_interrupt_key_match.py`

    ```diff
    @@ -21,7 +21,7 @@ class StubAdapter(BasePlatformAdapter):
         def __init__(self):
             super().__init__(PlatformConfig(enabled=True, token="test"), Platform.TELEGRAM)
     
    -    async def connect(self):
    +    async def connect(self, *, is_reconnect: bool = False):
             return True
     
         async def disconnect(self):
    ```
  - `tests/gateway/test_keep_typing_timeout.py`

    ```diff
    @@ -38,7 +38,7 @@ class _StubAdapter(BasePlatformAdapter):
         def __init__(self):
             super().__init__(PlatformConfig(enabled=True, token="test"), Platform.TELEGRAM)
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             return True
     
         async def disconnect(self) -> None:
    ```
  - `tests/gateway/test_pending_drain_no_recursion.py`

    ```diff
    @@ -35,7 +35,7 @@
     
     
     class _StubAdapter(BasePlatformAdapter):
    -    async def connect(self):
    +    async def connect(self, *, is_reconnect: bool = False):
             pass
     
         async def disconnect(self):
    ```
  - `tests/gateway/test_pending_drain_race.py`

    ```diff
    @@ -36,7 +36,7 @@
     
     
     class _StubAdapter(BasePlatformAdapter):
    -    async def connect(self):
    +    async def connect(self, *, is_reconnect: bool = False):
             pass
     
         async def disconnect(self):
    ```
  - `tests/gateway/test_platform_base.py`

    ```diff
    @@ -1276,7 +1276,7 @@ def _adapter(self):
             """Create a minimal adapter instance for testing static/instance methods."""
     
             class StubAdapter(BasePlatformAdapter):
    -            async def connect(self):
    +            async def connect(self, *, is_reconnect: bool = False):
                     return True
     
                 async def disconnect(self):
    ```
  - `tests/gateway/test_platform_reconnect.py`

    ```diff
    @@ -26,8 +26,12 @@ def __init__(
             self._succeed = succeed
             self._fatal_error = fatal_error
             self._fatal_retryable = fatal_retryable
    +        # Records the is_reconnect value of every connect() call so tests can
    +        # assert that the watcher distinguishes reconnect from cold boot (#46621).
    +        self.connect_calls: list[bool] = []
     
    -    async def connect(self):
    +    async def connect(self, *, is_reconnect: bool = False):
    +        self.connect_calls.append(is_reconnect)
             if self._fatal_error:
                 self._set_fatal_error("test_error", self._fatal_error, retryable=self._fatal_retryable)
                 return False
    ```

    ```diff
    @@ -140,7 +144,7 @@ async def test_connect_adapter_timeout_raises_retryable_exception(self, monkeypa
             runner = _make_runner()
             adapter = StubAdapter()
     
    -        async def hang():
    +        async def hang(*, is_reconnect: bool = False):
                 await asyncio.sleep(60)
                 return True
     
    ```

    ```diff
    @@ -217,6 +221,59 @@ async def fake_sleep(n):
             assert Platform.TELEGRAM not in runner._failed_platforms
             assert Platform.TELEGRAM in runner.adapters
     
    +    @pytest.mark.asyncio
    +    async def test_reconnect_passes_is_reconnect_true(self):
    +        """The watcher must connect with is_reconnect=True so adapters preserve
    +        their server-side update queue across an outage (#46621). Without this,
    +        bootstrap start_polling(drop_pending_updates=True) silently dropped every
    +        message queued while the bot was offline."""
    +        runner = _make_runner()
    +        runner._sync_voice_mode_state_to_adapter = MagicMock()
    +
    +        runner._failed_platforms[Platform.TELEGRAM] = {
    +            "config": PlatformConfig(enabled=True, token="test"),
    +            "attempts": 1,
    +            "next_retry": time.monotonic() - 1,
    +        }
    +
    +        succeed_adapter = StubAdapter(succeed=True)
    +        real_sleep = asyncio.sleep
    +
    +        with patch.object(runner, "_create_adapter", return_value=succeed_adapter):
    +            with patch("gateway.run.build_channel_directory", create=True):
    +                runner._running = True
    +                call_count = 0
    +
    +                async def fake_sleep(n):
    +                    nonlocal call_count
    +                    call_count += 1
    +                    if call_count > 1:
    +                        runner._running = False
    +                    await real_sleep(0)
    +
    +                with patch("asyncio.sleep", side_effect=fake_sleep):
    +                    await runner._platform_reconnect_watcher()
    +
    +        assert succeed_adapter.connect_calls == [True], (
    +            f"watcher must pass is_reconnect=True; got {succeed_adapter.connect_calls!r}"
    +        )
    +        assert Platform.TELEGRAM in runner.adapters
    +
    +    @pytest.mark.asyncio
    +    async def test_cold_connect_defaults_to_is_reconnect_false(self):
    +        """The cold-start connect path (_connect_adapter_with_timeout with no
    +        is_reconnect arg) must default to False so a first boot still drops any
    +        stale queue (#46621)."""
    +        runner = _make_runner()
    +        adapter = StubAdapter(succeed=True)
    +
    +        success = await runner._connect_adapter_with_timeout(adapter, Platform.TELEGRAM)
    +
    +        assert success is True
    +        assert adapter.connect_calls == [False], (
    +            f"cold-start must default to is_reconnect=False; got {adapter.connect_calls!r}"
    +        )
    +
         @pytest.mark.asyncio
         async def test_reconnect_retries_resume_pending_for_platform(self):
             """A successful reconnect retries the startup auto-resume scoped to
    ```
  - `tests/gateway/test_platform_reconnect_fd_leak.py`

    ```diff
    @@ -103,7 +103,7 @@ def __init__(self, *, succeed: bool = False, fatal_error: str | None = None,
             self._fatal_retryable = fatal_retryable
             self._raise_during_connect = raise_during_connect
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             if self._raise_during_connect:
                 raise RuntimeError("simulated connect exception")
             if self._fatal_error:
    ```

    ```diff
    @@ -254,7 +254,7 @@ def __init__(self):
                         Platform.TELEGRAM,
                     )
     
    -            async def connect(self) -> bool:
    +            async def connect(self, *, is_reconnect: bool = False) -> bool:
                     return True
     
                 async def disconnect(self) -> None:
    ```
  - `tests/gateway/test_post_delivery_callback_chaining.py`

    ```diff
    @@ -13,7 +13,7 @@
     
     
     class _MinAdapter(BasePlatformAdapter):
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             return True
     
         async def disconnect(self) -> None:
    ```
  - `tests/gateway/test_queue_consumption.py`

    ```diff
    @@ -27,7 +27,7 @@ class _StubAdapter(BasePlatformAdapter):
         def __init__(self):
             super().__init__(PlatformConfig(enabled=True, token="test"), Platform.TELEGRAM)
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             return True
     
         async def disconnect(self) -> None:
    ```
  - `tests/gateway/test_relay_capability_surface.py`

    ```diff
    @@ -22,7 +22,7 @@
     class _MinAdapter(BasePlatformAdapter):
         """Smallest concrete adapter: implements exactly the abstract methods."""
     
    -    async def connect(self):  # pragma: no cover - not called
    +    async def connect(self, *, is_reconnect: bool = False):  # pragma: no cover - not called
             return True
     
         async def disconnect(self):  # pragma: no cover - not called
    ```
  - `tests/gateway/test_run_cleanup_progress.py`

    ```diff
    @@ -41,7 +41,7 @@ def __init__(self, platform=Platform.TELEGRAM):
             self.edits = []
             self.deleted = []
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             return True
     
         async def disconnect(self) -> None:
    ```
  - `tests/gateway/test_run_progress_interrupt.py`

    ```diff
    @@ -28,7 +28,7 @@ def __init__(self, platform=Platform.TELEGRAM):
             self.edits = []
             self.typing = []
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             return True
     
         async def disconnect(self) -> None:
    ```
  - `tests/gateway/test_run_progress_topics.py`

    ```diff
    @@ -22,7 +22,7 @@ def __init__(self, platform=Platform.TELEGRAM):
             self.edits = []
             self.typing = []
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             return True
     
         async def disconnect(self) -> None:
    ```
  - `tests/gateway/test_runner_fatal_adapter.py`

    ```diff
    @@ -11,7 +11,7 @@ class _FatalAdapter(BasePlatformAdapter):
         def __init__(self):
             super().__init__(PlatformConfig(enabled=True, token="token"), Platform.TELEGRAM)
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             self._set_fatal_error(
                 "telegram_token_lock",
                 "Another local Hermes gateway is already using this Telegram bot token.",
    ```

    ```diff
    @@ -33,7 +33,7 @@ class _RuntimeRetryableAdapter(BasePlatformAdapter):
         def __init__(self):
             super().__init__(PlatformConfig(enabled=True, token="token"), Platform.WHATSAPP)
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             return True
     
         async def disconnect(self) -> None:
    ```
  - `tests/gateway/test_runner_startup_failures.py`

    ```diff
    @@ -12,7 +12,7 @@ class _RetryableFailureAdapter(BasePlatformAdapter):
         def __init__(self):
             super().__init__(PlatformConfig(enabled=True, token="***"), Platform.TELEGRAM)
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             self._set_fatal_error(
                 "telegram_connect_error",
                 "Telegram startup failed: temporary DNS resolution failure.",
    ```

    ```diff
    @@ -34,7 +34,7 @@ class _DisabledAdapter(BasePlatformAdapter):
         def __init__(self):
             super().__init__(PlatformConfig(enabled=False, token="***"), Platform.TELEGRAM)
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             raise AssertionError("connect should not be called for disabled platforms")
     
         async def disconnect(self) -> None:
    ```

    ```diff
    @@ -51,7 +51,7 @@ class _SuccessfulAdapter(BasePlatformAdapter):
         def __init__(self):
             super().__init__(PlatformConfig(enabled=True, token="***"), Platform.DISCORD)
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             return True
     
         async def disconnect(self) -> None:
    ```

    ```diff
    @@ -467,7 +467,7 @@ class _NonRetryableFailureAdapter(BasePlatformAdapter):
         def __init__(self):
             super().__init__(PlatformConfig(enabled=True, token="***"), Platform.DISCORD)
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             self._set_fatal_error(
                 "discord-bot-token_lock",
                 "Discord bot token already in use (PID 999). Stop the other gateway first.",
    ```
  - `tests/gateway/test_send_multiple_images.py`

    ```diff
    @@ -41,7 +41,7 @@ def __init__(self):
             self.sent_animations = []
             self.sent_files = []
     
    -    async def connect(self):
    +    async def connect(self, *, is_reconnect: bool = False):
             return True
     
         async def disconnect(self):
    ```
  - `tests/gateway/test_send_retry.py`

    ```diff
    @@ -35,7 +35,7 @@ async def send(self, chat_id, content, reply_to=None, metadata=None, **kwargs) -
             self._send_calls.append((chat_id, content))
             return self._next_result()
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             return True
     
         async def disconnect(self) -> None:
    ```
  - `tests/gateway/test_session_hygiene.py`

    ```diff
    @@ -56,7 +56,7 @@ def __init__(self):
             super().__init__(PlatformConfig(enabled=True, token="[redacted:secret-assignment]"), Platform.TELEGRAM)
             self.sent = []
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             return True
     
         async def disconnect(self) -> None:
    ```
  - `tests/gateway/test_session_split_brain_11016.py`

    ```diff
    @@ -37,7 +37,7 @@
     
     
     class _StubAdapter(BasePlatformAdapter):
    -    async def connect(self):
    +    async def connect(self, *, is_reconnect: bool = False):
             pass
     
         async def disconnect(self):
    ```
  - `tests/gateway/test_status_command.py`

    ```diff
    @@ -611,7 +611,7 @@ async def fake_handler(event):
         class _ConcreteAdapter(BasePlatformAdapter):
             platform = Platform.TELEGRAM
     
    -        async def connect(self): pass
    +        async def connect(self, *, is_reconnect: bool = False): pass
             async def disconnect(self): pass
             async def send(self, chat_id, content, **kwargs): pass
             async def get_chat_info(self, chat_id): return {}
    ```

    ```diff
    @@ -692,7 +692,7 @@ async def test_post_delivery_callback_generation_snapshot_happens_after_bind():
         class _ConcreteAdapter(BasePlatformAdapter):
             platform = Platform.TELEGRAM
     
    -        async def connect(self): pass
    +        async def connect(self, *, is_reconnect: bool = False): pass
             async def disconnect(self): pass
             async def send(self, chat_id, content, **kwargs): pass
             async def get_chat_info(self, chat_id): return {}
    ```
  - `tests/gateway/test_telegram_clarify_buttons.py`

    ```diff
    @@ -403,7 +403,7 @@ def __init__(self):
                     # Skip base __init__ — we're not exercising it
                     self.sent: list = []
     
    -            async def connect(self): pass
    +            async def connect(self, *, is_reconnect: bool = False): pass
                 async def disconnect(self): pass
                 async def send(self, chat_id, content, **kw):
                     self.sent.append({"chat_id": chat_id, "content": content})
    ```

    ```diff
    @@ -436,7 +436,7 @@ class _Stub(BasePlatformAdapter):
                 name = "stub"
                 def __init__(self):
                     self.sent: list = []
    -            async def connect(self): pass
    +            async def connect(self, *, is_reconnect: bool = False): pass
                 async def disconnect(self): pass
                 async def send(self, chat_id, content, **kw):
                     self.sent.append(content)
    ```
  - `tests/gateway/test_telegram_conflict.py`

    ```diff
    @@ -425,3 +425,67 @@ def _boom():
         except (asyncio.CancelledError, Exception):
             pass
         await _cancel_heartbeat(adapter)
    +
    +
    +def _build_polling_app(monkeypatch):
    +    """Wire a mock PTB Application whose start_polling captures kwargs."""
    +    captured = {}
    +
    +    async def fake_start_polling(**kwargs):
    +        captured.update(kwargs)
    +
    +    updater = SimpleNamespace(
    +        start_polling=AsyncMock(side_effect=fake_start_polling),
    +        stop=AsyncMock(),
    +        running=True,
    +    )
    +    bot = SimpleNamespace(set_my_commands=AsyncMock(), delete_webhook=AsyncMock())
    +    app = SimpleNamespace(
    +        bot=bot,
    +        updater=updater,
    +        add_handler=MagicMock(),
    +        initialize=AsyncMock(),
    +        start=AsyncMock(),
    +    )
    +    builder = MagicMock()
    +    builder.token.return_value = builder
    +    builder.request.return_value = builder
    +    builder.get_updates_request.return_value = builder
    +    builder.build.return_value = app
    +    monkeypatch.setattr(
    +        "plugins.platforms.telegram.adapter.Application",
    +        SimpleNamespace(builder=MagicMock(return_value=builder)),
    +    )
    +    monkeypatch.setattr(
    +        "gateway.status.acquire_scoped_lock",
    +        lambda scope, identity, metadata=None: (True, None),
    +    )
    +    monkeypatch.setattr("asyncio.sleep", AsyncMock())
    +    return captured
    +
    +
    [redacted:email]
    +async def test_cold_connect_drops_pending_updates(monkeypatch):
    +    """A cold first boot (is_reconnect=False) drops the stale Bot API queue."""
    +    adapter = TelegramAdapter(PlatformConfig(enabled=True, token="***"))
    +    captured = _build_polling_app(monkeypatch)
    +
    +    ok = await adapter.connect()  # default is_reconnect=False
    +
    +    assert ok is True
    +    assert captured["drop_pending_updates"] is True
    +    await _cancel_heartbeat(adapter)
    +
    +
    [redacted:email]
    +async def test_reconnect_preserves_pending_updates(monkeypatch):
    +    """A watcher reconnect (is_reconnect=True) preserves the queue Telegram
    +    accumulated during the outage — the core of #46621."""
    +    adapter = TelegramAdapter(PlatformConfig(enabled=True, token="***"))
    +    captured = _build_polling_app(monkeypatch)
    +
    +    ok = await adapter.connect(is_reconnect=True)
    +
    +    assert ok is True
    +    assert captured["drop_pending_updates"] is False
    +    await _cancel_heartbeat(adapter)
    ```
  - `tests/gateway/test_telegram_thread_fallback.py`

    ```diff
    @@ -1127,7 +1127,7 @@ async def test_base_send_image_fallback_preserves_metadata():
         from gateway.platforms.base import BasePlatformAdapter
     
         class _ConcreteBaseAdapter(BasePlatformAdapter):
    -        async def connect(self):
    +        async def connect(self, *, is_reconnect: bool = False):
                 return True
     
             async def disconnect(self):
    ```
  - `tests/gateway/test_tool_response_drop_recovery.py`

    ```diff
    @@ -40,7 +40,7 @@ def __init__(self, platform: Platform):
             super().__init__(PlatformConfig(enabled=True, token="[redacted:secret-assignment]"), platform)
             self.sent: list[dict] = []
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
             return True
     
         async def disconnect(self) -> None:
    ```
  - `tests/gateway/test_tts_media_routing.py`

    ```diff
    @@ -22,7 +22,7 @@ class _MediaRoutingAdapter(BasePlatformAdapter):
         def __init__(self):
             super().__init__(PlatformConfig(enabled=True, token="test"), Platform.TELEGRAM)
     
    -    async def connect(self):
    +    async def connect(self, *, is_reconnect: bool = False):
             return True
     
         async def disconnect(self):
    ```
  - `tests/tools/test_send_message_tool.py`

    ```diff
    @@ -871,7 +871,7 @@ class FakeAdapter:
                 def __init__(self, _config):
                     self.connected = False
     
    -            async def connect(self):
    +            async def connect(self, *, is_reconnect: bool = False):
                     self.connected = True
                     calls.append(("connect",))
                     return True
    ```
- **#61767: fix(gateway): enforce reconnect contract across adapters**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/platforms/qqbot/adapter.py`, `infographic/gateway-reconnect-contract/infographic.png`, `plugins/platforms/wecom/callback_adapter.py`, `scripts/release.py`, `tests/gateway/test_adapter_connect_is_reconnect_contract.py`, `tests/gateway/test_qqbot.py`
  - fix hunks:
  - `gateway/platforms/qqbot/adapter.py`

    ```diff
    @@ -278,8 +278,16 @@ def enforces_own_access_policy(self) -> bool:
         # Connection lifecycle
         # ------------------------------------------------------------------
     
    -    async def connect(self) -> bool:
    -        """Authenticate, obtain gateway URL, and open the WebSocket."""
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
    +        """
    +        Authenticate, obtain gateway URL, and open the WebSocket.
    +
    +        Args:
    +            is_reconnect: False on a cold first boot; True when the
    +                reconnect watcher is re-establishing this platform after
    +                an outage. QQBot has no server-side update queue so this
    +                flag is accepted for interface conformance only.
    +        """
             if not AIOHTTP_AVAILABLE:
                 message = "QQ startup failed: aiohttp not installed"
                 self._set_fatal_error("qq_missing_dependency", message, retryable=True)
    ```
  - `plugins/platforms/wecom/callback_adapter.py`

    ```diff
    @@ -115,7 +115,13 @@ def _normalize_apps(extra: Dict[str, Any]) -> List[Dict[str, Any]]:
         # Lifecycle
         # ------------------------------------------------------------------
     
    -    async def connect(self) -> bool:
    +    async def connect(self, *, is_reconnect: bool = False) -> bool:
    +        # ``is_reconnect`` is forwarded by GatewayRunner on every retry per
    +        # the BasePlatformAdapter.connect contract. Callback adapters have
    +        # no server-side queue to preserve, so the flag is accepted-and-
    +        # ignored — but the kwarg MUST be present or the reconnect watcher
    +        # dies with TypeError and the platform silently stays offline.
    +        del is_reconnect
             if not self._apps:
                 logger.warning("[WecomCallback] No callback apps configured")
                 return False
    ```
  - `scripts/release.py`

    ```diff
    @@ -45,6 +45,8 @@
     
     # Auto-extracted from noreply emails + manual overrides
     AUTHOR_MAP = {
    +    "[redacted:email]": "lemonwan",  # PR #59430 sibling salvage (adapter reconnect contract guard)
    +    "[redacted:email]": "luxuguang-leo",  # PR #52966 salvage (QQBot reconnect contract)
         "[redacted:email]": "evelynburger",  # PR #57544 salvage (gateway: webhook payload filters + route scripts; commit under unlinked identity)
         "[redacted:email]": "SiteupAgencia",  # PR #57435 salvage (tui_gateway: back off notification poller when session is busy; #55578)
         "[redacted:email]": "rainbowgore",  # PR #59405 salvage (mcp: bound stdio initialize handshake to stop subprocess/FD leak; #59349)
    ```
  - `tests/gateway/test_adapter_connect_is_reconnect_contract.py`

    ```diff
    @@ -0,0 +1,157 @@
    +"""Regression: every platform adapter's ``connect()`` must accept the
    +``is_reconnect`` keyword-only argument.
    +
    +The gateway reconnect watcher forwards ``is_reconnect=True`` to every
    +adapter on every retry (see ``GatewayRunner._call_adapter_connect`` in
    +``gateway/run.py``). An adapter whose ``connect()`` signature omits
    +``is_reconnect`` blows up on the first reconnect attempt with::
    +
    +    TypeError: <Foo>Adapter.connect() got an unexpected
    +               keyword argument 'is_reconnect'
    +
    +…and never recovers, leaving that platform silently disconnected until
    +the operator manually restarts the gateway. This exact bug shipped for
    +``QQAdapter`` and was only discovered after messages stopped flowing on
    +the QQ channel for hours.
    +
    +To prevent this class of bug from regressing, we statically parse every
    +``adapter.py`` under ``gateway/platforms/`` and ``plugins/platforms/``
    +and assert that its ``connect()`` method accepts an ``is_reconnect``
    +keyword. Doing this via AST (rather than importing) avoids pulling every
    +platform's optional third-party SDK (aiohttp, slack_sdk, telegram,
    +matrix-nio, etc.) into the test environment.
    +"""
    +
    +from __future__ import annotations
    +
    +import ast
    +from pathlib import Path
    +
    +import pytest
    +
    +
    +REPO_ROOT = Path(__file__).resolve().parents[2]
    +
    +# Directories that hold platform adapters. Each entry is a directory
    +# whose immediate children are either ``adapter.py`` files or
    +# sub-packages that expose one.
    +ADAPTER_ROOTS = [
    +    REPO_ROOT / "gateway" / "platforms",
    +    REPO_ROOT / "plugins" / "platforms",
    +]
    +
    +
    +def _iter_adapter_files() -> list[Path]:
    +    """Every ``*adapter*.py`` under the two adapter roots.
    +
    +    We intentionally cast a wide net (any ``adapter.py`` / ``*_adapter.py``
    +    inside these trees) so a new platform can't sneak in without the
    +    contract check firing.
    +    """
    +    files: list[Path] = []
    +    for root in ADAPTER_ROOTS:
    +        if not root.is_dir():
    +            continue
    +        for path in root.rglob("*.py"):
    +            if path.name == "adapter.py" or path.stem.endswith("_adapter"):
    +                files.append(path)
    +    return sorted(files)
    +
    +
    +def _find_adapter_classes(module: ast.Module) -> list[ast.ClassDef]:
    +    """Classes that look like a platform adapter.
    +
    +    Heuristic: any class whose name ends in ``Adapter`` and that defines
    +    an ``async def connect`` method. This catches every subclass of
    +    ``BasePlatformAdapter`` in the tree today (QQAdapter, TelegramAdapter,
    +    SlackAdapter, …) without importing the base class.
    +    """
    +    hits: list[ast.ClassDef] = []
    +    for node in ast.walk(module):
    +        if not isinstance(node, ast.ClassDef):
    +            continue
    +        if not node.name.endswith("Adapter"):
    +            continue
    +        has_connect = any(
    +            isinstance(item, ast.AsyncFunctionDef) and item.name == "connect"
    +            for item in node.body
    +        )
    +        if has_connect:
    +            hits.append(node)
    +    return hits
    +
    +
    +def _connect_accepts_is_reconnect(cls: ast.ClassDef) -> bool:
    +    """True iff the class's own ``connect()`` accepts ``is_reconnect``.
    +
    +    Accepts the kwarg via:
    +    - keyword-only argument named ``is_reconnect`` (the canonical form
    +      used by ``BasePlatformAdapter``), OR
    +    - ``**kwargs`` catch-all (also safe — the kwarg is absorbed).
    +    """
    +    for item in cls.body:
    +        if not (isinstance(item, ast.AsyncFunctionDef) and item.name == "connect"):
    +            continue
    +        args = item.args
    +        if any(a.arg == "is_reconnect" for a in args.kwonlyargs):
    +            return True
    +        if any(a.arg == "is_reconnect" for a in args.args):
    +            return True
    +        if args.kwarg is not None:  # **kwargs
    +            return True
    +        return False
    +    return False
    +
    +
    +ADAPTER_FILES = _iter_adapter_files()
    +
    +
    +def test_adapter_discovery_finds_platforms():
    +    """Sanity: the discovery walker actually found a meaningful set of
    +    adapters. If this drops to a trivial number, the glob broke and the
    +    contract test below is silently passing on nothing.
    +    """
    +    assert len(ADAPTER_FILES) >= 20, (
    +        f"Expected to discover >=20 platform adapter files under "
    +        f"{[str(p) for p in ADAPTER_ROOTS]}, found {len(ADAPTER_FILES)}. "
    +        f"The discovery glob is likely broken."
    +    )
    +
    +
    [redacted:email](
    +    "adapter_file",
    +    ADAPTER_FILES,
    +    ids=lambda p: str(p.relative_to(REPO_ROOT)),
    +)
    +def test_adapter_connect_accepts_is_reconnect(adapter_file: Path):
    +    """Every ``*Adapter.connect()`` must accept ``is_reconnect``.
    +
    +    This is the contract enforced by ``BasePlatformAdapter.connect`` and
    +    relied on by ``GatewayRunner._call_adapter_connect``. Violating it
    +    silently disables the affected platform after its first reconnect.
    +    """
    +    source = adapter_file.read_text(encoding="utf-8")
    +    try:
    +        tree = ast.parse(source, filename=str(adapter_file))
    +    except SyntaxError as exc:
    +        pytest.fail(f"Could not parse {adapter_file}: {exc}")
    +
    +    classes = _find_adapter_classes(tree)
    +    if not classes:
    +        pytest.skip(
    +            f"{adapter_file.relative_to(REPO_ROOT)} has no *Adapter class "
    +            f"with an async connect() — nothing to check."
    +        )
    +
    +    offenders = [cls.name for cls in classes if not _connect_accepts_is_reconnect(cls)]
    +
    +    assert not offenders, (
    +        f"{adapter_file.relative_to(REPO_ROOT)}: the following adapter "
    +        f"class(es) define `async def connect()` WITHOUT accepting the "
    +        f"`is_reconnect` kwarg: {offenders}. "
    +        f"Add `*, is_reconnect: bool = False` to the signature "
    +        f"(matching BasePlatformAdapter.connect). "
    +        f"The gateway reconnect watcher forwards this kwarg on every "
    +        f"retry — an adapter that rejects it silently disconnects after "
    +        f"the first outage."
    +    )
    ```
  - `tests/gateway/test_qqbot.py`

    ```diff
    @@ -190,6 +190,21 @@ def test_connect_uses_redirect_guard_hook(self):
             assert kwargs.get("follow_redirects") is True
             assert kwargs.get("event_hooks", {}).get("response") == [_ssrf_redirect_guard]
     
    +    def test_connect_accepts_is_reconnect_param(self):
    +        """connect() must accept is_reconnect for interface conformance with
    +        the base adapter, which the reconnect watcher calls with
    +        is_reconnect=True."""
    +        from gateway.platforms.qqbot import QQAdapter
    +
    +        adapter = QQAdapter(_make_config(app_id="a", client_secret="b"))
    +        adapter._ensure_token = [redacted:secret-assignment]"stop after client init"))
    +
    +        # Both forms must not raise TypeError.
    +        connected_default = asyncio.run(adapter.connect())
    +        connected_explicit = asyncio.run(adapter.connect(is_reconnect=True))
    +        assert connected_default is False
    +        assert connected_explicit is False
    +
     
     # ---------------------------------------------------------------------------
     # WebSocket proxy handling
    ```
- **#116134: Telegram gateway can keep the messages sent while it was offline (#71811, salvage #105899)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `cli-config.yaml.example`, `plugins/platforms/telegram/adapter.py`, `tests/gateway/test_telegram_cold_boot_queue.py`, `website/docs/user-guide/messaging/telegram.md`
- **#41194: feat: add TELEGRAM_CATCHUP_ON_STARTUP env var toggle for pending-updates on gateway startup**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#41193`, `#116134`, `#71811`
- **#46656: fix: preserve queued Telegram updates on reconnect**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#52844`, `#46621`
- **#46671: fix(telegram): preserve update queue on reconnect after outage (#46621)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#52844`, `#46621`
- **#60831: fix(qqbot): accept is_reconnect kwarg in connect() to match base class**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `276542c729c10ff9d093760897f4c2d1256a79ce`, `8416545c423a1f0f88f1236fd999c03922318547`, `#61767`, `#61767`, `#52966`, `#61767`

## Fix status

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