{"repo":"NousResearch/hermes-agent","public_id":"58236","display_label":"Telegram connect hangs and attribute errors","summary":"A cluster of bug reports and fixes centered on two related Telegram gateway problems: startup hangs at 'Connecting (attempt 1/8)' due to asyncio/timeout issues, and an 'HTTPXRequest do_request is read-only' AttributeError from a Python 3.11 __slots__ conflict. Multiple fix iterations address timeouts, watchdogs, and instrumentation approaches.","subsystem":"gateway","counts":{"members":25,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":12,"merged_prs":5,"attempt_prs":8},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":58293},"resolution":{"state":"partially_resolved","by":"pr-58293,pr-64574,pr-71118"},"freshness":{"state":"verified","last_verified_at":"2026-07-25T02:11:05+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.07999999999999996},"fix_edges":[{"issue_number":58236,"pr_number":58293,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":63309,"pr_number":64370,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":64482,"pr_number":64574,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":64730,"pr_number":64574,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":67498,"pr_number":70884,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":67498,"pr_number":71118,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":58236,"title":"Gateway hangs indefinitely on Telegram connect (attempt 1/8) — asyncio.wait_for never fires under s6 supervision","url":null,"state":"closed"},{"kind":"issue","number":59202,"title":"Telegram gateway connect() hangs indefinitely on first attempt at container boot — 30s asyncio.wait_for timeout never fires","url":null,"state":"closed"},{"kind":"issue","number":62704,"title":"Gateway deadlocks connecting to Telegram (hangs at 'Connecting (attempt 1/8)'), both polling and webhook","url":null,"state":"closed"},{"kind":"issue","number":63309,"title":"Telegram gateway hangs at 'Connecting to Telegram (attempt 1/8)' after upgrade; network is reachable","url":null,"state":"closed"},{"kind":"issue","number":64482,"title":"[Bug]: Telegram won't connect  'HTTPXRequest' object attribute 'do_request' is read-only","url":null,"state":"closed"},{"kind":"issue","number":64566,"title":"Telegram startup fails with 'HTTPXRequest' object attribute 'do_request' is read-only (v0.18.2, Docker)","url":null,"state":"closed"},{"kind":"issue","number":64694,"title":"Telegram adapter crashes on startup: HTTPXRequest.do_request is read-only under python-telegram-bot 22.6","url":null,"state":"closed"},{"kind":"issue","number":64730,"title":"[Bug]: ✗ telegram failed to connect","url":null,"state":"closed"},{"kind":"issue","number":67485,"title":"Telegram start_polling() wedges on initial poll acquisition (free slot) — gateway alive/heartbeat fresh, never acquires, 30s guard never fires (0.18.2)","url":null,"state":"closed"},{"kind":"issue","number":67498,"title":"Telegram gateway hangs at 'Connecting to Telegram (attempt 1/8)' even after applying the #63309/#64370 TELEGRAM_FALLBACK_IPS workaround — py-spy shows all threads idle, not blocked","url":null,"state":"closed"},{"kind":"issue","number":67817,"title":"Telegram fails to connect: 'HTTPXRequest' object attribute 'do_request' is read-only","url":null,"state":"closed"},{"kind":"issue","number":68465,"title":"Bug: Telegram gateway stuck at \"Connecting to Telegram (attempt 1/8)\" on Windows after clean run","url":null,"state":"closed"},{"kind":"pr","number":58250,"title":"fix(telegram): use wall deadline for init timeout","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"d50aae0e3","number":0},{"kind":"pr","value":"#58293","number":58293},{"kind":"pr","value":"#58236","number":58236}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":58293,"title":"fix(telegram): wall-deadline init timeout + shut down abandoned init app","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/platforms/telegram/adapter.py","scripts/release.py","tests/gateway/test_telegram_init_deadline.py"]},{"kind":"pr","number":63345,"title":"fix(telegram): diagnose blocked-loop init hangs, unbind DoH discovery from system DNS","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#64370","number":64370},{"kind":"pr","value":"#63309","number":63309}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":64370,"title":"fix(telegram): diagnose blocked-loop init hangs, unbind DoH discovery from system DNS","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/platforms/telegram/adapter.py","plugins/platforms/telegram/telegram_network.py","tests/gateway/test_telegram_init_deadline.py","tests/gateway/test_telegram_network.py"],"fix_hunks":[{"file":"plugins/platforms/telegram/adapter.py","hunks":["@@ -9,6 +9,7 @@\n \n import asyncio\n import dataclasses\n+import faulthandler\n import inspect\n import json\n import logging","@@ -45,6 +46,35 @@ def _consume_abandoned_task(task: asyncio.Task) -> None:\n         logger.debug(\"Abandoned Telegram init task failed after timeout\", exc_info=True)\n \n \n+# Grace period after the wall-clock deadline fires: if the event loop still\n+# hasn't processed the expiry callback by then, the loop thread itself is\n+# blocked in a synchronous call — the exact state in which every asyncio-based\n+# timeout (including this helper's own expiry hand-off) goes silent, so the\n+# gateway hangs at \"attempt 1/8\" with no further output (#63309).\n+_LOOP_BLOCKED_DUMP_GRACE = 5.0\n+\n+\n+def _dump_loop_blocked_diagnostics(timeout: float, grace: float) -> None:\n+    \"\"\"Emit diagnostics from the deadline timer thread when the loop is stuck.\n+\n+    Runs OFF the event loop, so it works precisely when the loop cannot. The\n+    faulthandler dump names the frame the loop thread is blocked in — the one\n+    piece of information #63309-class hangs otherwise never surface.\n+    \"\"\"\n+    logger.warning(\n+        \"[Telegram] init deadline (%.0fs) expired but the event loop has not \"\n+        \"processed the expiry after a further %.0fs — the loop thread appears \"\n+        \"BLOCKED in a synchronous call, which is why no timeout fires (#63309). \"\n+        \"Dumping all thread stacks to stderr to identify the blocking frame.\",\n+        timeout,\n+        grace,\n+    )\n+    try:\n+        faulthandler.dump_traceback(all_threads=True)\n+    except Exception:\n+        logger.debug(\"faulthandler traceback dump failed\", exc_info=True)\n+\n+\n async def _await_with_thread_deadline(awaitable, timeout: float, *, on_abandon=None):\n     \"\"\"Await with a wall-clock deadline that does not depend on loop timers.\n ","@@ -66,17 +96,34 @@ async def _await_with_thread_deadline(awaitable, timeout: float, *, on_abandon=N\n     task = asyncio.ensure_future(awaitable)\n     loop = asyncio.get_running_loop()\n     deadline = loop.create_future()\n+    # Set the moment the loop actually runs the expiry callback (or the helper\n+    # exits normally). threading.Event so the watchdog thread can read it\n+    # without touching asyncio state from off-loop.\n+    loop_processed_expiry = threading.Event()\n \n     def _mark_expired() -> None:\n+        loop_processed_expiry.set()\n         if not deadline.done():\n             deadline.set_result(None)\n \n     def _expire_from_thread() -> None:\n         loop.call_soon_threadsafe(_mark_expired)\n \n+    def _watchdog_check() -> None:\n+        # The deadline fired _LOOP_BLOCKED_DUMP_GRACE ago but the loop never\n+        # ran _mark_expired: the loop thread is stuck in a synchronous call.\n+        # Diagnose from this thread — the loop can't.\n+        if not loop_processed_expiry.is_set():\n+            _dump_loop_blocked_diagnostics(timeout, _LOOP_BLOCKED_DUMP_GRACE)\n+\n     timer = threading.Timer(max(timeout, 0.0), _expire_from_thread)\n     timer.daemon = True\n     timer.start()\n+    watchdog = threading.Timer(\n+        max(timeout, 0.0) + _LOOP_BLOCKED_DUMP_GRACE, _watchdog_check\n+    )\n+    watchdog.daemon = True\n+    watchdog.start()\n     try:\n         done, _ = await asyncio.wait(\n             {task, deadline},","@@ -99,6 +146,11 @@ def _expire_from_thread() -> None:\n         raise asyncio.TimeoutError()\n     finally:\n         timer.cancel()\n+        watchdog.cancel()\n+        # cancel() cannot stop a Timer whose callback is already running;\n+        # setting the event closes that race so a completed await can never\n+        # be misreported as a blocked loop.\n+        loop_processed_expiry.set()\n \n \n async def _run_abandon_cleanup(on_abandon) -> None:"]},{"file":"plugins/platforms/telegram/telegram_network.py","hunks":["@@ -205,14 +205,24 @@ async def discover_fallback_ips() -> list[str]:\n     \"\"\"\n     async with httpx.AsyncClient(timeout=httpx.Timeout(_DOH_TIMEOUT)) as client:\n         doh_tasks = [_query_doh_provider(client, p) for p in _DOH_PROVIDERS]\n-        system_dns_task = asyncio.to_thread(_resolve_system_dns)\n-        results = await asyncio.gather(system_dns_task, *doh_tasks, return_exceptions=True)\n-\n-    # results[0] = system DNS IPs (set), results[1:] = DoH IP lists\n-    system_ips: set[str] = results[0] if isinstance(results[0], set) else set()\n+        system_dns_task = asyncio.ensure_future(asyncio.to_thread(_resolve_system_dns))\n+        results = await asyncio.gather(*doh_tasks, return_exceptions=True)\n+\n+    # The system-resolver leg runs socket.getaddrinfo in a worker thread with\n+    # no timeout of its own — a wedged OS resolver (broken VPN/DNS) can sit for\n+    # minutes. Its result only feeds the no-usable-answers log line below, so\n+    # it must never gate discovery: bound it and move on (#63309). The DoH legs\n+    # are already bounded by the client timeout above.\n+    system_ips: set[str] = set()\n+    try:\n+        system_result = await asyncio.wait_for(system_dns_task, timeout=_DOH_TIMEOUT)\n+        if isinstance(system_result, set):\n+            system_ips = system_result\n+    except Exception:\n+        logger.debug(\"System-DNS resolution for %s did not complete in time\", _TELEGRAM_API_HOST)\n \n     doh_ips: list[str] = []\n-    for r in results[1:]:\n+    for r in results:\n         if isinstance(r, list):\n             doh_ips.extend(r)\n "]},{"file":"tests/gateway/test_telegram_init_deadline.py","hunks":["@@ -199,3 +199,79 @@ async def test_shutdown_abandoned_app_handles_none_and_missing_requests():\n     app.shutdown = AsyncMock(side_effect=RuntimeError(\"still running\"))\n     app.bot = None\n     await tg_adapter._shutdown_abandoned_app(app)  # must not raise\n+\n+\n[redacted:email]\n+async def test_blocked_loop_after_expiry_dumps_diagnostics(monkeypatch):\n+    \"\"\"#63309: when the loop thread is stuck in a synchronous call, the expiry\n+    callback never runs and every asyncio timeout goes silent. The off-loop\n+    watchdog must detect that state and emit diagnostics from its own thread.\"\"\"\n+    import asyncio as _asyncio\n+    import time as _time\n+\n+    dumps = []\n+    monkeypatch.setattr(\n+        tg_adapter,\n+        \"_dump_loop_blocked_diagnostics\",\n+        lambda timeout, grace: dumps.append((timeout, grace)),\n+    )\n+    monkeypatch.setattr(tg_adapter, \"_LOOP_BLOCKED_DUMP_GRACE\", 0.15)\n+\n+    hung = _asyncio.get_running_loop().create_future()  # never completes\n+    task = _asyncio.ensure_future(\n+        tg_adapter._await_with_thread_deadline(hung, timeout=0.05)\n+    )\n+    # Let the helper start its deadline + watchdog timers…\n+    await _asyncio.sleep(0)\n+    # …then block the event loop straight through deadline (0.05s) AND the\n+    # watchdog grace (0.15s): call_soon_threadsafe stays queued, exactly like\n+    # a sync call pinning the loop during Application.initialize().\n+    _time.sleep(0.5)\n+    with pytest.raises(_asyncio.TimeoutError):\n+        await task\n+\n+    assert dumps == [(0.05, 0.15)]\n+    hung.cancel()\n+\n+\n[redacted:email]\n+async def test_responsive_loop_expiry_does_not_dump(monkeypatch):\n+    \"\"\"A normal timeout on a responsive loop must not trigger the watchdog.\"\"\"\n+    import asyncio as _asyncio\n+\n+    dumps = []\n+    monkeypatch.setattr(\n+        tg_adapter,\n+        \"_dump_loop_blocked_diagnostics\",\n+        lambda timeout, grace: dumps.append((timeout, grace)),\n+    )\n+    monkeypatch.setattr(tg_adapter, \"_LOOP_BLOCKED_DUMP_GRACE\", 0.1)\n+\n+    hung = _asyncio.get_running_loop().create_future()\n+    with pytest.raises(_asyncio.TimeoutError):\n+        await tg_adapter._await_with_thread_deadline(hung, timeout=0.05)\n+    # Give the (cancelled) watchdog window time to have fired if it were going to.\n+    await _asyncio.sleep(0.3)\n+    assert dumps == []\n+    hung.cancel()\n+\n+\n[redacted:email]\n+async def test_completed_await_never_reports_blocked_loop(monkeypatch):\n+    \"\"\"Success before the deadline must cancel the watchdog (no false dump).\"\"\"\n+    import asyncio as _asyncio\n+\n+    dumps = []\n+    monkeypatch.setattr(\n+        tg_adapter,\n+        \"_dump_loop_blocked_diagnostics\",\n+        lambda timeout, grace: dumps.append((timeout, grace)),\n+    )\n+    monkeypatch.setattr(tg_adapter, \"_LOOP_BLOCKED_DUMP_GRACE\", 0.05)\n+\n+    async def _quick():\n+        return \"ok\"\n+\n+    assert await tg_adapter._await_with_thread_deadline(_quick(), timeout=0.2) == \"ok\"\n+    await _asyncio.sleep(0.4)\n+    assert dumps == []"]},{"file":"tests/gateway/test_telegram_network.py","hunks":["@@ -705,3 +705,54 @@ async def test_invalid_ip_in_doh_response_skipped(self, monkeypatch):\n \n         ips = await tnet.discover_fallback_ips()\n         assert ips == [\"149.154.167.220\"]\n+\n+    @pytest.mark.asyncio\n+    async def test_hung_system_dns_does_not_gate_doh_results(self, monkeypatch):\n+        \"\"\"#63309: socket.getaddrinfo has no timeout of its own — a wedged OS\n+        resolver must not stall discovery. DoH answers must come back promptly\n+        even while the system-DNS worker thread is still hanging.\"\"\"\n+        import time as _time\n+\n+        self._patch_doh(monkeypatch, {\n+            \"https://dns.google\": (200, _doh_answer(\"149.154.167.220\")),\n+            \"https://cloudflare-dns.com\": (200, _doh_answer()),\n+        }, system_dns_ips=[\"149.154.166.110\"])\n+        monkeypatch.setattr(tnet, \"_DOH_TIMEOUT\", 0.2)\n+\n+        def _hung_getaddrinfo(*a, **kw):\n+            _time.sleep(1.5)  # far beyond the discovery bound\n+            raise OSError(\"resolver wedged\")\n+\n+        monkeypatch.setattr(tnet.socket, \"getaddrinfo\", _hung_getaddrinfo)\n+\n+        start = _time.monotonic()\n+        ips = await tnet.discover_fallback_ips()\n+        elapsed = _time.monotonic() - start\n+\n+        assert ips == [\"149.154.167.220\"]\n+        assert elapsed < 1.0, f\"discovery gated on hung system DNS ({elapsed:.2f}s)\"\n+\n+    @pytest.mark.asyncio\n+    async def test_hung_system_dns_with_no_doh_answers_bounded_seed_fallback(self, monkeypatch):\n+        \"\"\"Worst case — resolver wedged AND no DoH answers — must still return\n+        the seed list within the bound instead of hanging connect().\"\"\"\n+        import time as _time\n+\n+        self._patch_doh(monkeypatch, {\n+            \"https://dns.google\": (200, {\"Status\": 0}),\n+            \"https://cloudflare-dns.com\": (200, {\"garbage\": True}),\n+        }, system_dns_ips=[\"149.154.166.110\"])\n+        monkeypatch.setattr(tnet, \"_DOH_TIMEOUT\", 0.2)\n+\n+        def _hung_getaddrinfo(*a, **kw):\n+            _time.sleep(1.5)\n+            raise OSError(\"resolver wedged\")\n+\n+        monkeypatch.setattr(tnet.socket, \"getaddrinfo\", _hung_getaddrinfo)\n+\n+        start = _time.monotonic()\n+        ips = await tnet.discover_fallback_ips()\n+        elapsed = _time.monotonic() - start\n+\n+        assert ips == tnet._SEED_FALLBACK_IPS\n+        assert elapsed < 1.0, f\"seed fallback gated on hung system DNS ({elapsed:.2f}s)\"\n"]}]},{"kind":"pr","number":64486,"title":"fix(telegram): wrap Application.initialize() in wait_for to fix connect hang (#63309)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#63309","number":63309},{"kind":"pr","value":"#64370","number":64370},{"kind":"pr","value":"#63345","number":63345}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":64506,"title":"fix(telegram): instrument getUpdates request via subclass re-tag, not slotted attr (#64482)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#64574","number":64574}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":64574,"title":"fix(telegram): slots-safe getUpdates instrumentation for Python 3.13 (#64482)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/platforms/telegram/adapter.py","tests/test_telegram_polling_progress_ptb.py"],"fix_hunks":[{"file":"plugins/platforms/telegram/adapter.py","hunks":["@@ -1969,34 +1969,62 @@ def _record_polling_progress(self, generation: int) -> None:\n         self._polling_conflict_count = 0\n         self._send_path_degraded = False\n \n+    def _observe_polling_request_result(self, request, generation, result):\n+        \"\"\"Record getUpdates progress from an observed do_request result.\n+\n+        Purely observational: PTB still parses the untouched payload and owns\n+        any resulting exception. Kept as its own method so the observation\n+        logic is shared and independently testable.\n+        \"\"\"\n+        status_code, payload = result\n+        if generation is None or not (200 <= status_code < 300):\n+            return\n+        try:\n+            # Use the request's own parser so health observation agrees\n+            # exactly with PTB's authoritative response handling (e.g.\n+            # UTF-8 replacement decoding and BOM rejection).\n+            envelope = request.parse_json_payload(payload)\n+        except Exception:\n+            return\n+        if (\n+            isinstance(envelope, dict)\n+            and envelope.get(\"ok\") is True\n+            and \"result\" in envelope\n+        ):\n+            self._record_polling_progress(generation)\n+\n     def _instrument_polling_request(self, request):\n-        \"\"\"Wrap one dedicated PTB getUpdates request with progress tracking.\"\"\"\n-        do_request = request.do_request\n-\n-        async def _do_request(*args, **kwargs):\n-            generation = _POLLING_GENERATION_CONTEXT.get()\n-            result = await do_request(*args, **kwargs)\n-            status_code, payload = result\n-            if generation is not None and 200 <= status_code < 300:\n-                try:\n-                    # Use the request's own parser so health observation agrees\n-                    # exactly with PTB's authoritative response handling (e.g.\n-                    # UTF-8 replacement decoding and BOM rejection).\n-                    envelope = request.parse_json_payload(payload)\n-                except Exception:\n-                    # Instrumentation is observational: PTB still parses the\n-                    # untouched payload and owns the resulting exception.\n-                    pass\n-                else:\n-                    if (\n-                        isinstance(envelope, dict)\n-                        and envelope.get(\"ok\") is True\n-                        and \"result\" in envelope\n-                    ):\n-                        self._record_polling_progress(generation)\n-            return result\n-\n-        request.do_request = _do_request\n+        \"\"\"Instrument one dedicated PTB getUpdates request with progress tracking.\n+\n+        PTB's request classes (``BaseRequest`` / ``HTTPXRequest``) use\n+        ``__slots__``. On Python 3.13 their instances no longer carry a\n+        ``__dict__`` (the ``AbstractAsyncContextManager`` MRO stopped yielding\n+        one), so ``request.do_request = wrapper`` raises\n+        ``AttributeError: 'HTTPXRequest' object attribute 'do_request' is\n+        read-only`` and the whole Telegram connect fails (#64482). It only\n+        appeared to work on Python 3.12, where those instances still had a\n+        ``__dict__``.\n+\n+        Instead of monkey-patching the instance, re-tag it to a thin subclass\n+        that overrides ``do_request``. This is portable across Python versions\n+        and works for both the real request and the test doubles. The subclass\n+        declares ``__slots__ = ()`` so its instance layout stays identical to\n+        the base, which is what makes the ``__class__`` swap legal on a slotted\n+        instance.\n+        \"\"\"\n+        adapter = self\n+        base_cls = type(request)\n+\n+        class _InstrumentedPollingRequest(base_cls):\n+            __slots__ = ()\n+\n+            async def do_request(self, *args, **kwargs):\n+                generation = _POLLING_GENERATION_CONTEXT.get()\n+                result = await super().do_request(*args, **kwargs)\n+                adapter._observe_polling_request_result(self, generation, result)\n+                return result\n+\n+        request.__class__ = _InstrumentedPollingRequest\n         return request\n \n     async def _start_polling_once("]},{"file":"tests/test_telegram_polling_progress_ptb.py","hunks":["@@ -92,6 +92,34 @@ async def do_request(self, url, method, request_data=None, **_kwargs):\n         return 200, self.payload\n \n \n+class _SlottedEnvelopeRequest(BaseRequest):\n+    \"\"\"A getUpdates request with no instance ``__dict__``.\n+\n+    Reproduces PTB's real HTTPXRequest shape on Python 3.13, where every\n+    class in the MRO defines ``__slots__`` and instances therefore reject an\n+    instance-attribute ``do_request`` monkey-patch as \"read-only\" (#64482).\n+    ``__slots__`` names the payload so the double needs no ``__dict__``.\n+    \"\"\"\n+\n+    __slots__ = (\"_payload\",)\n+\n+    def __init__(self, payload):\n+        self._payload = payload\n+\n+    @property\n+    def read_timeout(self):\n+        return 10\n+\n+    async def initialize(self):\n+        return None\n+\n+    async def shutdown(self):\n+        return None\n+\n+    async def do_request(self, url, method, request_data=None, **_kwargs):\n+        return 200, self._payload\n+\n+\n async def _cancel_task(task):\n     if task is None or task.done():\n         return","@@ -226,6 +254,47 @@ async def test_real_base_request_valid_success_envelope_records_progress():\n     assert adapter._send_path_degraded is False\n \n \n[redacted:email]\n+async def test_slotted_request_instrumented_without_read_only_error():\n+    \"\"\"Instrumenting a __slots__ request must not raise, and must still record.\n+\n+    Regression for #64482: PTB's HTTPXRequest carries no instance ``__dict__``\n+    on Python 3.13, so the old ``request.do_request = wrapper`` monkey-patch\n+    raised ``AttributeError: ... 'do_request' is read-only`` and broke every\n+    Telegram connect. The subclass re-tag must instrument such an instance and\n+    still observe getUpdates progress.\n+    \"\"\"\n+    adapter = TelegramAdapter(PlatformConfig(enabled=True, token=\"[redacted:secret-assignment]\"))\n+    generation, progress = adapter._begin_polling_generation()\n+    adapter._polling_network_error_count = 4\n+    adapter._polling_conflict_count = 3\n+\n+    request = _SlottedEnvelopeRequest(b'{\"ok\":true,\"result\":[]}')\n+    # Guard the premise where the runtime actually enforces it: PTB's request\n+    # MRO is only fully slotted on Python 3.13+ (contextlib's\n+    # AbstractAsyncContextManager gained ``__slots__ = ()`` there). On older\n+    # runtimes the instance still carries a ``__dict__`` and the legacy\n+    # monkey-patch is accepted, so the read-only premise cannot be asserted.\n+    if not hasattr(request, \"__dict__\"):\n+        with pytest.raises(AttributeError, match=\"read-only\"):\n+            request.do_request = lambda *a, **k: None\n+\n+    instrumented = adapter._instrument_polling_request(request)\n+    context_token = [redacted:secret-assignment]\n+    try:\n+        result = await instrumented.post(\n+            \"https://api.telegram.org/bot-token/getUpdates\"\n+        )\n+    finally:\n+        tg_adapter._POLLING_GENERATION_CONTEXT.reset(context_token)\n+\n+    assert result == []\n+    assert progress.is_set()\n+    assert adapter._polling_network_error_count == 0\n+    assert adapter._polling_conflict_count == 0\n+    assert adapter._send_path_degraded is False\n+\n+\n @pytest.mark.asyncio\n async def test_real_ptb_stop_cleanup_cannot_heal_recovery_generation():\n     assert tg_adapter.TELEGRAM_AVAILABLE is True\n"]}]},{"kind":"pr","number":64639,"title":"fix(telegram): attach polling instrumentation without an instance attribute","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#64506","number":64506},{"kind":"pr","value":"#64381","number":64381},{"kind":"pr","value":"#64506","number":64506},{"kind":"pr","value":"#64506","number":64506},{"kind":"pr","value":"#64574","number":64574},{"kind":"pr","value":"#64506","number":64506},{"kind":"pr","value":"#64574","number":64574},{"kind":"pr","value":"#64639","number":64639},{"kind":"pr","value":"#64482","number":64482},{"kind":"pr","value":"#64566","number":64566}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":64773,"title":"fix(telegram): catch AttributeError from PTB python-3.11 __slots__ conflict with actionable message","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"f5f79ff1b","number":0},{"kind":"pr","value":"#64574","number":64574},{"kind":"pr","value":"#64574","number":64574},{"kind":"pr","value":"#64482","number":64482},{"kind":"pr","value":"#64730","number":64730}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":69240,"title":"fix(telegram): require initial polling readiness","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"83e30e371f","number":0},{"kind":"commit","value":"9f8810a693","number":0},{"kind":"commit","value":"c8ff720508","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":70546,"title":"fix(telegram): prevent connect hang with retry watchdog and fresh app per attempt (#67498)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#70884","number":70884}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":70884,"title":"fix(telegram+tools): prevent connect hang with retry watchdog and fresh app per attempt (#67498, #68915)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["contributors/emails/[redacted:email]","plugins/platforms/telegram/adapter.py","tests/tools/test_process_registry.py","tools/process_registry.py"]},{"kind":"pr","number":71118,"title":"fix(telegram): require proven polling readiness on cold start","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["contributors/emails/[redacted:email]","gateway/run.py","plugins/platforms/telegram/adapter.py","tests/gateway/test_platform_reconnect.py","tests/gateway/test_telegram_conflict.py","tests/gateway/test_telegram_polling_progress.py","tests/gateway/test_telegram_start_polling_timeout.py"]}],"trust":{"default_tier":"verified","includes_suggested":false}}