hermes - ✅(Solved) Fix [Bug]: hermes config set delegation.* silently has no effect on running process (CLI_CONFIG cache stale) [3 pull requests, 2 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#18946Fetched 2026-05-03 04:53:24
View on GitHub
Comments
2
Participants
2
Timeline
10
Reactions
0
Timeline (top)
labeled ×5cross-referenced ×3commented ×2

Error Message

Returns: google/gemini-2.5-flash On disk: google/gemini-3-flash-preview AssertionError: stale cache wins

Root Cause

tools/delegate_tool.py line 2322 — _load_config() checks the in-memory cli.CLI_CONFIG first and only falls through to a fresh disk read when CLI_CONFIG is empty:

def _load_config() -> dict:
    """Load delegation config from CLI_CONFIG or persistent config."""
    try:
        from cli import CLI_CONFIG
        cfg = CLI_CONFIG.get("delegation", {})
        if cfg:
            return cfg          # <-- runtime cache wins; disk ignored
    except Exception:
        pass
    try:
        from hermes_cli.config import load_config
        full = load_config()
        return full.get("delegation", {})
    except Exception:
        return {}

CLI_CONFIG is initialized once at cli.py:600 (CLI_CONFIG = load_cli_config()) and never refreshed. hermes config set mutates the file on disk but has no IPC channel to notify the running process. Result: stale cache silently wins for the lifetime of the process.

Fix Action

Fixed

PR fix notes

PR #18947: fix(delegate): always reload delegation config from disk, drop stale CLI_CONFIG cache (#18946)

Description (problem / solution / changelog)

Summary

Fixes #18946. hermes config set delegation.<key> <value> writes config.yaml on disk and reports success, but the change had no effect on the running process — delegate_task continued using the previously-cached values until the CLI/gateway was restarted. Silent failure with no warning.

The bug

tools/delegate_tool.py:2322_load_config() consulted cli.CLI_CONFIG first and only fell through to a fresh disk read when CLI_CONFIG was empty:

def _load_config() -> dict:
    try:
        from cli import CLI_CONFIG
        cfg = CLI_CONFIG.get("delegation", {})
        if cfg:
            return cfg          # <-- runtime cache wins; disk ignored
    except Exception:
        pass
    try:
        from hermes_cli.config import load_config
        full = load_config()
        return full.get("delegation", {})
    except Exception:
        return {}

CLI_CONFIG is initialized once at cli.py:600 (CLI_CONFIG = load_cli_config()) and never refreshed. hermes config set mutates the file on disk but has no IPC channel to notify the running process. Result: stale cache silently wins for the lifetime of the process.

This affected every delegation.* knob (model, provider, max_iterations, max_concurrent_children, reasoning_effort, base_url, api_key, inherit_mcp_toolsets, etc.) since they all flow through the same _load_config() path.

Fix

Always read fresh from hermes_cli.config.load_config(). The disk read is cheap — config.yaml is small and only consulted at delegation boundaries (i.e. when a subagent is spawned), not on every API call.

cli.CLI_CONFIG is preserved as a fallback only for test contexts where hermes_cli.config isn't importable but CLI_CONFIG has been mocked in directly. The order is inverted from before: disk first, in-memory cache second.

Tests

tests/tools/test_delegate.py::TestLoadConfigDiskFreshness — three cases:

  • test_disk_changes_visible_after_initial_load — the headline regression: disk holds new value, CLI_CONFIG holds old value, _load_config() returns the new value
  • test_falls_back_to_cli_config_when_disk_read_fails — preserves test-context behaviour where CLI_CONFIG is mocked but hermes_cli.config.load_config raises
  • test_returns_empty_dict_when_both_sources_fail — defensive: both sources fail → returns {}, doesn't propagate exception

Verified the empty-CLI_CONFIG fallback path implicitly through the existing 121 tests in TestDelegationCredentialResolution / TestDelegationProviderIntegration / etc., which all mock _load_config directly and therefore aren't affected by the implementation change.

Full test run:

$ pytest tests/tools/test_delegate.py -q
124 passed in 2.44s

Notes for reviewers

  • The disk read on each _load_config() call is well within budget — delegation is invoked at subagent-spawn time (a heavy operation involving network roundtrips, agent construction, and tool schema enumeration). One additional YAML load adds microseconds to an operation that already takes hundreds of milliseconds minimum.
  • An mtime-based reload would be slightly more efficient, but adds complexity (cache invalidation logic, atomicity concerns on writes, etc.) for a savings that's not measurable. If profiling later shows this as a hot path, an mtime check is a backwards-compatible refinement.
  • Behavior in test contexts that mock cli.CLI_CONFIG directly (not hermes_cli.config.load_config) is preserved by the fallback — see test_falls_back_to_cli_config_when_disk_read_fails. The 121 existing tests in this file mock _load_config itself, so they're indifferent to the change.

Fixes #18946

Changed files

  • tests/tools/test_delegate.py (modified, +88/-0)
  • tools/delegate_tool.py (modified, +22/-13)

PR #18967: fix(delegate): read delegation config from disk first to avoid stale cache

Description (problem / solution / changelog)

Summary

Fixes #18946

_load_config() in tools/delegate_tool.py checked the runtime cache (cli.CLI_CONFIG) first and never fell back to disk when the cache existed. This meant hermes config set delegation.model <X> wrote to disk correctly but the running process continued using the old value silently.

Changes

Reverse the priority in _load_config(): always read from the persistent config file first, falling back to CLI_CONFIG only when the disk read fails. This makes delegation.* changes take effect immediately without requiring a process restart.

Test plan

# Start hermes, then from another shell:
hermes config set delegation.model some-other-model
# delegate_task should now use the new model without restart

Changed files

  • tools/delegate_tool.py (modified, +12/-12)

PR #18978: fix(tools): prefer load_config() over stale CLI_CONFIG in delegate _load_config()

Description (problem / solution / changelog)

Fix

Reverses the priority in _load_config() so that load_config() (which has mtime-based cache invalidation) is checked before the in-memory CLI_CONFIG dict.

Problem

hermes config set delegation.model X writes to disk and reports success, but _load_config() in delegate_tool.py checks the in-memory CLI_CONFIG dict first. Since CLI_CONFIG was loaded once at process startup and never invalidated, the stale cached value wins — the new disk config is never read until the process restarts.

This affects CLI, gateway, and cron — any long-running process that calls delegate_task() after a config change via hermes config set.

Root Cause

_load_config() prioritized CLI_CONFIG (stale in-memory dict) over load_config() (mtime-cached disk reads with automatic invalidation). The load_config() function in hermes_cli/config.py already handles cache invalidation correctly by checking (mtime_ns, size) on every call — it just was never reached because CLI_CONFIG was checked first.

Fix Details

  • Try hermes_cli.config.load_config() first (mtime-aware, always fresh)
  • Fall back to cli.CLI_CONFIG only if load_config() fails
  • Added if cfg: guard to the CLI_CONFIG path for consistency

Testing

  1. Start a gateway session with delegation.model: google/gemini-2.5-flash
  2. From another shell: hermes config set delegation.model google/gemini-3-flash-preview
  3. Call delegate_task(...) from the original session
  4. Before fix: subagent uses gemini-2.5-flash (stale cache)
  5. After fix: subagent uses gemini-3-flash-preview (fresh from disk)

Fixes #18946

Changed files

  • tools/delegate_tool.py (modified, +12/-10)

Code Example

import os, sys, yaml, tempfile, pathlib

tmp = pathlib.Path(tempfile.mkdtemp(prefix="hermes-repro-"))
cfg_path = tmp / "config.yaml"
cfg_path.write_text(yaml.safe_dump({
    "model": {"default": "claude-opus-4-7", "provider": "anthropic"},
    "delegation": {"model": "google/gemini-2.5-flash", "provider": "nous"},
}))
os.environ["HERMES_HOME"] = str(tmp)
sys.path.insert(0, "/path/to/hermes-agent")

# Simulate gateway startup: CLI_CONFIG loaded once at import time
from hermes_cli.config import load_config
CLI_CONFIG_v1 = load_config()

# Simulate `hermes config set delegation.model google/gemini-3-flash-preview`
data = yaml.safe_load(cfg_path.read_text())
data["delegation"]["model"] = "google/gemini-3-flash-preview"
cfg_path.write_text(yaml.safe_dump(data))

# Inject the cached CLI_CONFIG to simulate running gateway state
import cli as cli_mod
cli_mod.CLI_CONFIG = CLI_CONFIG_v1

# Call the delegate_task code path
from tools.delegate_tool import _load_config
result = _load_config()
print(f"Returns:  {result['model']}")
print(f"On disk:  google/gemini-3-flash-preview")
assert result["model"] == "google/gemini-3-flash-preview", "stale cache wins"

---

Returns:  google/gemini-2.5-flash
On disk:  google/gemini-3-flash-preview
AssertionError: stale cache wins

---

def _load_config() -> dict:
    """Load delegation config from CLI_CONFIG or persistent config."""
    try:
        from cli import CLI_CONFIG
        cfg = CLI_CONFIG.get("delegation", {})
        if cfg:
            return cfg          # <-- runtime cache wins; disk ignored
    except Exception:
        pass
    try:
        from hermes_cli.config import load_config
        full = load_config()
        return full.get("delegation", {})
    except Exception:
        return {}
RAW_BUFFERClick to expand / collapse

Bug Description

hermes config set delegation.model <X> writes config.yaml on disk and reports success, but the change does not take effect for delegate_task in any already-running process (CLI/gateway/cron). The runtime keeps using the previous delegation.model until the process is restarted.

This is silent — there's no warning that a restart is required, and the next delegate_task call happily runs on the old model with no indication anything is stale.

Steps to Reproduce

  1. Start a CLI/gateway session with delegation.model: google/gemini-2.5-flash in config.yaml.
  2. From a separate shell (or mid-session), run hermes config set delegation.model google/gemini-3-flash-preview. Confirm config.yaml on disk now shows google/gemini-3-flash-preview.
  3. From the original session, call delegate_task(...). The subagent runs on google/gemini-2.5-flash, not the new model.
  4. Restart the process. Now delegate_task correctly uses google/gemini-3-flash-preview.

Minimal reproducer

This 30-line script reproduces the bug deterministically without spinning up a real subagent:

import os, sys, yaml, tempfile, pathlib

tmp = pathlib.Path(tempfile.mkdtemp(prefix="hermes-repro-"))
cfg_path = tmp / "config.yaml"
cfg_path.write_text(yaml.safe_dump({
    "model": {"default": "claude-opus-4-7", "provider": "anthropic"},
    "delegation": {"model": "google/gemini-2.5-flash", "provider": "nous"},
}))
os.environ["HERMES_HOME"] = str(tmp)
sys.path.insert(0, "/path/to/hermes-agent")

# Simulate gateway startup: CLI_CONFIG loaded once at import time
from hermes_cli.config import load_config
CLI_CONFIG_v1 = load_config()

# Simulate `hermes config set delegation.model google/gemini-3-flash-preview`
data = yaml.safe_load(cfg_path.read_text())
data["delegation"]["model"] = "google/gemini-3-flash-preview"
cfg_path.write_text(yaml.safe_dump(data))

# Inject the cached CLI_CONFIG to simulate running gateway state
import cli as cli_mod
cli_mod.CLI_CONFIG = CLI_CONFIG_v1

# Call the delegate_task code path
from tools.delegate_tool import _load_config
result = _load_config()
print(f"Returns:  {result['model']}")
print(f"On disk:  google/gemini-3-flash-preview")
assert result["model"] == "google/gemini-3-flash-preview", "stale cache wins"

Output:

Returns:  google/gemini-2.5-flash
On disk:  google/gemini-3-flash-preview
AssertionError: stale cache wins

Root cause

tools/delegate_tool.py line 2322 — _load_config() checks the in-memory cli.CLI_CONFIG first and only falls through to a fresh disk read when CLI_CONFIG is empty:

def _load_config() -> dict:
    """Load delegation config from CLI_CONFIG or persistent config."""
    try:
        from cli import CLI_CONFIG
        cfg = CLI_CONFIG.get("delegation", {})
        if cfg:
            return cfg          # <-- runtime cache wins; disk ignored
    except Exception:
        pass
    try:
        from hermes_cli.config import load_config
        full = load_config()
        return full.get("delegation", {})
    except Exception:
        return {}

CLI_CONFIG is initialized once at cli.py:600 (CLI_CONFIG = load_cli_config()) and never refreshed. hermes config set mutates the file on disk but has no IPC channel to notify the running process. Result: stale cache silently wins for the lifetime of the process.

Why this is more than cosmetic

This makes A/B testing delegation models on a live session impossible without a full gateway restart, and the failure is silent — hermes config set reports success, the file on disk is correct, but the running agent reads stale values. Users will wrongly attribute behavior changes (or non-changes) to the wrong model.

It also affects any other delegation.* knob (provider, max_iterations, max_concurrent_children, reasoning_effort, etc.) since they all flow through the same _load_config() path.

Proposed fix

Smallest change with no protocol/IPC work: have _load_config() always reload from disk (the disk read is cheap — config.yaml is small and reads dozens of times per session is fine). The CLI_CONFIG path was a micro-optimization; the cache-staleness footgun is a worse tradeoff.

Alternative: stat config.yaml's mtime in _load_config() and only reload when it has advanced since the last read. Slightly more code, fewer disk reads.

Happy to submit a PR with the simpler "always reload" version plus a regression test in tests/tools/test_delegate_tool.py.

Environment

  • Hermes Agent v0.12.0 (2026.4.30)
  • Reproduces on current main (HEAD as of issue filing)
  • OS: Linux

Related

  • #11999 (closed) — earlier delegation config bug; different code path, but same general concern about runtime config plumbing
  • #16723 (open) — /config display reads stale source; symptom-adjacent (display side) but distinct from this (write-side staleness)
  • #4073 (open) — /config is read-only in TUI mid-session — implicitly acknowledges runtime mutation isn't well-supported, but doesn't cover the silent-failure shape of this bug

extent analysis

TL;DR

The issue can be fixed by modifying the _load_config() function to always reload the configuration from disk instead of relying on the cached CLI_CONFIG.

Guidance

  • The root cause of the issue is the stale cache in CLI_CONFIG which is not updated when the configuration file on disk is changed.
  • To fix this, the _load_config() function should be modified to always reload the configuration from disk, or to check the modification time of the configuration file and reload only when it has changed.
  • The proposed fix is to simplify the _load_config() function to always reload from disk, as the disk read is cheap and the cache-staleness issue is a worse tradeoff.
  • An alternative solution is to stat the configuration file's modification time in _load_config() and only reload when it has advanced since the last read.

Example

def _load_config() -> dict:
    """Load delegation config from disk."""
    from hermes_cli.config import load_config
    return load_config().get("delegation", {})

Notes

  • The issue affects not only the delegation.model but also other delegation.* knobs, as they all flow through the same _load_config() path.
  • The fix should be accompanied by a regression test in tests/tools/test_delegate_tool.py to ensure the issue is fully resolved.

Recommendation

Apply the proposed workaround by modifying the _load_config() function to always reload the configuration from disk, as it is a simple and effective solution to the problem.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING