fix(telegram): cap initialize() with per-attempt timeout so unreachable fallback IPs can't hang startup

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>
This commit is contained in:
Brett 2026-07-01 04:39:37 -07:00 committed by Teknium
parent d739194926
commit 9f03095044
3 changed files with 26 additions and 2 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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`). |