diff --git a/gateway/run.py b/gateway/run.py index bed0cbff762c..c5b5516cd6f9 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -1589,6 +1589,18 @@ if _config_path.exists(): _trust_recent_seconds = _gateway_cfg.get("trust_recent_files_seconds") if _trust_recent_seconds is not None: os.environ["HERMES_MEDIA_TRUST_RECENT_SECONDS"] = str(_trust_recent_seconds) + # Bridge gateway.platform_connect_timeout → the internal env var the + # connect path + Discord adapter ready-wait both read (#19776). + # Unlike the agent.*/display.* bridges above (config-authoritative), + # this env var is the manual-override escape hatch, so it WINS if + # already set explicitly; otherwise config.yaml supplies the value. + if ( + "platform_connect_timeout" in _gateway_cfg + and not os.environ.get("HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT", "").strip() + ): + os.environ["HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT"] = str( + _gateway_cfg["platform_connect_timeout"] + ) except Exception as _bridge_err: # Previously this was silent (`except Exception: pass`), which # hid partial bridge failures and let .env defaults shadow diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 5c568fd24fd2..5b2ad0fd9270 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -2686,6 +2686,17 @@ DEFAULT_CONFIG = { # Gateway settings — control how messaging platforms (Telegram, Discord, # Slack, etc.) deliver agent-produced files as native attachments. "gateway": { + # Seconds the gateway waits for a single messaging platform to finish + # connecting during startup (and on reconnect). Discord in particular + # can blow past the old fixed 30s when an account has many slash + # commands to sync (#19776: 90-173 skills → ~28-31s sync). Raise this + # if your gateway hits "discord connect timed out" / "Timeout waiting + # for connection to Discord" restart loops. ``0`` or negative disables + # the timeout entirely (wait indefinitely). Bridged at startup to the + # internal HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT env var, which still + # works as a manual override and wins if set explicitly. + "platform_connect_timeout": 30, + # Scale-to-zero idle detection (Phase 0). The gateway watches for idle # and, when an instance is opted in via the NAS "Labs" toggle (carried as # the HERMES_SCALE_TO_ZERO env stamp) AND messaging is relay-only/absent diff --git a/tests/gateway/test_config_env_bridge_authority.py b/tests/gateway/test_config_env_bridge_authority.py index a82beb397b9d..f5e10408e4a5 100644 --- a/tests/gateway/test_config_env_bridge_authority.py +++ b/tests/gateway/test_config_env_bridge_authority.py @@ -46,6 +46,7 @@ def _run_gateway_import(hermes_home: Path, initial_env: dict[str, str]) -> dict[ "HERMES_AGENT_TIMEOUT_WARNING", "HERMES_GATEWAY_BUSY_INPUT_MODE", "HERMES_GATEWAY_BUSY_TEXT_MODE", + "HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT", "HERMES_TIMEZONE", ): v = os.environ.get(k) @@ -81,13 +82,15 @@ def _run_gateway_import(hermes_home: Path, initial_env: dict[str, str]) -> dict[ def _write_config(home: Path, agent_cfg: dict | None = None, display_cfg: dict | None = None, - timezone: str | None = None) -> None: + timezone: str | None = None, gateway_cfg: dict | None = None) -> None: import yaml cfg: dict = {} if agent_cfg: cfg["agent"] = agent_cfg if display_cfg: cfg["display"] = display_cfg + if gateway_cfg: + cfg["gateway"] = gateway_cfg if timezone: cfg["timezone"] = timezone (home / "config.yaml").write_text(yaml.safe_dump(cfg)) @@ -174,3 +177,29 @@ def test_env_value_survives_when_config_omits_key(hermes_home: Path) -> None: env = _run_gateway_import(hermes_home, initial_env={}) assert env.get("HERMES_MAX_ITERATIONS") == "123" + + +def test_config_platform_connect_timeout_supplies_env_when_unset(hermes_home: Path) -> None: + """config.yaml:gateway.platform_connect_timeout supplies the env var when + it isn't already set (#19776 — config surface for the Discord connect + timeout, replacing the undocumented env-var-only workaround).""" + _write_config(hermes_home, gateway_cfg={"platform_connect_timeout": 90}) + + env = _run_gateway_import(hermes_home, initial_env={}) + + assert env.get("HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT") == "90" + + +def test_env_platform_connect_timeout_wins_over_config(hermes_home: Path) -> None: + """Unlike the agent.*/display.*/timezone bridges (config-authoritative), + HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT is the manual-override escape hatch: + an explicitly-set env var WINS over config.yaml. This divergence is + intentional (#19776) — the env var is the operator's emergency knob.""" + _write_config(hermes_home, gateway_cfg={"platform_connect_timeout": 90}) + + env = _run_gateway_import( + hermes_home, + initial_env={"HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT": "120"}, + ) + + assert env.get("HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT") == "120" diff --git a/website/docs/reference/environment-variables.md b/website/docs/reference/environment-variables.md index 1f42397ea229..35b34217cbab 100644 --- a/website/docs/reference/environment-variables.md +++ b/website/docs/reference/environment-variables.md @@ -686,7 +686,7 @@ Advanced per-platform knobs for throttling the outbound message batcher. Most us | `HERMES_VISION_DOWNLOAD_TIMEOUT` | Timeout in seconds for downloading an image before handing it to vision models (default: `30`). | | `HERMES_VISION_MAX_CONCURRENCY` | Max concurrent image **encode/resize** bursts across the whole process (override for `auxiliary.vision.max_concurrency`; default: host CPU core count, no ceiling). Bounds only the CPU-bound encode step so a video-frame fan-out can't saturate every core and starve the event loop — the LLM calls stay fully concurrent. Values `< 1` are ignored. | | `HERMES_RESTART_DRAIN_TIMEOUT` | Gateway: seconds to wait for active runs to drain on `/restart` before forcing the restart (default: `900`). | -| `HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT` | Per-platform connect timeout during gateway startup (seconds). | +| `HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT` | Per-platform connect timeout during gateway startup and reconnect (seconds; `0`/negative waits indefinitely). Applies to the connect attempt *and* the Discord adapter's ready-wait, so accounts with many slash commands to sync don't get killed mid-startup. Bridged from `gateway.platform_connect_timeout` in `config.yaml` (default `30`); this env var is the manual override and wins if set explicitly. | | `HERMES_GATEWAY_BUSY_INPUT_MODE` | Default gateway busy-input behavior: `queue`, `steer`, or `interrupt`. Can be overridden per chat with `/busy`. | | `HERMES_GATEWAY_BUSY_ACK_ENABLED` | Whether the gateway sends an acknowledgment message (⚡/⏳/⏩) when a user sends input while the agent is busy (default: `true`). Set to `false` to suppress these messages entirely — the input is still queued/steered/interrupts as normal, only the chat reply is silenced. Bridged from `display.busy_ack_enabled` in `config.yaml`. | | `HERMES_GATEWAY_NO_SUPERVISE` | Inside the s6-overlay Docker image, opt out of auto-supervision when running `hermes gateway run` and use pre-s6 foreground semantics (no auto-restart, gateway is the container's main process). Truthy values: `1`, `true`, `yes`. Equivalent to the `--no-supervise` CLI flag. No-op outside the s6 image. |