From 9f0309504496a82d15b07aef30f8f7bf84cab660 Mon Sep 17 00:00:00 2001 From: Brett Date: Wed, 1 Jul 2026 04:39:37 -0700 Subject: [PATCH] fix(telegram): cap initialize() with per-attempt timeout so unreachable fallback IPs can't hang startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap each Telegram initialize() attempt in asyncio.wait_for(HERMES_TELEGRAM_INIT_TIMEOUT, default 30s). When api.telegram.org and all fallback IPs are unreachable, the connect chain has no outer bound, so a single initialize() blocks for minutes and the retry-on-exception loop never fires — the gateway appears to hang after the banner. The timeout guarantees each attempt is bounded, then retries with backoff, then fails with an actionable error. Also adds WARNING-level progress logs before DoH discovery and each connect attempt (visible at default log level). Salvaged onto plugins/platforms/telegram/adapter.py (Telegram moved from gateway/platforms/ since the PR was opened). Adds env var to docs + AUTHOR_MAP. Co-authored-by: Hermes Agent <127238744+teknium1@users.noreply.github.com> --- plugins/platforms/telegram/adapter.py | 26 +++++++++++++++++-- scripts/release.py | 1 + .../docs/reference/environment-variables.md | 1 + 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/plugins/platforms/telegram/adapter.py b/plugins/platforms/telegram/adapter.py index 91ec2a40b0d..172e0a33cce 100644 --- a/plugins/platforms/telegram/adapter.py +++ b/plugins/platforms/telegram/adapter.py @@ -2840,6 +2840,7 @@ class TelegramAdapter(BasePlatformAdapter): disable_fallback = (os.getenv("HERMES_TELEGRAM_DISABLE_FALLBACK_IPS", "").strip().lower() in {"1", "true", "yes", "on"}) fallback_ips = self._fallback_ips() if not fallback_ips: + logger.warning("[%s] Discovering Telegram API fallback IPs via DNS-over-HTTPS…", self.name) fallback_ips = await discover_fallback_ips() logger.info( "[%s] Auto-discovered Telegram fallback IPs: %s", @@ -2909,16 +2910,37 @@ class TelegramAdapter(BasePlatformAdapter): # Handle inline keyboard button callbacks (update prompts) self._app.add_handler(CallbackQueryHandler(self._handle_callback_query)) - # Start polling — retry initialize() for transient TLS resets + # Start polling — retry initialize() for transient TLS resets. + # Each attempt is capped by _init_timeout so a single unreachable + # fallback-IP chain can't block startup indefinitely. try: from telegram.error import NetworkError, TimedOut except ImportError: NetworkError = TimedOut = OSError # type: ignore[misc,assignment] _max_connect = 8 + _init_timeout = _env_float("HERMES_TELEGRAM_INIT_TIMEOUT", 30.0) for _attempt in range(_max_connect): try: - await self._app.initialize() + logger.warning( + "[%s] Connecting to Telegram (attempt %d/%d)…", + self.name, _attempt + 1, _max_connect, + ) + await asyncio.wait_for(self._app.initialize(), timeout=_init_timeout) break + except asyncio.TimeoutError: + if _attempt < _max_connect - 1: + wait = min(2 ** _attempt, 15) + logger.warning( + "[%s] Connect attempt %d/%d timed out after %.0fs — retrying in %ds", + self.name, _attempt + 1, _max_connect, _init_timeout, wait, + ) + await asyncio.sleep(wait) + else: + raise OSError( + f"Telegram initialization timed out after {_max_connect} attempts " + f"({_init_timeout:.0f}s each). Check network connectivity to api.telegram.org " + f"or set HERMES_TELEGRAM_HTTP_CONNECT_TIMEOUT to a lower value." + ) except (NetworkError, TimedOut, OSError) as init_err: if _attempt < _max_connect - 1: wait = min(2 ** _attempt, 15) diff --git a/scripts/release.py b/scripts/release.py index f9da0f70340..dc6fd0bb2e1 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -46,6 +46,7 @@ ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json" # Auto-extracted from noreply emails + manual overrides AUTHOR_MAP = { "ud@arubangles.com": "udatny", # PR #29433 salvage (subdirectory_hints: catch RuntimeError from Path.expanduser()/Path.home() so a literal ~ in tool-call args — e.g. LLM "~500-700" or ~unknownuser — can't escape the hint walker and crash the conversation loop) + "brett@personalfinancelab.com": "brett539", # PR #49369 salvage (cap Telegram initialize() with asyncio.wait_for(HERMES_TELEGRAM_INIT_TIMEOUT, default 30s) per attempt so an unreachable fallback-IP connect chain can't block gateway startup indefinitely; add WARNING progress logs before DoH discovery and each connect attempt) "mac-studio@Fabios-Mac-Studio.local": "valenteff", # PR #53277 salvage (macOS launchd reload: retry bootstrap via _launchctl_bootstrap until launchctl-list confirms registration or the restart-drain window elapses; retry TimeoutExpired not just CalledProcessError; log persistent orphans) "gary@bitcryptic.com": "bitcryptic-gw", # PR #53997 salvage (Matrix E2EE: resolve device_id via query_keys({mxid: []}) when whoami returns none; guard verification call sites so query_keys is never sent [null]; reset _device_id_unverified at connect() start; disconnect before reconnect) "hodlclone@gmail.com": "HODLCLONE", # PR #49351 salvage (Nous Portal token resilience: rotate refresh tokens write-through to the source auth store in profile mode, skip Nous fallback when no local token, sync gateway session model after fallback) diff --git a/website/docs/reference/environment-variables.md b/website/docs/reference/environment-variables.md index 35b34217cba..9f4aa214453 100644 --- a/website/docs/reference/environment-variables.md +++ b/website/docs/reference/environment-variables.md @@ -672,6 +672,7 @@ Advanced per-platform knobs for throttling the outbound message batcher. Most us | `HERMES_TELEGRAM_MEDIA_BATCH_DELAY_SECONDS` | Grace window before flushing queued Telegram media (default: `0.6`). | | `HERMES_TELEGRAM_FOLLOWUP_GRACE_SECONDS` | Delay before sending a follow-up after the agent finishes, to avoid racing the last stream chunk. | | `HERMES_TELEGRAM_HTTP_CONNECT_TIMEOUT` / `_READ_TIMEOUT` / `_WRITE_TIMEOUT` / `_POOL_TIMEOUT` | Override the underlying `python-telegram-bot` HTTP timeouts (seconds). | +| `HERMES_TELEGRAM_INIT_TIMEOUT` | Per-attempt cap (seconds) on the Telegram `initialize()` connect chain during gateway startup, so an unreachable fallback-IP chain can't block startup indefinitely (default: `30`). | | `HERMES_TELEGRAM_HTTP_POOL_SIZE` | Max concurrent HTTP connections to the Telegram API. | | `HERMES_TELEGRAM_DISABLE_FALLBACK_IPS` | Disable the hard-coded Cloudflare fallback IPs used when DNS fails (`true`/`false`). | | `HERMES_DISCORD_TEXT_BATCH_DELAY_SECONDS` | Grace window before flushing a queued Discord text chunk (default: `0.6`). |