Skip to content

fix(gateway): resolve max_iterations from config.yaml directly instead of the env round-trip - #64298

Open
Soju06 wants to merge 1 commit into
NousResearch:mainfrom
Soju06:upstream-pr/pr11-max-iter
Open

fix(gateway): resolve max_iterations from config.yaml directly instead of the env round-trip#64298
Soju06 wants to merge 1 commit into
NousResearch:mainfrom
Soju06:upstream-pr/pr11-max-iter

Conversation

@Soju06

@Soju06 Soju06 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Problem

The gateway resolves the per-turn iteration cap by bridging config.yaml agent.max_turns into HERMES_MAX_ITERATIONS and then reading the env var back. That round-trip has three holes that let a stale ~/.hermes/.env value (e.g. HERMES_MAX_ITERATIONS=90 written by an older setup flow) silently win over the user's configured budget, reintroducing 90/90 iteration-exhaustion on long turns:

  1. agent.max_turns: null in config.yaml makes _bridge_max_turns_from_config write the literal string "None" into the env ("max_turns" in agent_cfg is true), and _current_max_iterations then swallows the int() failure and returns the hardcoded default 90 — ignoring a perfectly valid .env fallback.
  2. Legacy root-level max_turns (which hermes_cli.config._normalize_max_turns_config explicitly supports and migrates into agent.max_turns) is invisible to the gateway bridge, so those configs run at whatever stale value .env holds.
  3. The startup "Agent budget" log line reads the raw env var, so it can report a value that disagrees with what per-turn resolution later computes.

Change

gateway/run.py:

  • New _read_config_max_iterations(home): loads config.yaml (env-var expansion + managed-scope overlay, both fail-open, exactly as the bridge did), returns int(agent.max_turns) when present, falls back to legacy root-level max_turns (agent.max_turns wins when both are set), and returns None for missing/null/malformed values instead of propagating garbage into the environment.
  • _bridge_max_turns_from_config now delegates to it and only writes the env var for a valid integer.
  • New _resolve_gateway_max_iterations(default=90, *, reload_runtime_env=False): optionally refreshes the runtime env (rotated credentials), then prefers the config value directly — syncing the env var for subprocess consumers — and only consults HERMES_MAX_ITERATIONS when config omits the key (with an int-parse guard).
  • _current_max_iterations becomes a thin wrapper over _resolve_gateway_max_iterations(reload_runtime_env=True), so all existing per-turn call sites (native gateway turns and the API-server adapter) pick up config-authoritative resolution without signature changes.
  • The startup budget log now logs the resolved value instead of the raw env var.

scripts/release.py: contributor-attribution entry.

Correctness notes

  • Managed-scope overlay behavior is preserved: administrator-pinned agent.max_turns is applied inside _read_config_max_iterations, so direct config reads cannot bypass a managed pin.
  • Multiplex mode is unchanged: _reload_runtime_env_preserving_config_authority still skips the global .env reload and only re-bridges config.
  • Malformed agent.max_turns (non-integer) now falls back to the env var / default rather than crashing the turn or poisoning the env.
  • Tests that monkeypatch gateway.run._current_max_iterations keep working since the symbol and signature are unchanged.

Tests

  • tests/gateway/test_runtime_env_reload_config_authority.py: new tests for (a) config winning over a stale .env after a runtime env reload, (b) legacy root-level max_turns, (c) agent.max_turns: null falling back to the env value; the existing _current_max_iterations reload test now pins a config-less hermes home so it keeps exercising the env-fallback path.
  • Targeted suites all green: tests/gateway/test_runtime_env_reload_config_authority.py, tests/gateway/test_cached_agent_max_iterations.py (9 passed), tests/gateway/test_api_server.py + tests/scripts (201 passed).
  • Full tests/gateway run: 9101 passed; the remaining failures (telegram/slack/feishu/path-completion modules, unrelated to this change) reproduce identically on a clean main checkout in the same environment.

Measured impact

Reproduced the failure mode end-to-end: with config.yaml agent.max_turns: 500 and a stale .env HERMES_MAX_ITERATIONS=90, per-turn resolution previously could run at 90; it now resolves 500 and re-syncs the env var to 500.

🤖 Generated with Claude Code

@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery area/config Config system, migrations, profiles P2 Medium — degraded but workaround exists labels Jul 14, 2026

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Approved

This PR removes the HERMES_MAX_ITERATIONS env var round-trip and reads agent.max_turns directly from config.yaml.

Looks Good

  • _read_config_max_iterations is a clean, focused helper.
  • Handles both agent.max_turns and legacy root-level max_turns for backward compat.
  • Reduces unnecessary env var surface.

No Issues Found


Reviewed by Hermes Agent

@Soju06
Soju06 force-pushed the upstream-pr/pr11-max-iter branch 2 times, most recently from 5667442 to 000589f Compare July 14, 2026 14:43

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tracing the remaining config/env resolution paths. The premise is still present on current main: gateway/run.py:1394-1405 serializes a nested null as "None", and it does not read legacy root-level max_turns.

Problems

  • gateway/run.py:1611-1614 is a separate bootstrap bridge that the PR leaves unchanged. It still writes str(_agent_cfg["max_turns"]), so agent.max_turns: null poisons the process environment with "None". The new startup call at PR line gateway/run.py:6882 invokes _resolve_gateway_max_iterations() without reloading .env; it therefore resolves 90 rather than a valid .env fallback and logs the wrong budget. A later normal turn reloads .env, so startup and turn resolution disagree.

Suggested changes

  • Route the bootstrap assignment through the new validated helper, including legacy root-level max_turns handling.
  • Add an import/bootstrap regression using tests/gateway/test_config_env_bridge_authority.py: agent.max_turns: null plus .env HERMES_MAX_ITERATIONS=120 should preserve 120.

Automated hermes-sweeper review.

Comment thread gateway/run.py Outdated
@@ -6843,7 +6882,7 @@ async def start(self) -> bool:
# config.yaml → env bridge did the right thing at a glance (instead

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This resolver does not reload .env, but the unchanged bootstrap bridge at gateway/run.py:1611-1614 still writes "None" for agent.max_turns: null. Therefore this startup log resolves 90 even when .env contains a valid fallback such as 120; a later normal turn reloads .env and differs. Route the bootstrap bridge through the validated helper and cover the import/startup path.

@Soju06
Soju06 force-pushed the upstream-pr/pr11-max-iter branch from 000589f to c7b0ae9 Compare July 16, 2026 02:19
@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 16, 2026
@Soju06
Soju06 force-pushed the upstream-pr/pr11-max-iter branch from c7b0ae9 to 6447d4d Compare July 16, 2026 15:08
@Soju06

Soju06 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Good catch — the import-time bootstrap bridge was indeed left on the raw str() path. Changes: the bootstrap assignment now routes through _bridge_max_turns_from_config(), which validates via _read_config_max_iterations(), so agent.max_turns: null no longer exports the literal "None" and legacy root-level max_turns is bridged at import time as well. With the import-time env no longer poisoned, the startup call to _resolve_gateway_max_iterations() falls back to the .env value when config yields nothing, matching per-turn resolution — startup and turn now log the same budget. Added the requested regression in tests/gateway/test_config_env_bridge_authority.py: agent.max_turns: null with .env HERMES_MAX_ITERATIONS=120 preserves 120 across gateway.run import, plus a legacy root-level max_turns bridge test. Both bridge/reload test files pass (16 tests).

@Soju06

Soju06 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@teknium1 Gentle ping — all points from the review here have been addressed (summary in the comment above), the branch is rebased on current main, and CI is green. Ready for another look whenever convenient.

@Soju06
Soju06 force-pushed the upstream-pr/pr11-max-iter branch from 6447d4d to 6c7be58 Compare July 23, 2026 02:27
@Soju06

Soju06 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@teknium1 Re-review request: rebased over the #69423 salvage — the new sessions.* config→env bridge is preserved and now routes through the same shared loader as the validated agent.max_turns resolution (single YAML read per bridge call), so the null→"None" fix from your review holds for both startup and per-turn paths. Bridge suites green.

@GottZ GottZ left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Summary

Five PRs address the gateway max-iteration precedence bug: #17306 and #18230 add config-first resolution at agent creation, #18764 establishes the broader config-authoritative bridge, #43407 centralizes runtime/startup/API resolution, and #64298 closes the remaining null, legacy-root, and startup-consistency gaps.

Related pull requests

  • #17306 [closed] related — (+58/-3) — superseded: Adds a config-first resolver for the two native gateway creation paths, but current main already preserves agent.max_turns across dotenv reloads; it remains relevant as an earlier implementation of the same root-cause fix.
  • #18230 [closed] related — (+82/-6) — duplicate/superseded: Implements substantially the same config-first resolver as #17306 and also applies it to the API server; it remains relevant because it documents API-server coverage, but the underlying behavior is already on main.
  • #18764 [merged] related — (+276/-24) — merged foundation: Makes config.yaml authoritative over stale .env values for max_turns and several related settings, removes setup's duplicate HERMES_MAX_ITERATIONS write, and adds bridge regressions; #64298 addresses residual max-turns edge cases left by this reference implementation.
  • #43407 [closed] related — (+121/-27) — superseded: Centralizes config-authoritative resolution across startup, native gateway, and API-server paths and reloads dotenv before resolving per-turn budgets; it remains relevant as the direct precursor to the current-main implementation, while #64298 covers additional null and legacy-root cases.
  • #64298 related — (+160/-17) — merge: Replaces the fragile env round-trip with validated direct config resolution, preserves a valid env fallback for agent.max_turns: null, honors legacy root-level max_turns, and aligns startup logging with per-turn resolution. The contributor keep_open review on #64298 identified an unhandled bootstrap bridge, and the current diff explicitly routes that bridge through the shared validated loader and adds the requested import-time regressions.

Duplicates

#17306 and #18230 are near-duplicates of the config-first agent-construction fix; #43407 overlaps their resolution strategy and the runtime-authority work already established by merged #18764. #64298 is not a pure duplicate because it adds null handling, legacy root-level compatibility, and startup/per-turn consistency.

Suggested consolidation

Merge #64298 as the focused completion of the merged #18764 foundation: its current diff addresses the bootstrap objection from the keep_open review and covers the remaining reproducible edge cases. Keep #17306, #18230, and #43407 closed as superseded/duplicate implementations; no reopening or separate merge is warranted.

Cross-PR triage: Reviewed 5 pull requests and 0 issues in this complex. Each diff was read against this issue; Assessment working set: 49 kB of PR diffs, 9 kB of issue/PR text, 7 kB of discussion (11 comments), 2 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

@Soju06
Soju06 force-pushed the upstream-pr/pr11-max-iter branch from 6c7be58 to 5d658c2 Compare July 28, 2026 02:35
…d of the env round-trip

The gateway bridged config.yaml agent.max_turns into HERMES_MAX_ITERATIONS
and read the env var back. That round-trip let a stale ~/.hermes/.env value
win in several cases: agent.max_turns: null poisoned the env with the
literal string "None" (collapsing to the hardcoded 90 default), legacy
root-level max_turns was invisible to the bridge, and the startup budget
log read the raw env var.

Add _read_config_max_iterations / _resolve_gateway_max_iterations so config
is consulted directly (env-var expansion and managed-scope overlay
preserved, fail-open) with the env var as fallback only when config omits
the key. _current_max_iterations becomes a thin wrapper, so all per-turn
call sites and the API-server adapter pick this up unchanged. The
import-time bootstrap bridge routes through the same validated helper, so
a null key never exports "None" and startup and per-turn resolution agree.
@Soju06
Soju06 force-pushed the upstream-pr/pr11-max-iter branch from 5d658c2 to 5ae5cb7 Compare July 30, 2026 01:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants