{"repo":"NousResearch/hermes-agent","public_id":"58790","display_label":"Telegram CLOSE_WAIT fd leak keepalive fix","summary":"These issues and PRs address a file-descriptor leak caused by CLOSE_WAIT connections in the Telegram transport, specifically where keepalive limits were not being applied to the fallback or general-pool transport. The work involves identifying the gap and wiring keepalive settings into the affected transports.","subsystem":null,"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":58982,"merged_at":"2026-07-05T16:38:43+00:00"},"resolution":{"state":"resolved","by":"pr-58982"},"freshness":{"state":"verified","last_verified_at":"2026-07-05T16:38:43+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":58790,"pr_number":58982,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-05T16:38:43+00:00"}],"members":[{"kind":"issue","number":58790,"title":"bug(telegram): fallback-IP transport bypasses #31599 keepalive fix → general-pool CLOSE_WAIT fd leak","url":null,"state":"closed"},{"kind":"pr","number":58803,"title":"fix(telegram): forward keepalive limits into fallback transport","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"01ee312de","number":0},{"kind":"pr","value":"#58982","number":58982}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":58804,"title":"fix(telegram): default fallback transport to platform keepalive limits (stop CLOSE_WAIT fd leak)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#58803","number":58803},{"kind":"pr","value":"#58982","number":58982}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":58982,"title":"fix(telegram): forward keepalive limits into fallback transport (stop CLOSE_WAIT fd leak)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/platforms/telegram/adapter.py","tests/gateway/test_telegram_network.py"],"fix_hunks":[{"file":"plugins/platforms/telegram/adapter.py","hunks":["@@ -3025,8 +3025,11 @@ def _env_float(name: str, default: float) -> float:\n             def _with_limits(httpx_kwargs: Optional[dict] = None) -> dict:\n                 \"\"\"Merge tuned keepalive limits into httpx client kwargs.\n \n-                A caller-supplied ``limits`` (none today) is left untouched;\n-                otherwise the CLOSE_WAIT-safe limits are injected.\n+                Used by the proxy and direct-DNS branches, where httpx honours\n+                the client-level ``limits`` kwarg. A caller-supplied ``limits``\n+                is left untouched; otherwise the CLOSE_WAIT-safe limits are\n+                injected. The fallback-IP branch does NOT use this helper — see\n+                the ``_transport_kwargs`` note below for why.\n                 \"\"\"\n                 kwargs = dict(httpx_kwargs or {})\n                 if _pool_limits is not None and \"limits\" not in kwargs:","@@ -3054,17 +3057,31 @@ def _with_limits(httpx_kwargs: Optional[dict] = None) -> dict:\n                 )\n                 # Keep request/update pools separate to reduce contention during\n                 # polling reconnect + bot API bootstrap/delete_webhook calls.\n+                # httpx ignores the client-level `limits` kwarg when a custom\n+                # `transport` is supplied (#58790).  Unlike the proxy/direct\n+                # branches (which inject limits at the client level via\n+                # `_with_limits`), this branch MUST pass the tuned limits\n+                # directly into TelegramFallbackTransport so its inner\n+                # AsyncHTTPTransport instances honour keepalive_expiry — do not\n+                # route this through `_with_limits`, httpx would discard it.\n+                _transport_kwargs: dict = {}\n+                if _pool_limits is not None:\n+                    _transport_kwargs[\"limits\"] = _pool_limits\n                 request = HTTPXRequest(\n                     **request_kwargs,\n-                    httpx_kwargs=_with_limits(\n-                        {\"transport\": TelegramFallbackTransport(fallback_ips)}\n-                    ),\n+                    httpx_kwargs={\n+                        \"transport\": TelegramFallbackTransport(\n+                            fallback_ips, **_transport_kwargs\n+                        )\n+                    },\n                 )\n                 get_updates_request = HTTPXRequest(\n                     **request_kwargs,\n-                    httpx_kwargs=_with_limits(\n-                        {\"transport\": TelegramFallbackTransport(fallback_ips)}\n-                    ),\n+                    httpx_kwargs={\n+                        \"transport\": TelegramFallbackTransport(\n+                            fallback_ips, **_transport_kwargs\n+                        )\n+                    },\n                 )\n             elif proxy_url:\n                 logger.info(\"[%s] Proxy detected; passing explicitly to HTTPXRequest: %s\", self.name, proxy_url)"]},{"file":"tests/gateway/test_telegram_network.py","hunks":["@@ -354,6 +354,37 @@ def factory(**kwargs):\n         assert len(seen_kwargs) == 2\n         assert all(\"proxy\" not in kwargs for kwargs in seen_kwargs)\n \n+    def test_forwards_limits_to_inner_transports(self, monkeypatch):\n+        \"\"\"Verify that caller-supplied limits reach the inner\n+        AsyncHTTPTransport instances (#58790).  httpx ignores the\n+        client-level limits kwarg when a custom transport is\n+        supplied, so the limits must be forwarded via transport_kwargs.\n+        \"\"\"\n+        seen_kwargs = []\n+\n+        def factory(**kwargs):\n+            seen_kwargs.append(kwargs.copy())\n+            return FakeTransport([], {})\n+\n+        for key in (\"HTTPS_PROXY\", \"HTTP_PROXY\", \"ALL_PROXY\", \"https_proxy\", \"http_proxy\", \"all_proxy\", \"TELEGRAM_PROXY\", \"NO_PROXY\", \"no_proxy\"):\n+            monkeypatch.delenv(key, raising=False)\n+        monkeypatch.setattr(tnet.httpx, \"AsyncHTTPTransport\", factory)\n+\n+        custom_limits = httpx.Limits(\n+            max_connections=42,\n+            max_keepalive_connections=10,\n+            keepalive_expiry=30.0,\n+        )\n+        transport = tnet.TelegramFallbackTransport(\n+            [\"149.154.167.220\"], limits=custom_limits\n+        )\n+\n+        # 1 primary + 1 fallback = 2 AsyncHTTPTransport instances\n+        assert len(seen_kwargs) == 2\n+        for kw in seen_kwargs:\n+            assert \"limits\" in kw\n+            assert kw[\"limits\"] is custom_limits\n+\n \n class TestFallbackTransportClose:\n     @pytest.mark.asyncio\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}