Skip to content

fix: prevent stale agent_version in Docker gateway deployments (#6150)#6289

Open
webtecnica wants to merge 1 commit into
nesquena:masterfrom
webtecnica:fix/6150-docker-agent-version
Open

fix: prevent stale agent_version in Docker gateway deployments (#6150)#6289
webtecnica wants to merge 1 commit into
nesquena:masterfrom
webtecnica:fix/6150-docker-agent-version

Conversation

@webtecnica

Copy link
Copy Markdown
Contributor

Summary

Fixes #6150: /api/settings can return stale agent_version in Docker gateway deployments even when the Agent /health endpoint and runtime detection report the correct version.

Root Cause

Two issues combine to produce stale version display:

  1. _gateway_health_base_url() in api/updates.py only checked GATEWAY_HEALTH_URL and HERMES_GATEWAY_HEALTH_URL, but Docker compose deployments following the docs set HERMES_API_URL and HERMES_WEBUI_GATEWAY_BASE_URL instead. The health probe couldn't reach the Agent at all in these setups, so it never refreshed the version.

  2. /api/settings in api/routes.py used the import-time AGENT_VERSION constant — computed once at process startup — and never re-probed the gateway on subsequent requests, so even when the gateway health URL was configured correctly, the settings endpoint returned whatever version was detected when the WebUI process started.

Changes

api/updates.py_gateway_health_base_url()

Added HERMES_API_URL and HERMES_WEBUI_GATEWAY_BASE_URL to the env-var fallback chain, matching the resolution order already used by api/agent_health.py:

GATEWAY_HEALTH_URL → HERMES_GATEWAY_HEALTH_URL → HERMES_API_URL → HERMES_WEBUI_GATEWAY_BASE_URL → http://hermes-agent:8642

api/routes.pyGET /api/settings

Now imports and calls _detect_agent_version_from_gateway_health(timeout=2.0) on every settings request, falling back to the import-time AGENT_VERSION only when the gateway is unreachable. This ensures Settings → System always shows the live running Agent version.

Verification

  • The env-var chain is identical to the battle-tested chain in api/agent_health.py
  • _detect_agent_version_from_gateway_health() is safe — it catches all exceptions internally and returns None on any failure
  • When the gateway is unreachable (non-Docker, agent not running), the or AGENT_VERSION fallback preserves existing behavior
  • Import-time AGENT_VERSION is still used as a baseline for displays that shouldn't block on an HTTP round-trip

…ena#6150)

Two changes address /api/settings returning a stale agent_version when
WebUI runs in Docker and the Agent sits in another container:

1. _gateway_health_base_url() now falls back through HERMES_API_URL and
   HERMES_WEBUI_GATEWAY_BASE_URL — matching the chain already used by
   agent_health.py — so the health probe works with the env vars that
   Docker compose deployments actually set.

2. /api/settings now calls _detect_agent_version_from_gateway_health()
   on every request (timeout=2.0) and only falls back to the import-time
   AGENT_VERSION when the gateway is unreachable. This ensures Settings →
   System shows the live Agent version instead of whatever was detected
   at process startup.
@greptile-apps

greptile-apps Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two related bugs that caused /api/settings to return a stale agent_version in Docker gateway deployments: the env-var fallback chain in _gateway_health_base_url() was missing HERMES_API_URL and HERMES_WEBUI_GATEWAY_BASE_URL, and the settings endpoint was reading an import-time constant rather than probing the live gateway.

  • api/updates.py: Adds the two missing env vars to _gateway_health_base_url(), aligning it with the battle-tested chain already used in api/agent_health.py. The change is minimal and low-risk.
  • api/routes.py: Replaces the static AGENT_VERSION constant with a live call to _detect_agent_version_from_gateway_health(timeout=2.0) on every /api/settings request. Because the function probes two HTTP paths serially with no caching, a temporarily-unreachable gateway can block the settings endpoint for up to 4 seconds per request — a latency regression that will affect every page load in affected deployments.

Confidence Score: 3/5

Safe to merge for correctness, but the settings endpoint will block for up to 4 s per request whenever the gateway is configured but unreachable — this warrants a caching layer before shipping to production traffic.

The env-var chain fix in updates.py is clean and straightforward. The concern is in routes.py: calling an uncached, synchronous two-path HTTP probe (each with a 2.0 s timeout) on every GET /api/settings means a temporarily-unreachable gateway adds up to 4 seconds of blocking latency per page load. The settings route is hit on every UI refresh, so this is a live latency regression for any Docker deployment where the agent is slow or briefly down.

api/routes.py — the synchronous gateway probe needs a TTL cache before this pattern is production-safe.

Important Files Changed

Filename Overview
api/updates.py Adds HERMES_API_URL and HERMES_WEBUI_GATEWAY_BASE_URL to the _gateway_health_base_url() fallback chain, matching the resolution order in agent_health.py — low-risk, targeted, well-documented fix.
api/routes.py Calls _detect_agent_version_from_gateway_health(timeout=2.0) on every GET /api/settings request; with 2 probe paths each capable of a full 2.0 s timeout, the endpoint can block for up to 4 seconds per request when the configured gateway is unreachable, introducing a latency regression for every caller of this endpoint.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant UI as Browser / UI
    participant R as routes.py GET /api/settings
    participant U as updates.py _detect_agent_version_from_gateway_health
    participant G as Agent Gateway (Docker container)

    UI->>R: GET /api/settings
    R->>U: "_detect_agent_version_from_gateway_health(timeout=2.0)"
    U->>U: _gateway_health_base_url()
    U->>G: GET /health (timeout 2.0 s)
    alt Gateway reachable
        G-->>U: 200 OK + version payload
        U-->>R: live agent_version string
    else both paths fail
        G--xU: timeout / error (up to 4 s total)
        U-->>R: None
        R->>R: fallback to import-time AGENT_VERSION
    end
    R-->>UI: settings JSON
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant UI as Browser / UI
    participant R as routes.py GET /api/settings
    participant U as updates.py _detect_agent_version_from_gateway_health
    participant G as Agent Gateway (Docker container)

    UI->>R: GET /api/settings
    R->>U: "_detect_agent_version_from_gateway_health(timeout=2.0)"
    U->>U: _gateway_health_base_url()
    U->>G: GET /health (timeout 2.0 s)
    alt Gateway reachable
        G-->>U: 200 OK + version payload
        U-->>R: live agent_version string
    else both paths fail
        G--xU: timeout / error (up to 4 s total)
        U-->>R: None
        R->>R: fallback to import-time AGENT_VERSION
    end
    R-->>UI: settings JSON
Loading

Reviews (1): Last reviewed commit: "fix: prevent stale agent_version in Dock..." | Re-trigger Greptile

Comment thread api/routes.py
Comment on lines +12390 to +12393
settings["agent_version"] = (
_detect_agent_version_from_gateway_health(timeout=2.0)
or AGENT_VERSION
)

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.

P1 Blocking HTTP probe on every settings request

_detect_agent_version_from_gateway_health(timeout=2.0) is called synchronously on every GET /api/settings with no caching. The function probes two paths sequentially (/health, then /health/detailed), each with its own 2.0 s socket timeout. When the configured gateway is reachable but slow — or when HERMES_API_URL points at a host that accepts the TCP connection but never responds — both probes exhaust their full timeout before None is returned and the fallback kicks in. That ceiling is 4 seconds of wall-clock blocking per settings request, which fires on every page load. In non-Docker environments the default hostname (hermes-agent) normally fails with a fast DNS error, but any env where one of the four gateway vars is set to a temporarily-unreachable host will regress noticeably. Adding a short TTL cache (e.g. 30 s) around the probe result would cap the HTTP overhead to one round-trip per interval rather than one per UI refresh.

Comment thread api/routes.py
from api.updates import (
AGENT_VERSION,
WEBUI_VERSION,
_detect_agent_version_from_gateway_health,

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.

P2 Importing a private symbol across module boundaries

_detect_agent_version_from_gateway_health is a module-private helper (leading underscore convention). Importing it directly into routes.py couples the route handler to an internal implementation detail of updates.py. If the function is later renamed, inlined, or replaced with a cached variant, routes.py will silently break at runtime (the surrounding except Exception: pass will swallow the ImportError). Consider promoting a thin public wrapper — e.g. get_live_agent_version() — in updates.py that routes.py can import by stable name.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@nesquena-hermes nesquena-hermes left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Right fix, but it adds a blocking network call to a hot endpoint — needs a small cache

Thanks @webtecnica — the diagnosis is correct (Docker gateway deployments show a stale import-time AGENT_VERSION), and extending the gateway-URL env chain to match agent_health.py is a good call.

One blocker before this can ship: as written, the /api/settings GET handler now calls _detect_agent_version_from_gateway_health(timeout=2.0) unconditionally on every request, and that function has no cache — it does a fresh urllib.request.urlopen each time (api/updates.py:539). /api/settings is a hot endpoint (page load, settings open, etc.), so in a deployment where the gateway is slow or unreachable this adds up to 2 seconds of blocking latency to every settings read. That's a regression for the common case to fix the Docker-display case.

Fix-spec

Cache the gateway-detected version with a short TTL so the network is hit at most once every N seconds, not per-request:

  • Add a module-level cache in api/updates.py, e.g. _gateway_agent_version_cache = {"value": None, "at": 0.0} and a _GATEWAY_AGENT_VERSION_TTL = 30.0.
  • Wrap the detection: return the cached value if time.monotonic() - at < TTL; otherwise call _detect_agent_version_from_gateway_health() (keep the default 0.75s timeout — 2.0s is long for a UI-blocking path), store the result + timestamp, and return it. Cache negative results too (so an unreachable gateway doesn't retry every request), but with a shorter TTL is fine.
  • In the settings handler, call the cached wrapper instead of the raw detector.

That keeps the Docker-display fix while bounding the worst-case settings latency to one detection per TTL window. Happy to take another look once that's in — the rest of the change is good.

@nesquena-hermes nesquena-hermes added the size:S Small PR (≤2 files, ≤30 LOC) label Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S Small PR (≤2 files, ≤30 LOC)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

/api/settings can return stale agent_version in Docker gateway deployments even when Agent /health and runtime detection report the correct version

2 participants