Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,30 @@ def _reload_runtime_env_preserving_config_authority() -> None:
logger = logging.getLogger(__name__)


def _max_iterations_env(default: int = 90) -> int:
"""Read ``HERMES_MAX_ITERATIONS`` safely for long-lived gateway paths.

The gateway re-reads runtime environment/config state in several places.
A malformed env value must not crash startup logging, background tasks,
or foreground message handling.
"""
raw = os.environ.get("HERMES_MAX_ITERATIONS")
if raw is None:
return int(default)
text = str(raw).strip()
if not text:
return int(default)
try:
return int(text)
except (TypeError, ValueError):
logger.warning(
"Invalid HERMES_MAX_ITERATIONS=%r; using default %d",
raw,
int(default),
)
return int(default)


# Sentinel placed into _running_agents immediately when a session starts
# processing, *before* any await. Prevents a second message for the same
# session from bypassing the "already running" guard during the async gap
Expand Down Expand Up @@ -3231,15 +3255,12 @@ async def start(self) -> bool:
# Log the resolved max_iterations budget so operators can verify the
# config.yaml → env bridge did the right thing at a glance (instead
# of silently running at a stale .env value for weeks).
try:
_effective_max_iter = int(os.getenv("HERMES_MAX_ITERATIONS", "90"))
logger.info(
"Agent budget: max_iterations=%d (agent.max_turns from config.yaml, "
"or HERMES_MAX_ITERATIONS from .env, or default 90)",
_effective_max_iter,
)
except Exception:
pass
_effective_max_iter = _max_iterations_env()
logger.info(
"Agent budget: max_iterations=%d (agent.max_turns from config.yaml, "
"or HERMES_MAX_ITERATIONS from .env, or default 90)",
_effective_max_iter,
)
# Redaction status: ON by default (#17691). Surface a prominent
# warning if an operator has explicitly opted out so they don't
# forget the downgrade is active — the redactor snapshots its
Expand Down Expand Up @@ -10207,7 +10228,7 @@ async def _run_background_task(
disabled_toolsets = agent_cfg.get("disabled_toolsets") or None

pr = self._provider_routing
max_iterations = int(os.getenv("HERMES_MAX_ITERATIONS", "90"))
max_iterations = _max_iterations_env()
reasoning_config = self._resolve_session_reasoning_config(source=source)
self._reasoning_config = reasoning_config
self._service_tier = self._load_service_tier()
Expand Down Expand Up @@ -14650,7 +14671,7 @@ def run_sync():
os.environ["HERMES_SESSION_KEY"] = session_key or ""

# Read from env var or use default (same as CLI)
max_iterations = int(os.getenv("HERMES_MAX_ITERATIONS", "90"))
max_iterations = _max_iterations_env()

# Map platform enum to the platform hint key the agent understands.
# Platform.LOCAL ("local") maps to "cli"; others pass through as-is.
Expand Down
20 changes: 20 additions & 0 deletions tests/gateway/test_runtime_env_reload_config_authority.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import os
from pathlib import Path
from unittest.mock import patch

import yaml

Expand Down Expand Up @@ -51,3 +52,22 @@ def test_reload_runtime_env_keeps_env_max_iterations_when_config_omits_key(
gateway_run._reload_runtime_env_preserving_config_authority()

assert os.environ["HERMES_MAX_ITERATIONS"] == "123"


def test_max_iterations_env_invalid_value_falls_back_to_default(monkeypatch) -> None:
monkeypatch.setenv("HERMES_MAX_ITERATIONS", "not-a-number")

with patch.object(gateway_run.logger, "warning") as warning:
assert gateway_run._max_iterations_env() == 90

warning.assert_called_once()
assert "Invalid HERMES_MAX_ITERATIONS" in warning.call_args[0][0]


def test_max_iterations_env_strips_whitespace_and_uses_valid_value(monkeypatch) -> None:
monkeypatch.setenv("HERMES_MAX_ITERATIONS", " 42 ")

with patch.object(gateway_run.logger, "warning") as warning:
assert gateway_run._max_iterations_env() == 42

warning.assert_not_called()
Loading