{"repo":"NousResearch/hermes-agent","public_id":"2489","display_label":"Telegram polling httpx errors and reconnect fixes","summary":"These members address a bug where Telegram polling crashes with httpx.ReadError when streaming is active, with fixes involving connection pool isolation and auto-reconnect logic.","subsystem":"gateway","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":2517,"merged_at":"2026-03-22T16:19:10+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"stale","last_verified_at":"2026-03-22T16:19:10+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":2476,"pr_number":2517,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-03-22T16:19:10+00:00"},{"issue_number":2489,"pr_number":2517,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-03-22T16:19:10+00:00"}],"members":[{"kind":"issue","number":2489,"title":"[Bug]: Telegram polling crashes with `httpx.ReadError` when streaming is enabled","url":null,"state":"closed"},{"kind":"pr","number":2490,"title":"fix: use separate httpx connection pools for Telegram polling and API…","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#2517","number":2517}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":2517,"title":"fix(telegram): auto-reconnect polling after network interruption","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/platforms/telegram.py"],"fix_hunks":[{"file":"gateway/platforms/telegram.py","hunks":["@@ -130,6 +130,7 @@ def __init__(self, config: PlatformConfig):\n         self._token_lock_identity: [redacted:secret-assignment] = None\n         self._polling_error_task: Optional[asyncio.Task] = None\n         self._polling_conflict_count: int = 0\n+        self._polling_network_error_count: int = 0\n         self._polling_error_callback_ref = None\n \n     @staticmethod","@@ -141,6 +142,80 @@ def _looks_like_polling_conflict(error: Exception) -> bool:\n             or \"another bot instance is running\" in text\n         )\n \n+    @staticmethod\n+    def _looks_like_network_error(error: Exception) -> bool:\n+        \"\"\"Return True for transient network errors that warrant a reconnect attempt.\"\"\"\n+        name = error.__class__.__name__.lower()\n+        if name in (\"networkerror\", \"timedout\", \"connectionerror\"):\n+            return True\n+        try:\n+            from telegram.error import NetworkError, TimedOut\n+            if isinstance(error, (NetworkError, TimedOut)):\n+                return True\n+        except ImportError:\n+            pass\n+        return isinstance(error, OSError)\n+\n+    async def _handle_polling_network_error(self, error: Exception) -> None:\n+        \"\"\"Reconnect polling after a transient network interruption.\n+\n+        Triggered by NetworkError/TimedOut in the polling error callback, which\n+        happen when the host loses connectivity (Mac sleep, WiFi switch, VPN\n+        reconnect, etc.).  The gateway process stays alive but the long-poll\n+        connection silently dies; without this handler the bot never recovers.\n+\n+        Strategy: exponential back-off (5s, 10s, 20s, 40s, 60s cap) up to\n+        MAX_NETWORK_RETRIES attempts, then mark the adapter retryable-fatal so\n+        the supervisor restarts the gateway process.\n+        \"\"\"\n+        if self.has_fatal_error:\n+            return\n+\n+        MAX_NETWORK_RETRIES = 10\n+        BASE_DELAY = 5\n+        MAX_DELAY = 60\n+\n+        self._polling_network_error_count += 1\n+        attempt = self._polling_network_error_count\n+\n+        if attempt > MAX_NETWORK_RETRIES:\n+            message = (\n+                \"Telegram polling could not reconnect after %d network error retries. \"\n+                \"Restarting gateway.\" % MAX_NETWORK_RETRIES\n+            )\n+            logger.error(\"[%s] %s Last error: %s\", self.name, message, error)\n+            self._set_fatal_error(\"telegram_network_error\", message, retryable=True)\n+            await self._notify_fatal_error()\n+            return\n+\n+        delay = min(BASE_DELAY * (2 ** (attempt - 1)), MAX_DELAY)\n+        logger.warning(\n+            \"[%s] Telegram network error (attempt %d/%d), reconnecting in %ds. Error: %s\",\n+            self.name, attempt, MAX_NETWORK_RETRIES, delay, error,\n+        )\n+        await asyncio.sleep(delay)\n+\n+        try:\n+            if self._app and self._app.updater and self._app.updater.running:\n+                await self._app.updater.stop()\n+        except Exception:\n+            pass\n+\n+        try:\n+            await self._app.updater.start_polling(\n+                allowed_updates=Update.ALL_TYPES,\n+                drop_pending_updates=False,\n+                error_callback=self._polling_error_callback_ref,\n+            )\n+            logger.info(\n+                \"[%s] Telegram polling resumed after network error (attempt %d)\",\n+                self.name, attempt,\n+            )\n+            self._polling_network_error_count = 0\n+        except Exception as retry_err:\n+            logger.warning(\"[%s] Telegram polling reconnect failed: %s\", self.name, retry_err)\n+            # The next network error will trigger another attempt.\n+\n     async def _handle_polling_conflict(self, error: Exception) -> None:\n         if self.has_fatal_error and self.fatal_error_code == \"telegram_polling_conflict\":\n             return","@@ -276,12 +351,15 @@ async def connect(self) -> bool:\n             loop = asyncio.get_running_loop()\n \n             def _polling_error_callback(error: Exception) -> None:\n-                if not self._looks_like_polling_conflict(error):\n-                    logger.error(\"[%s] Telegram polling error: %s\", self.name, error, exc_info=True)\n-                    return\n                 if self._polling_error_task and not self._polling_error_task.done():\n                     return\n-                self._polling_error_task = loop.create_task(self._handle_polling_conflict(error))\n+                if self._looks_like_polling_conflict(error):\n+                    self._polling_error_task = loop.create_task(self._handle_polling_conflict(error))\n+                elif self._looks_like_network_error(error):\n+                    logger.warning(\"[%s] Telegram network error, scheduling reconnect: %s\", self.name, error)\n+                    self._polling_error_task = loop.create_task(self._handle_polling_network_error(error))\n+                else:\n+                    logger.error(\"[%s] Telegram polling error: %s\", self.name, error, exc_info=True)\n \n             # Store reference for retry use in _handle_polling_conflict\n             self._polling_error_callback_ref = _polling_error_callback\n"]}]},{"kind":"pr","number":2524,"title":"fix(gateway): isolate Telegram polling from streaming edits to prevent httpx.ReadError","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"b1e2b5ea74720f9b7d7e1970f0a27dc2a043a41a","number":0},{"kind":"pr","value":"#2489","number":2489}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true}],"trust":{"default_tier":"verified","includes_suggested":false}}