Skip to content

fix: detect agent version from copied source - #2703

Closed
Michaelyklam wants to merge 1 commit into
nesquena:masterfrom
Michaelyklam:fix/issue-2691-agent-version-health
Closed

Michaelyklam wants to merge 1 commit into
nesquena:masterfrom
Michaelyklam:fix/issue-2691-agent-version-health

Conversation

@Michaelyklam

Copy link
Copy Markdown
Contributor

Thinking Path

  • The System panel should report the Hermes Agent version in supported Docker deployments.
  • In the two-container Compose shape, the WebUI sees a copied Agent source volume, not a live git checkout.
  • That source volume can lack both VERSION and .git, so the existing detection path falls through to not detected.
  • Hermes Agent still carries its package version in hermes_cli/__init__.py; future gateway health payloads may also expose a version.
  • This PR adds those fallbacks without changing non-Docker git/VERSION behavior.

What Changed

  • Added an Agent source-tree fallback that reads hermes_cli.__version__ when VERSION and git describe are unavailable.
  • Added a bounded gateway health fallback using GATEWAY_HEALTH_URL / HERMES_GATEWAY_HEALTH_URL or the Docker service default http://hermes-agent:8642.
  • Extracts version fields from /health or /health/detailed payloads when present.
  • Added regression coverage for copied-source and gateway-health version detection.
  • Updated the unreleased changelog.

Why It Matters

This fixes the officially documented two-container setup showing Agent: not detected even when the Agent container is present and reachable, and removes the need for users to hand-write a VERSION file after each image update.

Closes #2691

Verification

  • /home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_update_banner_fixes.py -q — 66 passed
  • /home/michael/.hermes/hermes-agent/venv/bin/python -m py_compile api/updates.py tests/test_update_banner_fixes.py
  • git diff --check

Risks / Follow-ups

  • The gateway health fallback only reports a version when the Agent gateway payload includes one; copied source trees are covered independently by hermes_cli.__version__.
  • The probe is best-effort and uses a short timeout so local/non-Docker installs still fall back cleanly.

Model Used

AI-assisted change with repository inspection, targeted editing, and shell-based test verification.

@Michaelyklam
Michaelyklam force-pushed the fix/issue-2691-agent-version-health branch from 3c37511 to eadafaa Compare May 21, 2026 14:45
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Read the diff at PR head eadafaac against origin/master, plus api/updates.py:145-170 (current _detect_agent_version()), api/config.py:90-138 (_discover_agent_dir), and the gateway's _handle_health at gateway/platforms/api_server.py:918-920 on the agent side.

Summary

Fix matches the bug shape in #2691 and the layering is right: this is a webui-side detection bug (agent-side already returns {"status":"ok","platform":"hermes-agent"} from /health), so a webui-only fix is correct. Two fallbacks are added, and ordering is good (cheap local file read before HTTP probe). A few concerns about the gateway health payload contract and one resilience edge.

Code reference

The new copied-source fallback (api/updates.py in the diff):

def _read_agent_source_version(agent_dir: Path) -> str | None:
    init_file = agent_dir / 'hermes_cli' / '__init__.py'
    try:
        text = init_file.read_text(encoding='utf-8')
    except (OSError, UnicodeDecodeError):
        return None
    m = re.search(r"""__version__\s*=\s*['"]([^'"]+)['"]""", text)
    if m and m.group(1).strip():
        return m.group(1).strip()
    return None

This is solid — hermes_cli/__init__.py:17 is __version__ = "0.14.0" (verified against the agent source tree). Regex is anchored on the __version__ = assignment, won't false-positive on other version strings.

Concern: gateway /health doesn't actually return a version today

Inspected ~/.hermes/hermes-agent/gateway/platforms/api_server.py:918-920:

async def _handle_health(self, request):
    return web.json_response({"status": "ok", "platform": "hermes-agent"})

And _handle_health_detailed at lines 922-941 returns gateway_state / platforms / pid / updated_at — also no version field. So _detect_agent_version_from_gateway_health() will, on every Docker user in the field today, succeed at the HTTP probe but find no version key and return None. The branch is correct as a future-proofing hook, but it won't fix #2691 by itself — the copied-source fallback (_read_agent_source_version) is what actually closes the issue today.

This is fine, but the PR description and CHANGELOG should be honest about that: the gateway probe is a forward-compatible scaffold that only fires once the agent gateway starts publishing version in /health (or /health/detailed). The test test_detect_agent_version_falls_back_to_gateway_health uses a FakeResponse that injects "version":"0.14.1" — that's a future-state contract, not the current one.

Suggestion: file a paired hermes-agent PR (or note in the body) to add "version": __version__ to the _handle_health and _handle_health_detailed payloads. The keys this PR checks for — version, agent_version, hermes_version, and agent.version — give the agent flexibility but at least one needs to start populating for the gateway branch to actually fire.

Resilience nit: timeout=0.75 is tight

def _detect_agent_version_from_gateway_health(timeout: float = 0.75) -> str | None:

For an in-Docker-network call between hermes-webui and hermes-agent over the compose bridge, 750ms is fine on warm containers, but cold-start probes (the moment the WebUI panel first loads after both containers come up) can take >1s to establish the first TCP connection if the agent is still booting aiohttp. Since the result is cached for 30 min (api/updates.py:35: CACHE_TTL = 1800), a single cold-miss returning 'not detected' will stick for half an hour. Bumping to ~2.0 or making it env-tunable would be safer. Tradeoff: longer worst-case System-panel load on truly unreachable agents.

Path validation looks right

parsed = urlparse(base)
if parsed.scheme not in ('http', 'https') or not parsed.netloc:
    return None

Good — defends against GATEWAY_HEALTH_URL being misconfigured to a file:// or empty value. The trailing-slash and /health//health/detailed suffix stripping in _gateway_health_base_url() is also fine.

Recommendation

  • LGTM on the copied-source fallback — that's the actual Agent: not detected in two-container Docker setup — version detection has no API fallback #2691 fix.
  • Gateway-probe branch is fine as a forward hook; flag it as such in the changelog so users don't expect it to fire on nousresearch/hermes-agent:0.14.0 today.
  • Optionally bump default timeout to ~2.0s, or add HERMES_GATEWAY_HEALTH_TIMEOUT env override.
  • Consider opening a small companion PR in nesquena/hermes-agent to add "version": __version__ to both _handle_health and _handle_health_detailed. Without that, the urllib branch is dead code in the wild.

Tests look right: test_detect_agent_version_reads_copied_source_tree exercises the real-world Docker path with VERSION + git both absent. test_detect_agent_version_falls_back_to_gateway_health covers the future-state contract. Both fakes use monkeypatch cleanly.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Shipped in v0.51.105 via release/stage-398 (#2708) — squash-merged so the source branch doesn't auto-close on the keyword.

Thanks for the contribution! Your authorship is preserved via the Co-authored-by trailer on the merged commit.

jakob1379 pushed a commit to jakob1379/hermes-webui that referenced this pull request May 21, 2026
…ource (Docker two-container System panel) (closes nesquena#2691)

Closes nesquena#2691

Co-authored-by: Michaelyklam <Michaelyklam@users.noreply.github.com>
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request May 21, 2026
…➔ 0.51.105) (#613)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.103` → `0.51.105` |

---

### Release Notes

<details>
<summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary>

### [`v0.51.105`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051105--2026-05-21--Release-CC-stage-398--4-PR-batch--hide-suggestions-preference--Docker-agent-version-from-copied-source--runner-local-adapter-selection--configurable-pinned-session-limit)

[Compare Source](nesquena/hermes-webui@v0.51.104...v0.51.105)

##### Added

- **PR [#&#8203;2687](nesquena/hermes-webui#2687 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (closes [#&#8203;2679](nesquena/hermes-webui#2679)) — Settings → Preferences gains a `Hide chat suggestions` toggle (config key `hide_empty_state_suggestions`). The empty new-chat screen normally shows three suggestion buttons as first-class tap targets, which causes accidental taps on mobile. Users who don't want the suggestions can hide them via the preference; the toggle persists across sessions and reloads. Default is OFF (suggestions remain visible) so existing users see no change.
- **PR [#&#8203;2700](nesquena/hermes-webui#2700 by [@&#8203;ai-ag2026](https://github.com/ai-ag2026) — Settings → Preferences gains a `Pinned conversations limit` numeric input. Builds on v0.51.96's [#&#8203;2614](nesquena/hermes-webui#2614) 3-cap by making the cap configurable (range 1–99, default 3, validated server-side via `_SETTINGS_INT_RANGES`). Backend validates the new cap on read, surfaces an error if a pin attempt would exceed it, and the right-click menu disables the pin item with an explanatory tooltip when the cap is reached. Default-3 keeps existing users on identical behavior.
- **PR [#&#8203;2696](nesquena/hermes-webui#2696 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) — RuntimeAdapter slice 4c — feature-flagged runner backend selection. The existing `HERMES_WEBUI_RUNTIME_ADAPTER` env var gains a new `runner-local` mode that wires up a `RunnerRuntimeAdapter` factory and adds a restart/reattach harness gate before the runner backend is used at the dispatcher. No user-visible change in this slice — unset / `legacy-direct` keeps existing behavior intact, and no production caller wires the new adapter yet. The slice exists so future work can land a sidecar runner without changing the runtime contract for existing users.

##### Fixed

- **PR [#&#8203;2703](nesquena/hermes-webui#2703 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (closes [#&#8203;2691](nesquena/hermes-webui#2691)) — System panel now detects the Hermes Agent version in Docker two-container deployments where the WebUI sees a copied Agent source volume instead of a live git checkout. The new detection cascade reads `VERSION` if present, falls back to the package metadata (`hermes_cli`), and finally to a `.git` describe if either is available, so the System panel reports the right version even when both `VERSION` and `.git` are absent in the copied source.

### [`v0.51.104`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051104--2026-05-21--Release-CB-stage-397--9-PR-batch--i18n-zh-CNzh-TW-cron-status--geist-contrast-skin-polish--tablet-hardware-Enter--stale-Codex-slash-model-state--SSE-reconnect-jitter--cron-run-inline-expansion--inflight-send-race--new-chat-model-provider-sync--virtualized-sidebar-scroll-clamp-resync--transcript-cache-invalidation-on-same-count-content)

[Compare Source](nesquena/hermes-webui@v0.51.103...v0.51.104)

##### Fixed

- **PR [#&#8203;2690](nesquena/hermes-webui#2690 by [@&#8203;laiaman](https://github.com/laiaman) — Correct the zh-CN and zh-Hant translations for the `cron_status_active` label so it reads "enabled / scheduled" (`已启用` / `已啟用`) instead of "running" (`运行中` / `活躍中`). The English source is "active" (enabled, scheduled), and the prior Chinese strings conflated it with the separate `cron_status_running` "currently executing" state, making both labels look identical when a job was both scheduled and not currently firing.
- **PR [#&#8203;2701](nesquena/hermes-webui#2701 by [@&#8203;jasonjcwu](https://github.com/jasonjcwu) — Geist-contrast skin composer polish: force `--user-bubble-text` to `#111` in light mode so typed text is black on the light input background; hide the textarea scrollbar to match the rest of the skin; recolor the send button so it reads correctly against the contrast palette.
- **PR [#&#8203;2706](nesquena/hermes-webui#2706 by [@&#8203;dobby-d-elf](https://github.com/dobby-d-elf) — Tablet (iPad-class) devices with an attached hardware keyboard now send on Enter and newline on Shift+Enter, matching desktop behavior. The prior touch-primary check forced Enter→newline on every touch device, but tablets with hardware keyboards have a physical Shift key and should follow the desktop contract. Detection uses `matchMedia('(pointer:coarse)')` + a `window.visualViewport` height-delta probe (>120px shrink = software keyboard open) so an iPad with hardware keyboard (viewport not shrunk) treats Enter as send, while a phone tapping into the composer (soft keyboard shrinks the viewport) keeps Enter as newline. Falls back to the legacy touch behavior when `visualViewport` is unavailable.
- **PR [#&#8203;2684](nesquena/hermes-webui#2684 by [@&#8203;ai-ag2026](https://github.com/ai-ag2026) — Repair stale `openai/...` slash-qualified model IDs when the active/session provider is `openai-codex`. A stale browser/localStorage selection of `openai/gpt-5` against an `openai-codex` provider previously routed the chat to OpenAI directly instead of through Codex. The cross-provider model-switch resolver now detects the mismatch and re-resolves the model to the matching `codex/...` ID before the request goes out. Explicit OpenRouter slash-qualified selections continue to fast-path through unchanged.
- **PR [#&#8203;2671](nesquena/hermes-webui#2671 by [@&#8203;AJV20](https://github.com/AJV20) (closes [#&#8203;2629](nesquena/hermes-webui#2629) + [#&#8203;2661](nesquena/hermes-webui#2661)) — Session-list SSE reconnects now use bounded jitter/backoff (each retry delay is `base*0.75 + random*(base*0.35)` where `base = min(30000, 5000 * 2^attempt)`, capped at 30s) instead of a fixed 5-second retry, so tabs that all dropped at the same time (server restart, network drop) don't all retry in lockstep. Expanded cron run rows now render the full output inline immediately on click; the truncated preview remains only for collapsed rows, and the full-output fallback no longer drops content when Markdown rendering is unavailable.
- **PR [#&#8203;2689](nesquena/hermes-webui#2689 by [@&#8203;ai-ag2026](https://github.com/ai-ag2026) — Preserve the optimistic in-flight message array across the `/api/chat/start` await window so a fast back-to-back send doesn't clear the user's message before the stream ID arrives. The fix snapshots the inflight entry before the await, recreates it if a sidebar/session refresh pruned it during that window, and skips stale-inflight cleanup for the submitting session until a stream ID is bound. Regression test covers the race.
- **PR [#&#8203;2674](nesquena/hermes-webui#2674 by [@&#8203;AJV20](https://github.com/AJV20) — Resync the new-chat model picker when the server-created session has the same model ID as the current dropdown but a different provider. New conversations now resync to the configured default model provider instead of inheriting a stale persisted picker selection (e.g. `openai/gpt-5` from a previous session). Without this, the dropdown text matched the new session's model, but the provider attribute still pointed at the stale choice.
- **PR [#&#8203;2688](nesquena/hermes-webui#2688 by [@&#8203;ai-ag2026](https://github.com/ai-ag2026) — Resync the virtualized session sidebar after restoring a saved scroll position if the browser clamps or rejects that scroll position. Without this, date-group headers could render without their session rows beneath them until the user manually scrolled or a later refresh recomputed the virtual window. Regression test pins the recompute path.
- **PR [#&#8203;2692](nesquena/hermes-webui#2692 by [@&#8203;ai-ag2026](https://github.com/ai-ag2026) (refs [#&#8203;2613](nesquena/hermes-webui#2613)) — Invalidate the transcript render cache on same-count content changes, not just on count changes. The prior cache key was `(message_count, render_window_size)`, which silently reused a cached transcript whenever a same-count edit produced visibly different content (e.g. a tool retry that replaces a single assistant message with corrected text). The new cache signature folds a content hash into the key so any visible change forces a fresh render. Regression test asserts cache-bust on same-count content swap.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19-->

Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/613
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
…ource (Docker two-container System panel) (closes nesquena#2691)

Closes nesquena#2691

Co-authored-by: Michaelyklam <Michaelyklam@users.noreply.github.com>
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
…ource (Docker two-container System panel) (closes nesquena#2691)

Closes nesquena#2691

Co-authored-by: Michaelyklam <Michaelyklam@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Agent: not detected in two-container Docker setup — version detection has no API fallback

2 participants