fix: prevent stale agent_version in Docker gateway deployments (#6150)#6289
fix: prevent stale agent_version in Docker gateway deployments (#6150)#6289webtecnica wants to merge 1 commit into
Conversation
…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.
|
| 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
%%{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
Reviews (1): Last reviewed commit: "fix: prevent stale agent_version in Dock..." | Re-trigger Greptile
| settings["agent_version"] = ( | ||
| _detect_agent_version_from_gateway_health(timeout=2.0) | ||
| or AGENT_VERSION | ||
| ) |
There was a problem hiding this comment.
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.
| from api.updates import ( | ||
| AGENT_VERSION, | ||
| WEBUI_VERSION, | ||
| _detect_agent_version_from_gateway_health, |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
Summary
Fixes #6150:
/api/settingscan return staleagent_versionin Docker gateway deployments even when the Agent/healthendpoint and runtime detection report the correct version.Root Cause
Two issues combine to produce stale version display:
_gateway_health_base_url()inapi/updates.pyonly checkedGATEWAY_HEALTH_URLandHERMES_GATEWAY_HEALTH_URL, but Docker compose deployments following the docs setHERMES_API_URLandHERMES_WEBUI_GATEWAY_BASE_URLinstead. The health probe couldn't reach the Agent at all in these setups, so it never refreshed the version./api/settingsinapi/routes.pyused the import-timeAGENT_VERSIONconstant — 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_URLandHERMES_WEBUI_GATEWAY_BASE_URLto the env-var fallback chain, matching the resolution order already used byapi/agent_health.py:api/routes.py—GET /api/settingsNow imports and calls
_detect_agent_version_from_gateway_health(timeout=2.0)on every settings request, falling back to the import-timeAGENT_VERSIONonly when the gateway is unreachable. This ensures Settings → System always shows the live running Agent version.Verification
api/agent_health.py_detect_agent_version_from_gateway_health()is safe — it catches all exceptions internally and returnsNoneon any failureor AGENT_VERSIONfallback preserves existing behaviorAGENT_VERSIONis still used as a baseline for displays that shouldn't block on an HTTP round-trip