{"repo":"NousResearch/hermes-agent","public_id":"35205","display_label":"Telegram gateway reconnect and shutdown reliability fixes","summary":"These issues and pull requests address bugs in the Telegram gateway around send_path_degraded not clearing after reconnect, network error recovery delegation, polling readiness timing, and missing user feedback during shutdown.","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":54076,"merged_at":"2026-06-28T08:38:18+00:00"},"resolution":{"state":"resolved","by":"pr-54076"},"freshness":{"state":"verified","last_verified_at":"2026-06-28T08:38:18+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":35205,"pr_number":54076,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-28T08:38:18+00:00"}],"members":[{"kind":"issue","number":35205,"title":"Telegram gateway: send_path_degraded stays true after successful reconnect, blocking all outbound messages","url":null,"state":"closed"},{"kind":"pr","number":35285,"title":"fix(telegram): clear send_path_degraded immediately on successful reconnect","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"2ecb6f7fe60f6a240d632bbd51bcbb25ad22c161","number":0},{"kind":"pr","value":"#35205","number":35205},{"kind":"pr","value":"#54076","number":54076}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":35286,"title":"fix(telegram): clear send_path_degraded immediately on successful reconnect","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"2ecb6f7fe60f6a240d632bbd51bcbb25ad22c161","number":0},{"kind":"pr","value":"#35205","number":35205},{"kind":"pr","value":"#54076","number":54076}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":54076,"title":"fix(telegram): clear send_path_degraded on successful reconnect (#35205)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["infographic/telegram-send-path-35205/infographic.png","plugins/platforms/telegram/adapter.py","tests/gateway/test_telegram_send_path_health.py"],"fix_hunks":[{"file":"plugins/platforms/telegram/adapter.py","hunks":["@@ -1572,6 +1572,17 @@ async def _handle_polling_network_error(self, error: Exception) -> None:\n                 self.name, attempt,\n             )\n             self._polling_network_error_count = 0\n+            # start_polling() succeeding IS the recovery signal: the long-poll\n+            # connection is live again, so clear the degraded flag immediately\n+            # rather than blocking all outbound sends for the full\n+            # HEARTBEAT_PROBE_DELAY window. The deferred probe below is a\n+            # defensive re-check — if it later detects a silent wedge (PTB\n+            # running=True but consumer task dead) it re-enters the ladder,\n+            # which re-sets _send_path_degraded. Without this clear here, a\n+            # clean reconnect leaves the flag stuck True until the 60s probe\n+            # (or forever, if the probe is never scheduled), blocking the send\n+            # path even though the bot has fully recovered. See #35205.\n+            self._send_path_degraded = False\n             # start_polling() returning is necessary but not sufficient:\n             # PTB's Updater can be left in a state where `running` is True\n             # but the underlying long-poll task is wedged on a stale httpx"]},{"file":"tests/gateway/test_telegram_send_path_health.py","hunks":["@@ -66,24 +66,71 @@ async def test_send_short_circuits_when_path_degraded():\n \n @pytest.mark.asyncio\n async def test_reconnect_storm_sets_and_heartbeat_clears_flag(monkeypatch):\n-    \"\"\"_handle_polling_network_error sets the flag; a successful heartbeat\n-    probe in _verify_polling_after_reconnect clears it.\"\"\"\n+    \"\"\"_handle_polling_network_error sets the flag while reconnecting; if the\n+    reconnect attempt itself raises (polling not yet healthy), the flag stays\n+    True until a later successful heartbeat probe in\n+    _verify_polling_after_reconnect clears it.\"\"\"\n     adapter = _make_adapter()\n     adapter._app = MagicMock()\n     adapter._app.updater = MagicMock()\n     adapter._app.updater.running = True\n     adapter._app.updater.stop = AsyncMock()\n-    adapter._app.updater.start_polling = AsyncMock()\n+    # First start_polling attempt fails — the reconnect handler must leave the\n+    # flag set (path still unhealthy) and not clear it prematurely.\n+    adapter._app.updater.start_polling = AsyncMock(side_effect=OSError(\"still down\"))\n     adapter._app.bot = MagicMock()\n     adapter._app.bot.get_me = AsyncMock(return_value=MagicMock())\n     adapter._polling_error_callback_ref = AsyncMock()\n     monkeypatch.setattr(\n         \"plugins.platforms.telegram.adapter.Update\", MagicMock(ALL_TYPES=[])\n     )\n+    # Suppress the self-rescheduled retry so the test doesn't recurse.\n+    monkeypatch.setattr(\n+        \"plugins.platforms.telegram.adapter.asyncio.ensure_future\", MagicMock()\n+    )\n \n-    await adapter._handle_polling_network_error(OSError(\"Bad Gateway\"))\n+    with patch(\"plugins.platforms.telegram.adapter.asyncio.sleep\", new_callable=AsyncMock):\n+        await adapter._handle_polling_network_error(OSError(\"Bad Gateway\"))\n+    # start_polling failed → path still degraded.\n     assert adapter._send_path_degraded is True\n \n+    # Now the deferred probe runs against a recovered (running) updater and\n+    # a responsive bot — it clears the flag.\n+    adapter._app.updater.running = True\n     with patch(\"plugins.platforms.telegram.adapter.asyncio.sleep\", new_callable=AsyncMock):\n         await adapter._verify_polling_after_reconnect()\n     assert adapter._send_path_degraded is False\n+\n+\n[redacted:email]\n+async def test_successful_reconnect_clears_flag_without_probe(monkeypatch):\n+    \"\"\"Regression for #35205: a successful start_polling() clears\n+    _send_path_degraded immediately, so outbound sends are not blocked for\n+    the full HEARTBEAT_PROBE_DELAY window (and never get stuck True if the\n+    deferred probe is never scheduled / never runs).\"\"\"\n+    adapter = _make_adapter()\n+    adapter._app = MagicMock()\n+    adapter._app.updater = MagicMock()\n+    adapter._app.updater.running = True\n+    adapter._app.updater.stop = AsyncMock()\n+    adapter._app.updater.start_polling = AsyncMock()\n+    adapter._app.bot = MagicMock()\n+    adapter._app.bot.get_me = AsyncMock(return_value=MagicMock())\n+    adapter._polling_error_callback_ref = AsyncMock()\n+    monkeypatch.setattr(\n+        \"plugins.platforms.telegram.adapter.Update\", MagicMock(ALL_TYPES=[])\n+    )\n+    # Don't let the deferred probe run — prove the clear happens in the\n+    # reconnect handler itself, not in _verify_polling_after_reconnect.\n+    monkeypatch.setattr(\n+        adapter, \"_verify_polling_after_reconnect\", AsyncMock()\n+    )\n+\n+    with patch(\"plugins.platforms.telegram.adapter.asyncio.sleep\", new_callable=AsyncMock):\n+        await adapter._handle_polling_network_error(OSError(\"Bad Gateway\"))\n+\n+    assert adapter._send_path_degraded is False\n+    assert adapter._polling_network_error_count == 0\n+    # And send() works again right away.\n+    result = await adapter.send(\"123\", \"hello\")\n+    assert result.success is True\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}