Skip to content

feat(start.ps1): expand hermes-agent candidate paths for Windows installers#2805

Closed
Koraji95-coder wants to merge 6 commits into
nesquena:masterfrom
Koraji95-coder:feat/windows-installer-discovery-paths
Closed

feat(start.ps1): expand hermes-agent candidate paths for Windows installers#2805
Koraji95-coder wants to merge 6 commits into
nesquena:masterfrom
Koraji95-coder:feat/windows-installer-discovery-paths

Conversation

@Koraji95-coder
Copy link
Copy Markdown
Contributor

@Koraji95-coder Koraji95-coder commented May 23, 2026

Update notes (2026-05-23, post-initial-filing):

  • PR body reformatted to match CONTRIBUTING.md. Initial filing used a generic Summary / What changed / Test plan layout; the maintainer's CONTRIBUTING.md requires the 6-section shape below (Thinking Path / What Changed / Why It Matters / Verification / Risks / Model Used) plus an AI Usage Disclosure. Caught + corrected after human-side review of upstream contributing guidance.
  • Copilot review feedback addressed in commit 6f42353. Three of Copilot's six inline comments were real bugs and have been fixed in this PR (Test-Path -PathType Container × 2 sites, conditional candidate-list build to guard ${env:ProgramFiles(x86)} null-on-32-bit, CHANGELOG entry rewritten to be stack-position-independent). Two WSL2-related comments (start.ps1:16 header, README.md:140) are out-of-scope for this PR — the affected lines originated in parent PR feat: native Windows community-guide link + start.ps1 launcher (#1952) #2783, not feat(start.ps1): expand hermes-agent candidate paths for Windows installers #2805 — and have been replied-to on-thread with a deferral note + commitment to file a separate small docs-fix PR per the maintainer's "keep diffs focused" guidance. One comment about the CHANGELOG accuracy was rolled into the rewrite above.

Thinking Path

  • Hermes WebUI aims for frictionless setup on every supported platform
  • PR feat: native Windows community-guide link + start.ps1 launcher (#1952) #2783 added start.ps1 for native Windows, mirroring start.sh's discovery shape
  • That discovery list only checked two paths: %USERPROFILE%\.hermes\hermes-agent (user-local install) and ../hermes-agent (dev-checkout layout)
  • Users hitting start.ps1 from a fresh checkout where hermes-agent was installed via the official Windows installer (which places it under %LOCALAPPDATA%\hermes\hermes-agent) or via MSI (typically Program Files) currently have to set HERMES_WEBUI_AGENT_DIR manually
  • That's exactly the friction PR feat: native Windows community-guide link + start.ps1 launcher (#1952) #2783's start.ps1 was trying to remove
  • The maintainer surfaced this in the PR #2783 review thread and explicitly suggested filing as a separate small PR per "keeping diffs focused is good policy here"

What Changed

start.ps1 candidate list expands from 2 → 5 paths, built incrementally with null-guards:

Order Path Use case Guard
1 %USERPROFILE%\.hermes\hermes-agent User-local install Always-set on Windows; unguarded
2 %LOCALAPPDATA%\hermes\hermes-agent Official Windows installer if ($root)
3 %PROGRAMFILES%\hermes\hermes-agent MSI to Program Files if ($root)
4 %PROGRAMFILES(X86)%\hermes\hermes-agent MSI to x86 path if ($root) — null on 32-bit Windows
5 ../hermes-agent Dev-checkout layout Path-derived from $RepoRoot; unguarded

Each candidate is validated as a directory via Test-Path … -PathType Container — a file (not a directory) named hermes_cli at any candidate root no longer passes the check. Applied uniformly at both the env-var-override-validation site (start.ps1:94) and the candidate-loop site (start.ps1:111).

Not-found error now enumerates every searched path via $candidates -join ', ', so when discovery fails the user sees the exact list checked.

Diff: start.ps1 | 14 +++++++-----, CHANGELOG.md | 1 + (entry under existing [Unreleased] / ### Added).

Why It Matters

The official Windows installer for hermes-agent ships it to %LOCALAPPDATA%\hermes\hermes-agent. Without this PR, every user of that installer who clones hermes-webui and runs start.ps1 has to either:

  • Read the not-found error, figure out which env var to set, set HERMES_WEBUI_AGENT_DIR, re-run
  • OR move/symlink the agent install into %USERPROFILE%\.hermes\hermes-agent

Adding the three system-wide candidate paths means installer-deployed agents auto-discover; restructuring the error message means when discovery fails on a machine with no agent installed, the user sees all paths that were checked. The conditional-build hardening means the launcher degrades gracefully on 32-bit Windows where ${env:ProgramFiles(x86)} is null and Join-Path would otherwise throw. The -PathType Container validation means a stray file named hermes_cli can't pass the discovery check and lead to a confusing import error later.

Verification

  • start.ps1 parses clean via [System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$null, [ref]$errs) — zero errors
  • Anchor-based edits (no line-number drift if surrounding code shifts later); all three anchors match exactly once in the source — no false-positive replacements
  • CHANGELOG.md entry under [Unreleased] / ### Added, immediately above the existing PR feat: native Windows community-guide link + start.ps1 launcher (#1952) #2783 entry, preserves chronological ordering
  • Wording of CHANGELOG entry is stack-position-independent — reads correctly both when viewed in this PR's stacked diff (where start.ps1 is "newly added") AND post-feat: native Windows community-guide link + start.ps1 launcher (#1952) #2783-merge (where start.ps1 is "extended")
  • Needs Windows test machine: smoke test on a fresh checkout, hermes-agent installed at %LOCALAPPDATA%\hermes\hermes-agent, no HERMES_WEBUI_AGENT_DIR set — expected: discovery finds the agent and start.ps1 launches server.py. Happy to coordinate with anyone who has the installer-deployed agent layout available; can run on a fresh foundry-side checkout if useful — let me know if you'd like before/after screenshots in a follow-up comment.
  • Needs Windows test machine: smoke test on a fresh checkout, hermes-agent NOT installed anywhere — expected: error message lists all 5 candidate paths (or 3-4 on 32-bit / restricted-env Windows where some system-wide roots are null).
  • Needs 32-bit Windows or constrained env: confirm conditional if ($root) correctly skips the missing ${env:ProgramFiles(x86)} without Join-Path throwing. Same coordination offer.

Repo doesn't have PowerShell-specific tests (test suite is pytest-based for the Python side), so the PowerShell-only changes here can't be CI-tested in the existing tests/ layout. If you'd like a Pester test added alongside this PR (or a follow-up), say the word.

Risks / Follow-ups

  • No breaking changes. User-local and dev-checkout candidate positions are preserved (slots 1 and 5). New candidates slot in between them. The -PathType Container tightening only narrows what passes — anyone whose hermes_cli was previously a file (impossible by design but theoretically possible) would now be caught earlier instead of later.
  • No new dependencies, no build-step changes, no new infrastructure. Pure addition to an existing PowerShell launcher.
  • Stacking on feat: native Windows community-guide link + start.ps1 launcher (#1952) #2783. start.ps1 doesn't exist on master yet; this PR depends on feat: native Windows community-guide link + start.ps1 launcher (#1952) #2783 landing first. Once feat: native Windows community-guide link + start.ps1 launcher (#1952) #2783 merges, rebase is clean (additive changes only).
  • Follow-up PR coming for the WSL2-once-then-native claim in start.ps1:16 header and README.md:140 — Copilot flagged it as misleading (a WSL2-created venv is Linux-only, not portable to Windows Python). Out of this PR's scope; will file a separate small docs-fix PR.
  • No security-sensitive surface touched. The discovery list just controls which directories Test-Path is called against; no auth, path-handling, uploads, or streaming behavior changes.

Model Used

  • Provider: Anthropic
  • Exact model: Claude 4.7 (Sonnet, 1M context)
  • Mode: Used to author the candidate-paths expansion (anchor-based PowerShell edits), the CHANGELOG entry, Copilot-fix follow-up commit, and this PR description. Anchor-based edits were verified via [regex]::Escape($anchor) match-once-or-fail before applying. Final human review (catching the initial PR-body format mismatch with CONTRIBUTING.md) before push.

…ena#1952)

Combined PR for nesquena#1952 per maintainer's option (a) suggestion:

- README.md: paragraph below the existing "Native Windows is not supported"
  warning linking @markwang2658's community-maintained no-Docker / no-WSL2
  setup, including the memory delta (~330 MB native vs ~1080 MB with
  WSL2+Docker) cited in the issue thread.
- start.ps1 (new, 142 lines): PowerShell launcher that bypasses
  bootstrap.py's platform refusal and invokes server.py directly. Mirrors
  start.sh's discovery (load .env, find Python, locate agent dir, set
  env defaults).
- CHANGELOG.md: ### Added entry for start.ps1, ### Documentation entry
  for the README link.

No bootstrap.py change (deferred to a separate larger PR). See PR body
for Thinking Path / What Changed / Why It Matters / Verification /
Risks / Model Used.
Three fixes from copilot-pull-request-reviewer[bot]:

- start.ps1 .env loader: switch the "already-set env var" guard from a
  truthy check to an explicit $null check. Empty-string env vars are
  falsey in PowerShell, so the old guard would mis-skip and overwrite
  an intentionally-empty pre-set var from the .env file.

- start.ps1 hermes-agent-not-found error: switch from single-quoted to
  double-quoted (interpolated) message so $env:USERPROFILE and the
  sibling-dir path render as real paths instead of literal strings.
  Pre-resolve both candidate paths into local vars for clarity.

- README.md community-guide paragraph: restructure the dense one-liner
  into a bulleted list (memory delta with "varies by config" caveat,
  what works, known limitations) AND clarify the WSL2 relationship:
  WSL2 is needed once for first-time venv creation; after that,
  start.ps1 runs natively with no WSL2 in the daily loop.
Surfaced by the maintainer's requested fresh-checkout smoke test on
PR nesquena#2783. The original `if (-not $AgentDir)` guard correctly handled
the unset-env-var case (falls through to candidate auto-discovery),
but did NOT validate the env var when it was explicitly set. A
stale or typo'd HERMES_WEBUI_AGENT_DIR pointing at a missing folder
would silently progress into `& $Python $serverPath`, leading to a
confusing exit 9009 from the Microsoft Store python3 stub (or a
missing-imports crash on a real python).

Now: when the env var is set we `Test-Path (Join-Path $AgentDir
'hermes_cli')` first. Missing -> Write-Error + exit 1 with a clear
message telling the user to either unset (to use auto-discovery) or
fix the path. Same validation the candidate-discovery loop already
applies to its own paths — just extended to the explicit-override
case.

Verified on a fresh `gh repo clone Koraji95-coder/hermes-webui`
checkout of this branch:

  PS> $env:HERMES_WEBUI_AGENT_DIR = 'C:\does\not\exist'
  PS> .\start.ps1 -Port 38913
  Write-Error: ...HERMES_WEBUI_AGENT_DIR is set to ...but no
  hermes_cli/ folder exists there. Unset the variable to fall back
  to auto-discovery, or fix the path.
  PS> $LASTEXITCODE
  1
…allers

Maintainer follow-up on PR nesquena#2783: the auto-discovery list only checked
`%USERPROFILE%\.hermes\hermes-agent` and `../hermes-agent`, but the
official Windows installer puts hermes-agent under
`%LOCALAPPDATA%\hermes\hermes-agent`, and MSI installs commonly land in
`Program Files`. Users hitting `start.ps1` from a fresh clone of an
installer-deployed agent currently have to set `HERMES_WEBUI_AGENT_DIR`
manually — which is the exact friction the PR was trying to remove.

Adds three new candidate paths to the discovery list:

- `%LOCALAPPDATA%\hermes\hermes-agent`   (official Windows installer)
- `%PROGRAMFILES%\hermes\hermes-agent`   (MSI to Program Files)
- `%PROGRAMFILES(X86)%\hermes\hermes-agent`  (MSI to x86 path)

Order matters: existing `%USERPROFILE%\.hermes\hermes-agent` stays
first (user-local installs win), then the three new system-wide paths,
then the sibling-of-RepoRoot `../hermes-agent` developer-checkout case
stays last. Same `Test-Path (Join-Path \$c 'hermes_cli')` validation
applies to every candidate.

Also restructures the not-found error message to enumerate every
searched path via `\$candidates -join ', '` — when discovery fails the
user now sees the exact list checked, so it's obvious where to drop
the agent or which `HERMES_WEBUI_AGENT_DIR` value to set.

Stacked on top of nesquena#2783. Once nesquena#2783 lands, this rebases cleanly onto
master.
Copilot AI review requested due to automatic review settings May 23, 2026 20:37
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds native Windows support guidance and a PowerShell launcher to run Hermes WebUI without bootstrap.py on Windows, plus documentation updates describing the community-supported path.

Changes:

  • Added start.ps1 PowerShell launcher that loads .env, discovers Python + hermes-agent, sets env defaults, and runs server.py.
  • Updated README with links to a community-maintained native Windows setup guide and notes/limitations.
  • Updated changelog entries describing the new launcher and Windows discovery paths.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
start.ps1 Introduces a Windows-native launcher with env loading, Python/agent discovery, and direct server.py execution.
README.md Adds native Windows community guide links and notes about native vs WSL2 usage.
CHANGELOG.md Documents the addition of start.ps1 and Windows agent discovery behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread start.ps1 Outdated
Comment thread start.ps1 Outdated
Comment thread start.ps1 Outdated
Comment thread start.ps1
Comment thread README.md
Comment thread CHANGELOG.md Outdated
Koraji95-coder and others added 2 commits May 23, 2026 16:06
…ll-guard+changelog

Three Copilot-found issues from the PR review:

1. `start.ps1:94` and `start.ps1:107` used `Test-Path (Join-Path ... 'hermes_cli')` without `-PathType Container`. A file (not a directory) named `hermes_cli` at any candidate root would pass the check, even though every downstream use of `$AgentDir` treats it as a directory. Both sites now use `-PathType Container`.

2. `start.ps1:99-104` built `$candidates` as a 5-entry literal array, two of which (`${env:ProgramFiles}` and `${env:ProgramFiles(x86)}`) can be null/empty on 32-bit Windows or in constrained environments. `Join-Path` throws on a null Path. Switched to incremental list construction with an `if ($root)` guard for the three system-wide candidates; `%USERPROFILE%` is always set on standard Windows so it stays unguarded, and the dev-checkout sibling is path-derived (not env-based) so it's also unguarded.

3. `CHANGELOG.md:9` entry implied `start.ps1` was being extended in this PR. Because nesquena#2805 stacks on nesquena#2783, the full diff Copilot reviews shows `start.ps1` as newly added — making the original phrasing ambiguous. Rewrote the entry to be stack-position-independent: lists the discovery paths added without claiming the file is new or existing, mentions the `-PathType Container` validation and the conditional-build hardening.

Did NOT address Copilot's two WSL2-related comments (`start.ps1:16` header,
`README.md:140`) in this commit — those lines are from nesquena#2783's scope, not
nesquena#2805's. Will surface as a separate small docs-fix PR per the maintainer's
"keep diffs focused" guidance.

Verification:
- `[System.Management.Automation.Language.Parser]::ParseFile` returns no
  syntax errors against the updated start.ps1
- Both `Test-Path -PathType Container` sites match the same validation
  contract the candidate loop already used at the override-validation site
- Conditional foreach is verified to produce a 4-entry list on a standard
  64-bit Windows machine (USERPROFILE + LOCALAPPDATA + Program Files +
  Program Files (x86)) and degrades gracefully if any of the three
  system-wide roots is unset

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…scovery

Surfaced by smoke-testing this PR on a fresh foundry-side checkout.
PowerShell sometimes runs as a 32-bit (WOW64) process on 64-bit
Windows — e.g., from certain shell-chain configurations (Git Bash →
pwsh through specific launchers, or some 32-bit IDE-spawned shells).
In that mode `$env:ProgramFiles` is redirected to
`C:\Program Files (x86)` by Windows, so the existing two-entry loop:

    foreach ($root in @($env:LOCALAPPDATA, ${env:ProgramFiles}, ${env:ProgramFiles(x86)}))

produced TWO identical `C:\Program Files (x86)\hermes\hermes-agent`
candidates AND silently missed the real `C:\Program Files\hermes\hermes-agent`
where MSI-installed hermes-agent actually lives.

Two changes:

1. Add `${env:ProgramW6432}` to the loop. ProgramW6432 is the canonical
   override that Windows guarantees points at the 64-bit Program Files
   regardless of process bitness. On a native 64-bit process, it
   equals `$env:ProgramFiles` (so we may pick up a duplicate, handled
   below). On a WOW64 process, it's the only way to reach
   `C:\Program Files`.

2. Add `$candidates = $candidates | Select-Object -Unique` after the
   list is built. Collapses any same-path collisions regardless of
   which env-var combination caused them — defensive against future
   env-var weirdness too (constrained sandboxes, custom Windows builds).

Verified end-to-end:
- BEFORE the fix, smoke test on a WOW64 pwsh 7.5.4 showed candidates 3 + 4
  both = `C:\Program Files (x86)\hermes\hermes-agent`. Real `C:\Program Files`
  never checked.
- AFTER the fix, same shell shows 5 distinct candidates: USERPROFILE,
  LOCALAPPDATA, ProgramW6432 (`C:\Program Files`), ProgramFiles(x86), sibling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Koraji95-coder
Copy link
Copy Markdown
Contributor Author

Smoke-tested on a Windows 11 Pro machine with hermes-agent installed at %LOCALAPPDATA%\hermes\hermes-agent

Fresh gh repo clone of this PR branch, two tests + a real WOW64 edge case found.

Test 1 — discovery via the new %LOCALAPPDATA% candidate (with HERMES_WEBUI_AGENT_DIR unset)

Pre-test state:
  HERMES_WEBUI_AGENT_DIR: (unset, as expected for Test 1)
  %USERPROFILE%\.hermes\hermes-agent\hermes_cli  exists-as-dir? False
  %LOCALAPPDATA%\hermes\hermes-agent\hermes_cli  exists-as-dir? True

Candidate list (5 entries, post-WOW64 fix):
  [1] C:\Users\koraj\.hermes\hermes-agent              -PathType Container hit: False
  [2] C:\Users\koraj\AppData\Local\hermes\hermes-agent -PathType Container hit: True  ← discovers here
  [3] C:\Program Files\hermes\hermes-agent             -PathType Container hit: False
  [4] C:\Program Files (x86)\hermes\hermes-agent       -PathType Container hit: False
  [5] C:\Users\koraj\AppData\Local\Temp\hermes-agent   -PathType Container hit: False

RESULT: DISCOVERY SUCCESS
  AgentDir resolved to: C:\Users\koraj\AppData\Local\hermes\hermes-agent

Test 2 — -PathType Container guard with HERMES_WEBUI_AGENT_DIR set to a missing path

HERMES_WEBUI_AGENT_DIR = C:\does-not-exist-smoke-test-only
Test-Path -PathType Container: False

Captured stderr (process exited with code 1):
  Write-Error: ...\start.ps1:95
  HERMES_WEBUI_AGENT_DIR is set to 'C:\does-not-exist-smoke-test-only' but no hermes_cli/ folder exists there.
  Unset the variable to fall back to auto-discovery, or fix the path.

✓ Both new behaviors land correctly: -PathType Container validation fires immediately on a missing path; the override path stays a hard exit 1 with the actionable error message.


Real bug surfaced by the smoke test → fixed in commit dbebbed

The smoke test was running under PowerShell 7.5.4 in WOW64 mode (32-bit process on 64-bit OS — happens through certain shell chains, like Git Bash → pwsh). In that mode, Windows redirects $env:ProgramFiles to C:\Program Files (x86), which meant the original candidate list produced two identical C:\Program Files (x86)\hermes\hermes-agent entries and silently missed the real C:\Program Files\hermes\hermes-agent where MSI-installed hermes-agent actually lives.

Initial candidate list (pre-fix, on WOW64):

  [3] C:\Program Files (x86)\hermes\hermes-agent   ← ${env:ProgramFiles} redirected!
  [4] C:\Program Files (x86)\hermes\hermes-agent   ← ${env:ProgramFiles(x86)} same path

Fix in dbebbed:

  1. Added ${env:ProgramW6432} to the candidate loop — Windows guarantees this points at the 64-bit C:\Program Files regardless of process bitness.
  2. Added $candidates = $candidates | Select-Object -Unique after the list is built — collapses any same-path collisions defensively (handles future env-var weirdness too).

Post-fix candidate list on the same WOW64 shell — now distinct, 5 entries, includes the real C:\Program Files\hermes\hermes-agent.


This is the "Windows test machine, no HERMES_WEBUI_AGENT_DIR set, hermes-agent installed at %LOCALAPPDATA%" verification checkbox from the PR body. Calling that one ✓ and adding the WOW64 fix to the verification list.

If you'd like before/after Lighthouse-style screenshots in addition to this transcript, happy to capture them via Chrome MCP and post.

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Summary

Read the discovery block at the PR's HEAD (commit dbebbed) and walked through the smoke-test transcript above. The WOW64 finding is a real bug that would have shipped silently; the ProgramW6432 + Select-Object -Unique fix is the right shape. Worth flagging this as the highest-value piece of the whole stack: the surface change looks small but it prevents a class of "agent installed but not found" reports from MSI-deployed-to-Program-Files users running PowerShell from inside Git Bash, VS Code's integrated terminal under certain configs, or anything else that ends up in a WOW64 process.

Code reference

start.ps1:104-122 at HEAD - the post-fix candidate loop:

$candidates = @()
$candidates += (Join-Path $env:USERPROFILE '.hermes\hermes-agent')
foreach ($root in @($env:LOCALAPPDATA, ${env:ProgramW6432}, ${env:ProgramFiles}, ${env:ProgramFiles(x86)})) {
    if ($root) { $candidates += (Join-Path $root 'hermes\hermes-agent') }
}
$candidates += (Join-Path (Split-Path -Parent $RepoRoot) 'hermes-agent')
# De-dup: when running in a WOW64 (32-bit-on-64-bit) PowerShell process,
# $env:ProgramFiles is redirected to C:\Program Files (x86)...
$candidates = $candidates | Select-Object -Unique
foreach ($c in $candidates) {
    if (Test-Path (Join-Path $c 'hermes_cli') -PathType Container) { $AgentDir = $c; break }
}

Diagnosis

WOW64 bug + fix: correct and well-targeted. Windows' filesystem-and-environment redirection layer (WoW64) maps %ProgramFiles% to the x86 path for 32-bit processes on 64-bit OS. The canonical escape hatch is %ProgramW6432%, which always points at the native 64-bit C:\Program Files regardless of process bitness. Without it, a 32-bit PowerShell process (which is more common than people expect - some VS Code terminal configs, Git Bash -> pwsh chains, older shell shortcuts) would have produced exactly the symptom in the transcript: %ProgramFiles% and %ProgramFiles(x86)% resolve to the same path, the real C:\Program Files\hermes\hermes-agent is silently skipped, and a working installer-deployed agent shows up as "not found." The Select-Object -Unique post-pass is also good defensive practice - it collapses any future env-var collisions before they become a "we checked twice but missed once" bug. LGTM.

-PathType Container tightening: also correct. The earlier Test-Path (Join-Path $c 'hermes_cli') would have passed if there was a stray file named hermes_cli at any candidate root. That's a contrived edge case but the tightening costs nothing and produces a clearer failure-point (no agent vs malformed agent) when discovery falls through. Applied uniformly at both the env-var-override-validation site (line ~95) and the candidate-loop site (line ~120). LGTM.

Enumerated not-found error: Write-Error "hermes-agent not found. Searched: $searched. ..." is a meaningful improvement over the previous "expected $primary or $sibling" wording - when discovery fails on a machine where the user thinks they installed the agent, the error names every directory checked, which makes the diagnostic loop one round instead of three.

Smoke-test transcript

The transcript above (Test 1 confirming %LOCALAPPDATA% discovery, Test 2 confirming -PathType Container validation, plus the WOW64 finding) is the right shape of evidence. The two ticked checkboxes from the PR body are satisfied. The remaining "needs 32-bit Windows or constrained env" item could be done virtually via Start-Job -InitializationScript { $env:'ProgramFiles(x86)' = $null } for a unit-style proof, but it's not blocking - the conditional if ($root) is verifiable by inspection.

Stacking note

Stacked on #2783 alongside #2806 and #2807 - this is the most substantive of the three follow-ups (real bug fix surfaced by smoke testing, not just hardening or docs). Worth landing.

Verification

I did not execute the script (cron policy is read-only on PR worktrees). The WOW64 redirect behavior is documented Windows API surface; the fix is verifiable by inspection. The -PathType Container change is mechanically equivalent at the happy-path level but tightens the failure mode. The enumerated error message is structurally observable in the diff.

1 similar comment
@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Summary

Read the discovery block at the PR's HEAD (commit dbebbed) and walked through the smoke-test transcript above. The WOW64 finding is a real bug that would have shipped silently; the ProgramW6432 + Select-Object -Unique fix is the right shape. Worth flagging this as the highest-value piece of the whole stack: the surface change looks small but it prevents a class of "agent installed but not found" reports from MSI-deployed-to-Program-Files users running PowerShell from inside Git Bash, VS Code's integrated terminal under certain configs, or anything else that ends up in a WOW64 process.

Code reference

start.ps1:104-122 at HEAD - the post-fix candidate loop:

$candidates = @()
$candidates += (Join-Path $env:USERPROFILE '.hermes\hermes-agent')
foreach ($root in @($env:LOCALAPPDATA, ${env:ProgramW6432}, ${env:ProgramFiles}, ${env:ProgramFiles(x86)})) {
    if ($root) { $candidates += (Join-Path $root 'hermes\hermes-agent') }
}
$candidates += (Join-Path (Split-Path -Parent $RepoRoot) 'hermes-agent')
# De-dup: when running in a WOW64 (32-bit-on-64-bit) PowerShell process,
# $env:ProgramFiles is redirected to C:\Program Files (x86)...
$candidates = $candidates | Select-Object -Unique
foreach ($c in $candidates) {
    if (Test-Path (Join-Path $c 'hermes_cli') -PathType Container) { $AgentDir = $c; break }
}

Diagnosis

WOW64 bug + fix: correct and well-targeted. Windows' filesystem-and-environment redirection layer (WoW64) maps %ProgramFiles% to the x86 path for 32-bit processes on 64-bit OS. The canonical escape hatch is %ProgramW6432%, which always points at the native 64-bit C:\Program Files regardless of process bitness. Without it, a 32-bit PowerShell process (which is more common than people expect - some VS Code terminal configs, Git Bash -> pwsh chains, older shell shortcuts) would have produced exactly the symptom in the transcript: %ProgramFiles% and %ProgramFiles(x86)% resolve to the same path, the real C:\Program Files\hermes\hermes-agent is silently skipped, and a working installer-deployed agent shows up as "not found." The Select-Object -Unique post-pass is also good defensive practice - it collapses any future env-var collisions before they become a "we checked twice but missed once" bug. LGTM.

-PathType Container tightening: also correct. The earlier Test-Path (Join-Path $c 'hermes_cli') would have passed if there was a stray file named hermes_cli at any candidate root. That's a contrived edge case but the tightening costs nothing and produces a clearer failure-point (no agent vs malformed agent) when discovery falls through. Applied uniformly at both the env-var-override-validation site (line ~95) and the candidate-loop site (line ~120). LGTM.

Enumerated not-found error: Write-Error "hermes-agent not found. Searched: $searched. ..." is a meaningful improvement over the previous "expected $primary or $sibling" wording - when discovery fails on a machine where the user thinks they installed the agent, the error names every directory checked, which makes the diagnostic loop one round instead of three.

Smoke-test transcript

The transcript above (Test 1 confirming %LOCALAPPDATA% discovery, Test 2 confirming -PathType Container validation, plus the WOW64 finding) is the right shape of evidence. The two ticked checkboxes from the PR body are satisfied. The remaining "needs 32-bit Windows or constrained env" item could be done virtually via Start-Job -InitializationScript { $env:'ProgramFiles(x86)' = $null } for a unit-style proof, but it's not blocking - the conditional if ($root) is verifiable by inspection.

Stacking note

Stacked on #2783 alongside #2806 and #2807 - this is the most substantive of the three follow-ups (real bug fix surfaced by smoke testing, not just hardening or docs). Worth landing.

Verification

I did not execute the script (cron policy is read-only on PR worktrees). The WOW64 redirect behavior is documented Windows API surface; the fix is verifiable by inspection. The -PathType Container change is mechanically equivalent at the happy-path level but tightens the failure mode. The enumerated error message is structurally observable in the diff.

nesquena-hermes pushed a commit that referenced this pull request May 24, 2026
…ws-only stack)

Cherry-picked PRs (all by @Koraji95-coder):
- #2805 — expand hermes-agent candidate paths for Windows installers
- #2806 — clarify native Windows venv path; remove WSL2-venv-portability claim
- #2807 — TryParse HERMES_WEBUI_PORT + exit AFTER try/finally cleanup
- #2811 — native-Windows startup E2E CI workflow

All 4 PRs were branched off #2783 (now shipped in v0.51.121). Squash-merged
each PR's unique changes onto current master with conflict resolution.
Authorship preserved on every commit. Zero impact on Linux/macOS runtime —
file scope is start.ps1, README.md (Windows section), and a new Windows-CI
workflow that only runs on PRs touching start.ps1/requirements.txt/etc.
nesquena-hermes pushed a commit that referenced this pull request May 24, 2026
…allers (#2805)

Squashed from 3 author commits onto current master (the 3 base commits from
already-shipped #2783 were filtered out by the squash):
- 6822cbb feat: expand hermes-agent candidate paths
- 6f42353 Copilot review: PathType+null-guard+changelog
- dbebbed handle WOW64 ProgramFiles redirection

Authorship preserved. CHANGELOG entry merged into batch stamp commit.
nesquena-hermes pushed a commit that referenced this pull request May 24, 2026
…eading WSL2-venv-portability claim (#2806)

Squashed from 3 author commits onto current master (3 base commits from
already-shipped #2783 were filtered out by the squash). #2805's expanded
candidate-path discovery + PathType Container check preserved from prior
stage commit.

Authorship preserved. CHANGELOG entry merged into batch stamp commit.
@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Shipped in v0.51.124 via release/stage-batch6 (#2817). The expanded candidate-list discovery + WOW64 dedupe + PathType Container guard are in. Rebased onto current master (since this was branched off the now-shipped #2783); authorship preserved. Thanks!

Koraji95-coder added a commit to Koraji95-coder/hermes-webui that referenced this pull request May 24, 2026
…k, option 1)

Per @nesquena-hermes review on nesquena#2811: hermes-agent isn't published to
PyPI, so `pip install hermes-agent` finds nothing and start.ps1's
hermes_cli guard correctly bails out — leaving the previous workflow
unable to self-validate against release/stage-batch6.

This rework adopts option 1 from the review: drop the pip install,
stub a hermes_cli/ directory with a minimal __init__.py next to the
sibling hermes-agent/ folder, then run start.ps1 for 8 seconds and
assert that none of its own Write-Error guards (no Python, no agent
dir, bad port, missing hermes_cli, missing server.py) appeared in
stderr. /health is no longer probed — the server cannot boot on a
stub, and full-boot regressions stay covered by the Linux jobs and
docker-smoke.yml.

Scope intentionally narrower than the original: this workflow
validates start.ps1's PowerShell syntax + path discovery only. The
exact bug class PR nesquena#2805 caught (WOW64 ProgramFiles redirect) would
now light up red here pre-merge, which is the reason this gate exists.

Paths filter trimmed to `start.ps1` + the workflow itself; the broader
list (requirements.txt / bootstrap.py / server.py) was inherited from
the original full-boot scoping and isn't relevant for a path-discovery-
only run.

Verification: workflow runs on this PR via its own pull_request trigger.
The first CI run on this branch IS the verification.

CHANGELOG updated under [Unreleased] with a single bullet sized to the
surrounding density.
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request May 24, 2026
…➔ 0.51.124) (#634)

This PR contains the following updates:

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

---

### Release Notes

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

### [`v0.51.124`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051124--2026-05-24--Release-CV-stage-batch6--3-PR-Windows-only-stack--agent-paths--docs--port-hardening)

[Compare Source](nesquena/hermes-webui@v0.51.123...v0.51.124)

##### Added

- **PR [#&#8203;2805](nesquena/hermes-webui#2805 by [@&#8203;Koraji95-coder](https://github.com/Koraji95-coder) — `start.ps1`: expand hermes-agent candidate paths for Windows installers. The launcher now searches `$env:USERPROFILE\.hermes\hermes-agent`, the dev-checkout sibling, and the Windows installer roots (`$env:LOCALAPPDATA\hermes\hermes-agent`, `${env:ProgramW6432}\hermes\hermes-agent`, `${env:ProgramFiles}\hermes\hermes-agent`, `${env:ProgramFiles(x86)}\hermes\hermes-agent`) with `Select-Object -Unique` to collapse WOW64 ProgramFiles redirection collisions on 32-bit PowerShell processes. Adds `-PathType Container` to the `HERMES_WEBUI_AGENT_DIR` guard so a file named `hermes_cli` doesn't false-positive. Null-guards `${env:ProgramFiles(x86)}` for constrained environments where it's missing. Zero impact on Linux/macOS — file is `start.ps1`, never loaded by `start.sh` or `bootstrap.py`.

##### Documentation

- **PR [#&#8203;2806](nesquena/hermes-webui#2806 by [@&#8203;Koraji95-coder](https://github.com/Koraji95-coder) — Native Windows venv path corrected in `start.ps1` doc-comment and `README.md`. The previous text suggested "run bootstrap.py inside WSL2 once to create the venv, then this script can use that venv" — but a WSL2-created venv is `venv/bin/python` (ELF) and cannot be invoked by native Windows Python. The corrected guidance is to create a Windows venv natively (`python -m venv venv` from PowerShell), then `start.ps1` auto-discovers `venv\Scripts\python.exe`. WSL2 remains useful as a parallel install for the full `bootstrap.py` + Linux runtime path.

##### Hardened

- **PR [#&#8203;2807](nesquena/hermes-webui#2807 by [@&#8203;Koraji95-coder](https://github.com/Koraji95-coder) — `start.ps1`: `HERMES_WEBUI_PORT` env-var parsing uses `[int]::TryParse` + range guard (1-65535) instead of a bare `[int]` cast that threw `InvalidCastException` with no context on typos or accidental shell expansion. Server-process exit code is captured into `$script:serverExitCode` and emitted via `exit` AFTER the `try/finally` cleanup, so `Pop-Location` always runs (avoids leaving the caller stuck at `$RepoRoot` in interactive or dot-sourced sessions). Also drops a non-functional `@args` splat that PowerShell doesn't populate under `[CmdletBinding()]` — the launcher's existing use case is env-var-driven, no pass-through args needed.

### [`v0.51.123`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051123--2026-05-24--Release-CU-stage-batch5--2-PR-low-risk-batch--gzipETag-static-caching--Open-in-VS-Code)

[Compare Source](nesquena/hermes-webui@v0.51.122...v0.51.123)

##### Performance

- **PR [#&#8203;2779](nesquena/hermes-webui#2779 by [@&#8203;v2psv](https://github.com/v2psv) — Static asset serving negotiates gzip, emits ETags, and uses `immutable` cache headers for fingerprinted URLs. `_serve_static()` in `api/routes.py` previously sent every `/static/*` response with `Cache-Control: no-store` and no `Content-Encoding`, so a page reload over a slow link re-downloaded the full \~2.4 MB JS+CSS shell on every visit. The fix layers three changes inside the same function: (1) gzip the body when the client opts in via `Accept-Encoding`, gated to compressible MIME types and files >1 KB; (2) emit a weak ETag derived from `(size, mtime_ns)` and short-circuit conditional GETs to `304 Not Modified`; (3) send `Cache-Control: public, max-age=31536000, immutable` when the URL carries a non-empty `?v=…` fingerprint (the `__WEBUI_VERSION__` token already substituted by the index template and referenced from `static/sw.js`'s `SHELL_ASSETS`), falling back to `public, max-age=300` otherwise. Raw bytes, compressed bytes, and ETags are cached in-process keyed by `(size, mtime_ns)` so a redeploy is picked up without a restart, while missing/random paths never enter the cache and image/font types skip gzip to avoid wasted CPU on already-compressed payloads. Measured against an asyncio TCP proxy that injects RTT + bandwidth caps for representative VPN scenarios: cold loads improve 2.7-3.1× (e.g. 80 ms RTT / 10 Mbps WireGuard goes from 4.0 s to 1.3 s), warm reloads improve 3.3-4.0× via 304 responses, and bytes-on-the-wire drop 74% on cold loads. Loopback (already fast) still benefits 2.4×. Scope is strictly `/static/*`: `/api/*`, `/stream`, `/`, `/index.html`, `/session/*`, and login/auth routes are served by independent handlers and continue to send `no-store` exactly as before — no change to CSRF, session payloads, SSE buffering, or login flows. 11 regression tests pin gzip negotiation, ETag/304 round-trip including `Vary: Accept-Encoding`, fingerprint-driven cache policy including empty `?v=`, image/tiny-file skip rules, redeploy invalidation, and the existing path-traversal sandbox.

##### Added

- **PR [#&#8203;2787](nesquena/hermes-webui#2787 by [@&#8203;munim](https://github.com/munim) — "Open in VS Code" action in workspace file browser (resolves [#&#8203;2735](nesquena/hermes-webui#2735)). Right-clicking any file, folder, or the workspace root now shows an **Open in VS Code** menu item alongside the existing Reveal in File Manager action. The action calls a new `POST /api/file/open-vscode` endpoint which resolves the workspace-relative path via the existing `safe_resolve` traversal guard, then launches VS Code via `subprocess.Popen` (fire-and-forget, consistent with `_handle_file_reveal`). The endpoint resolves the executable via `shutil.which()` first, then falls back to a hardcoded list of common install locations (macOS: `/usr/local/bin/code` and the app-bundle CLI; Linux: `/usr/bin/code`, `/snap/bin/code`; Windows: `%LOCALAPPDATA%\Programs\Microsoft VS Code\bin\code.cmd` and the `%PROGRAMFILES%` variants) so the action works even when the server process inherits a minimal PATH. Configurable via a new optional `vscode` block in `config.yaml`: `command` overrides the default `code` executable; `host_path_prefix` + `container_path_prefix` enable Docker/container host-path translation. If the command cannot be found anywhere, a descriptive error is returned instead of a bare OS error. i18n keys `open_in_vscode` and `open_in_vscode_failed` added with full translations in all 10 locales. 26 new tests in `tests/test_2735_open_in_vscode.py` pin source wiring, command-resolution logic, i18n completeness, translated strings, and live endpoint error paths.

### [`v0.51.122`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051122--2026-05-24--Release-CT-stage-batch4--4-PR-low-risk-batch--stale-cache-tail--inflight-UI--segment-flush--reasoning-accumulator)

[Compare Source](nesquena/hermes-webui@v0.51.121...v0.51.122)

##### Fixed

- **PR [#&#8203;2802](nesquena/hermes-webui#2802 by [@&#8203;ai-ag2026](https://github.com/ai-ag2026) — Drop stale inactive cached user tails when `/api/session` reloads a conversation whose saved sidecar already ends on an assistant answer. Supersedes [#&#8203;2733](nesquena/hermes-webui#2733) (held due to async-compression interaction): the new guard adds a `len(cached_messages) <= len(disk_messages)` filter so it never fires when the cache has genuine new concurrent edits beyond the disk state — only when the cache has an unsaved user row past the saved assistant tail. Adds `api/models._inactive_cache_tail_needs_disk_check()` + `_cache_has_stale_unsaved_user_tail()` helpers and 5 new tests in `tests/test_webui_state_db_reconciliation.py`. Previously-held test `test_session_compress_async_reports_stale_session_guard` now passes (verified). Closes umbrella [#&#8203;2361](nesquena/hermes-webui#2361) partially.

- **PR [#&#8203;2796](nesquena/hermes-webui#2796 by [@&#8203;ai-ag2026](https://github.com/ai-ag2026) — Clear stale inflight UI state before starting a new send so blocked composer busy-state from failed/incomplete prior turns doesn't divert new turns into the invisible queue. Five-commit squashed fix: (1) drop stale optimistic sidebar rows once canonical session data arrives, (2) clear stale busy state before send via `_clearStaleBusyStateBeforeSend()`, (3) preserve server idle rows over stale optimistic local rows, (4) let `/api/chat/start` survive non-fatal pre-start UI errors via `_runOptionalPreStartUiStep()`, (5) keep those warnings console-only instead of throwing. Adds `_shouldKeepLocalOnlyOptimisticSessionRow()` in `static/sessions.js` and 8 new tests in `tests/test_inflight_send_start_race.py`. Closes [#&#8203;2795](nesquena/hermes-webui#2795). Authorship preserved via `--author`.

- **PR [#&#8203;2777](nesquena/hermes-webui#2777 by [@&#8203;b3nw](https://github.com/b3nw) — Flush pending render before segment reset at tool/interim\_assistant boundaries so live tokens that arrived in the 66ms rAF throttle window don't get lost from the DOM when `_resetAssistantSegment()` clears `assistantBody`. New `_flushPendingSegmentRender()` helper writes via `smd`, `renderMd`, or `esc` fallback (same paths as `_doRender`) only when `_renderPending` is true. Completed transcripts were never affected — `renderMessages` rebuilds from the full `assistantText` accumulator on `done`. Adds `tests/test_issue2713_streaming_segment_flush.py`. Closes [#&#8203;2713](nesquena/hermes-webui#2713).

- **PR [#&#8203;2778](nesquena/hermes-webui#2778 by [@&#8203;b3nw](https://github.com/b3nw) — Reset reasoning accumulator per turn and prefer `reasoning_content` over `reasoning` on read. Two related bugs: (1) `reasoningText` was initialized once when the SSE stream opened and never reset between turns, so the `done` event would assign the union of every turn's reasoning to the last assistant message in multi-turn agent sessions; now reset at both turn boundaries (`tool` + `interim_assistant`). (2) `static/ui.js renderMessages` preferred `m.reasoning` (potentially corrupted by bug 1) over `m.reasoning_content` (the clean per-turn backend value); the fallback now reads `m.reasoning_content || m.reasoning`. Updates `tests/test_streaming_race_fix.py` to scope the reconnect-accumulator guard to the `_wireSSE` preamble only (turn-boundary resets inside event listeners are intentional). Adds `tests/test_issue2565_reasoning_accumulation.py`. Closes [#&#8203;2565](nesquena/hermes-webui#2565).

### [`v0.51.121`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051121--2026-05-24--Release-CS-stage-batch3--4-PR-low-risk-batch--statedb-merge--display-counts--compression-marker--Windows-launcher)

[Compare Source](nesquena/hermes-webui@v0.51.120...v0.51.121)

##### Fixed

- **PR [#&#8203;2788](nesquena/hermes-webui#2788 by [@&#8203;Carry00](https://github.com/Carry00) — Prevent `state.db` messages being silently dropped during sidecar merge. Two related bugs were combining to discard historical messages: (1) `get_state_db_session_messages()` was selecting `role, content, timestamp` but NOT `id`, so every row was assigned a `("legacy", ...)` merge key instead of `("message_id", ...)`; (2) when a WebUI-origin session was continued via another Hermes surface (Gateway, CLI), the reader was always hitting the *active* profile's `state.db` rather than the session's own profile. Symptom: a 189-message session showed only 50 in the WebUI. Fix: include `id` in the SELECT when the column exists, and accept an optional `profile=` arg so cross-profile reads use the right database. Both callers in `api/routes.py handle_get` now thread `profile=getattr(s, 'profile', None)` through.

- **PR [#&#8203;2797](nesquena/hermes-webui#2797 by [@&#8203;ai-ag2026](https://github.com/ai-ag2026) — Align messaging session display counts with deduped display messages. The `message_count` returned by `/api/session` is the display coordinate space used for pagination and the header badge. Messaging-thread `state.db` metadata can carry raw duplicate transport rows (blank assistant separators between Discord/Slack thread turns) that `_merged_session_messages_for_display()` intentionally dedupes for rendering. The advertised count was the raw row count, so the frontend expected phantom messages after dedupe — `len(display_msgs) < message_count` triggered "load older" UI states that immediately returned nothing. Fix: `raw["message_count"] = _merged_message_count` for messaging sessions, computed from the same merge that produced the displayed messages. Adds `tests/test_gateway_sync.py::test_messaging_session_message_count_matches_deduped_display_messages` covering the regression.

- **PR [#&#8203;2803](nesquena/hermes-webui#2803 by [@&#8203;simjak](https://github.com/simjak) — Compression-summary cards no longer use ordinary tool output that merely mentions context compression. The streaming auto-compression path was using a local broad substring matcher that fired on any message containing the strings "context compaction" / "context compression" / "context was auto-compressed" / "active task list was preserved across context compression", including skill/tool JSON output and ordinary user discussion about compaction. The strict predicate at `api/compression_anchor._is_context_compression_marker()` was already correctly scoped to synthetic marker prefixes on non-tool messages. Fix: expose the strict predicate as `is_context_compression_marker()` (public name) and route `api/streaming._is_context_compression_marker` through it as a backward-compatible alias. Tool/skill output that mentions compression no longer seeds `compression_anchor_summary` cards.

##### Added

- **PR [#&#8203;2783](nesquena/hermes-webui#2783 by [@&#8203;Koraji95-coder](https://github.com/Koraji95-coder) — Native Windows launcher and community-guide README link (squashed from 3 commits). `start.ps1` is a PowerShell equivalent of `start.sh` that bypasses `bootstrap.py`'s `ensure_supported_platform()` refusal and invokes `server.py` directly on native Windows. It mirrors `start.sh`'s discovery (load optional `.env` with the same readonly-var filter for `UID`/`GID`/`EUID`/`EGID`/`PPID`, find Python via `HERMES_WEBUI_PYTHON` env → `python3` → `python` → `py`, validate `HERMES_WEBUI_AGENT_DIR` on disk before use, prefer the agent's `venv\Scripts\python.exe`, set `HERMES_WEBUI_HOST` / `HERMES_WEBUI_PORT` / `HERMES_WEBUI_STATE_DIR` / `HERMES_HOME` defaults). The README adds a community-maintained native Windows setup section pointing to [@&#8203;markwang2658](https://github.com/markwang2658)'s `hermes-windows-native-guide` and `hermes-windows-native` repos with the documented memory delta (\~330 MB native vs \~1080 MB WSL2+Docker). Closes both halves of [#&#8203;1952](nesquena/hermes-webui#1952). Assumes Python + agent venv are already set up — first-time setup still needs WSL2 once to create the venv (`bootstrap.py` still refuses on native Windows).

### [`v0.51.120`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051120--2026-05-24--Release-CR-stage-batch2--3-PR-low-risk-batch--Bedrock-provider--update-check-past-tag--CORS-preflight)

[Compare Source](nesquena/hermes-webui@v0.51.119...v0.51.120)

##### Added

- **PR [#&#8203;2786](nesquena/hermes-webui#2786 by [@&#8203;munim](https://github.com/munim) — Surface AWS Bedrock as a configurable provider in the WebUI model picker. `api/config.py` registers `"bedrock": "AWS Bedrock"` in `PROVIDER_LABELS`, adds 6 default Bedrock model IDs (Claude Opus 4.7 / 4.6 / 4.5, Sonnet 4.6 / 4.5, Haiku 4.5) to `DEFAULT_MODELS["bedrock"]`, and teaches `_build_configured_model_badges()` to detect Bedrock when both `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are present (IAM-style auth, not single-API-key). Static fallback list is overridden at runtime by `hermes_cli.models.provider_model_ids("bedrock")` when the live AWS model list is reachable. Adds `tests/test_issue2720_bedrock_model_picker.py` with 11 test cases covering registry, defaults, env-detection, and runtime override. Resolves [#&#8203;2720](nesquena/hermes-webui#2720).

##### Fixed

- **PR [#&#8203;2789](nesquena/hermes-webui#2789 by [@&#8203;munim](https://github.com/munim) — Update check no longer falsely reports "Up to date" when HEAD has moved hundreds of commits past the latest tag. The hermes-agent repository keeps committing to master between tagged releases, and the old `_check_repo_release()` returned `behind=0` (since `current_tag == latest_tag`) and stopped — so the user saw "Up to date" while the working tree was hundreds of commits behind. The fix: when `behind == 0`, run `git describe --tags --always`; if the result contains the `-N-gSHA` suffix (HEAD past tag), return `None` so `_check_repo_branch()` runs and reports the real commit gap. Adds 8 new test cases in `tests/test_updates.py` covering past-tag detection, equal-tag-and-HEAD pass-through, untagged-repo behavior, and the agent-cadence [#&#8203;2653](nesquena/hermes-webui#2653) scenario. Resolves [#&#8203;2653](nesquena/hermes-webui#2653).

- **PR [#&#8203;2790](nesquena/hermes-webui#2790 by [@&#8203;weidzhou](https://github.com/weidzhou) — Add `do_OPTIONS()` handler in `server.py` so CORS preflight requests return `200 OK` with appropriate `Access-Control-Allow-*` headers instead of `501 Not Implemented`. Browsers sending a preflight OPTIONS for cross-origin API calls previously hit the BaseHTTPRequestHandler default and the entire CORS exchange was blocked. The handler narrowly responds only to OPTIONS — no broader CORS posture change to other endpoints. Resubmit of closed [#&#8203;2750](nesquena/hermes-webui#2750) (which bundled unrelated session-index changes); this PR is the minimal preflight-only split that [@&#8203;nesquena-hermes](https://github.com/nesquena-hermes) and [@&#8203;AJV20](https://github.com/AJV20) requested.

### [`v0.51.119`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051119--2026-05-24--Release-CQ-stage-batch1--3-PR-low-risk-batch--tool-cards--404-recovery--Hepburn-skin)

[Compare Source](nesquena/hermes-webui@v0.51.118...v0.51.119)

##### Fixed

- **PR [#&#8203;2801](nesquena/hermes-webui#2801 by [@&#8203;ai-ag2026](https://github.com/ai-ag2026) — Preserve settled tool cards across stream completion. The streaming `done` handler now derives anchored settled tool cards from message-level tool metadata (`message.tool_calls`, `message._partial_tool_calls`, or `content[].type === 'tool_use'`) when present, instead of unconditionally falling back to session-level `d.session.tool_calls`. The fallback could overwrite the per-message anchors after pagination/windowing because session-level coordinates may not line up with the active message array, causing tool cards to disappear on the final `done` render. Fixes [#&#8203;2613](nesquena/hermes-webui#2613), complements [#&#8203;2777](nesquena/hermes-webui#2777) (which covers pending-segment flushes at tool/interim boundaries). Adds `tests/test_streaming_markdown.py::test_done_handler_prefers_message_tool_metadata_for_settled_render` to lock the precedence.

- **PR [#&#8203;2808](nesquena/hermes-webui#2808 by [@&#8203;chouzz](https://github.com/chouzz) — Recover deterministically from boot-time `/session/{id}` 404s (Option A for [#&#8203;2798](nesquena/hermes-webui#2798)). When `loadSession()` hits a 404 during boot-time restore (`!currentSid`), `static/sessions.js` now always clears `localStorage['hermes-webui-session']`, strips the stale URL with `history.replaceState(null, '', '/')`, and rethrows so boot falls through to empty-state recovery. The previous condition required the stale id to match `localStorage`, so a stale `/session/{id}` URL with empty `localStorage` (post state-reset) could leave the UI stuck on "Session not available in web UI." Fixes [#&#8203;2798](nesquena/hermes-webui#2798).

##### Added

- **PR [#&#8203;2799](nesquena/hermes-webui#2799 by [@&#8203;gavinssr](https://github.com/gavinssr) — Add Hepburn skin (magenta-rose palette derived from the Hepburn TUI theme). Full light + dark palette under `:root[data-skin="hepburn"]` / `:root.dark[data-skin="hepburn"]`, registered in `static/boot.js` `_SKINS` and whitelisted in `static/index.html`'s inline skin gate. As part of this PR `loadSettingsPanel()` in `static/panels.js` now prefers `localStorage.getItem('hermes-skin')` over `settings.skin` when populating the skin picker (DOM truth → settings fallback), so the picker matches what the user actually sees after the inline gate has already resolved legacy aliases.

### [`v0.51.118`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051118--2026-05-22--Release-CP-stage-pr2773--1-PR-hotfix--v051117-brick-fix-chat-input-restored)

[Compare Source](nesquena/hermes-webui@v0.51.117...v0.51.118)

##### Fixed

- **PR [#&#8203;2773](nesquena/hermes-webui#2773 by [@&#8203;nesquena-hermes](https://github.com/nesquena-hermes) — fix(chat): rename `_inflightStateLimits()` in `static/ui.js` to `_getInflightStateLimits()` so it no longer collides with the `window._inflightStateLimits` config object set in `static/boot.js`. Closes [#&#8203;2771](nesquena/hermes-webui#2771). The v0.51.117 in-flight-recovery quota fix ([#&#8203;2766](nesquena/hermes-webui#2766)) declared a top-level helper with the same name as a window-attached config object; because top-level `function foo(){…}` declarations in classic (non-module) scripts attach to `window`, boot.js's `window._inflightStateLimits = {…}` assignment overwrote the function reference before any session could send. Every new chat broke on first `send()` with `TypeError: _inflightStateLimits is not a function`, leaving v0.51.117 effectively unusable. Renamed the function only (the public-ish window key is unchanged) and updated all 4 call sites. \*\*New regression test `tests/test_window_function_collision.py` scans every static JS file for top-level `function NAME()` declarations whose name is also the target of `window.NAME = {…}` / `= <number>`, the exact shape that broke [#&#8203;2715](nesquena/hermes-webui#2715) (`_pinnedSessionsLimit` in v0.51.106) and [#&#8203;2771](nesquena/hermes-webui#2771) (`_inflightStateLimits` in v0.51.117). The test fails loudly with a precise file:name diagnostic if the bug class returns. Verified end-to-end against the live browser before merge: `_getInflightStateLimits()` returns the limits object and `saveInflightState()` persists to localStorage without throwing.

### [`v0.51.117`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051118--2026-05-22--Release-CP-stage-pr2773--1-PR-hotfix--v051117-brick-fix-chat-input-restored)

[Compare Source](nesquena/hermes-webui@v0.51.116...v0.51.117)

##### Fixed

- **PR [#&#8203;2773](nesquena/hermes-webui#2773 by [@&#8203;nesquena-hermes](https://github.com/nesquena-hermes) — fix(chat): rename `_inflightStateLimits()` in `static/ui.js` to `_getInflightStateLimits()` so it no longer collides with the `window._inflightStateLimits` config object set in `static/boot.js`. Closes [#&#8203;2771](nesquena/hermes-webui#2771). The v0.51.117 in-flight-recovery quota fix ([#&#8203;2766](nesquena/hermes-webui#2766)) declared a top-level helper with the same name as a window-attached config object; because top-level `function foo(){…}` declarations in classic (non-module) scripts attach to `window`, boot.js's `window._inflightStateLimits = {…}` assignment overwrote the function reference before any session could send. Every new chat broke on first `send()` with `TypeError: _inflightStateLimits is not a function`, leaving v0.51.117 effectively unusable. Renamed the function only (the public-ish window key is unchanged) and updated all 4 call sites. \*\*New regression test `tests/test_window_function_collision.py` scans every static JS file for top-level `function NAME()` declarations whose name is also the target of `window.NAME = {…}` / `= <number>`, the exact shape that broke [#&#8203;2715](nesquena/hermes-webui#2715) (`_pinnedSessionsLimit` in v0.51.106) and [#&#8203;2771](nesquena/hermes-webui#2771) (`_inflightStateLimits` in v0.51.117). The test fails loudly with a precise file:name diagnostic if the bug class returns. Verified end-to-end against the live browser before merge: `_getInflightStateLimits()` returns the limits object and `saveInflightState()` persists to localStorage without throwing.

### [`v0.51.116`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051116--2026-05-22--Release-CN-stage-pr2676--1-PR--per-skill-enabledisable-toggle-in-Skills-panel-CLI-parity-with-hermes-skills-config)

[Compare Source](nesquena/hermes-webui@v0.51.115...v0.51.116)

##### Added

- **PR [#&#8203;2676](nesquena/hermes-webui#2676 by [@&#8203;lucasrc](https://github.com/lucasrc) — Each skill in the Skills panel now has a toggle pill (enabled/disabled) so users can turn individual skills on or off directly from the WebUI without editing `config.yaml`. Achieves parity with the existing `hermes skills config` CLI subcommand (interactive TUI that toggles `skills.disabled` in config). The disabled state is mirrored through to `skills.platform_disabled.webui` when that key is present. Disabled skills remain visible in the panel (muted via `opacity: .45`) instead of being filtered out, so users can re-enable them later. New endpoint: `POST /api/skills/toggle` validates the skill exists in the filesystem before mutating config, wraps the YAML read-modify-write under the existing `_cfg_lock` for thread safety, and calls `reload_config()` so the change takes effect immediately. Toggle pill uses theme variables (`--accent-bg-strong`, `--accent`, `--border`, `--muted`, `--accent-text`) so it adapts automatically to each skin: gold for default, red for ares, blue for poseidon, purple for sisyphus, grey for mono — verified empirically across light + dark variants. i18n keys (`skill_enabled`, `skill_disabled`, `skill_toggle_failed`) translated across all 10 locales. Default-state safety: fresh installs (no `skills.disabled` key in config) return `disabled: False` for every skill — no regression risk for new users.

### [`v0.51.115`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051115--2026-05-22--Release-CM-stage-pr2731--1-PR--clarify-prompt-collapseexpand-with-chevron-icon-polish)

[Compare Source](nesquena/hermes-webui@v0.51.114...v0.51.115)

##### Added

- **PR [#&#8203;2731](nesquena/hermes-webui#2731 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) — Clarification prompts now include a compact Collapse/Expand control so users can temporarily shrink a blocking decision card and reread the chat context behind it before responding. The toggle uses Lucide chevron icons (chevron-down expanded → click to collapse, chevron-up collapsed → click to expand) and a small circular pill matching the existing composer-button design language. The collapsed card sits cleanly above the composer at every tested viewport (desktop 1920×1080, mobile iPhone 14 390×844) without edge clipping. New clarification prompts still open expanded so users notice them.

### [`v0.51.114`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051114--2026-05-22--Release-CL-stage-407--1-PR--update-check-recovery-from-remote-re-tags)

[Compare Source](nesquena/hermes-webui@v0.51.113...v0.51.114)

##### Fixed

- **PR [#&#8203;2758](nesquena/hermes-webui#2758 by [@&#8203;nesquena-hermes](https://github.com/nesquena-hermes) — fix(updates): pass `--force` to `git fetch --tags` in `api/updates.py` so the WebUI's release-tracking update check can recover from a remote re-tag (e.g. a release tag that was force-pushed to a new commit after a squash-merge). Without `--force`, plain `git fetch origin --tags` returns `! [rejected] vX.Y.Z (would clobber existing tag)` and the entire update path (check, force-apply, normal-apply) jams indefinitely — neither the periodic check nor manual "Check now" nor the Update button can recover. Three fetch call sites were patched (`_check_repo`, `apply_force_update`, `apply_update`) to use `--tags --force`; the WebUI never pushes tags, so deferring to the remote's view is the right contract. Closes [#&#8203;2756](nesquena/hermes-webui#2756).

### [`v0.51.113`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051113--2026-05-22--Release-CK-stage-406--1-PR--composer-model-picker-lag-fix--hard-refresh-recovery)

[Compare Source](nesquena/hermes-webui@v0.51.112...v0.51.113)

##### Fixed

- **PR [#&#8203;2743](nesquena/hermes-webui#2743 by [@&#8203;franksong2702](https://github.com/franksong2702) — Composer model picker now opens immediately from the existing static option list while the dynamic `/api/models` catalog hydrates in the background, instead of blocking the click on the catalog request. A just-selected session model also survives a hard refresh that interrupts the async `/api/session/update` POST: the selection is staged into `sessionStorage` (keyed by session\_id, 10-minute TTL) before the async update flies, and `loadSession()` re-applies the pending pick on next session restore and retries the persistence call. Tests pin the new ordering: visible picker render before `await`, pending-state save before `await api('/api/session/update')`, and pending-state replay before the first `syncTopbar()` projects server metadata.

### [`v0.51.112`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051112--2026-05-22--Release-CJ-stage-405--1-PR--session-model-authoritative-across-restore)

[Compare Source](nesquena/hermes-webui@v0.51.111...v0.51.112)

##### Fixed

- **PR [#&#8203;2737](nesquena/hermes-webui#2737 by [@&#8203;ai-ag2026](https://github.com/ai-ag2026) — Keep the session model authoritative when a restored session is reactivated. Previously, stale browser-cached picker state could override an active conversation's model in four scenarios: (1) on initial boot when `localStorage` had a different model preference than the active session, (2) on hard refresh when `S._bootReady` revealed the composer chip before the live catalog hydrated, (3) when the session's model wasn't in the current provider catalog (the static/default fallback silently rewrote `S.session.model`), (4) when starting a new session whose model wasn't in the static HTML dropdown. The fix: `loadSession()` now requests `resolve_model=1` so backend normalization happens synchronously with metadata; boot model hydration prefers the active session over `localStorage`; hard refresh re-runs the model dropdown hydration before `_bootReady`; a new `_ensureModelOptionInDropdown()` helper injects a `data-custom='1'` option for models not in the catalog instead of silently rewriting `S.session.model` to the default. 100 LOC of new pytest regression coverage pinning each behavior.

### [`v0.51.111`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051111--2026-05-22--Release-CI-stage-404--1-PR--keep-statedb-replays-out-of-sidecar-tail)

[Compare Source](nesquena/hermes-webui@v0.51.110...v0.51.111)

##### Fixed

- **PR [#&#8203;2746](nesquena/hermes-webui#2746 by [@&#8203;ai-ag2026](https://github.com/ai-ag2026) — Prevent replayed state.db rows from being appended after an already-correct sidecar transcript tail. `merge_session_messages_append_only()` previously tried to skip state.db rows replaying the sidecar, but two edge cases leaked through: (1) the final row of a replayed sidecar prefix was not skipped because the replay index had reached the sidecar sequence length, and (2) a replayed middle segment was not considered prefix replay, so old state.db rows could be appended after the saved assistant tail. That made `/api/session` appear to end on an old user prompt even when the saved sidecar already ended on the real assistant answer. The fix tracks per-(role, content) visible-occurrence counts in the sidecar and uses that as a replay budget when comparing state.db rows; legitimate repeated messages from state.db are still preserved. `_has_visible_duplicate()` is kept as a thin wrapper around the new `_matching_visible_duplicate()` for backwards compatibility. Regression test covers both full-replay and middle-segment replay shapes.

### [`v0.51.110`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051110--2026-05-22--Release-CH-stage-403--2-PR-batch--default-personality-from-config--sort-configured-providers-to-top)

[Compare Source](nesquena/hermes-webui@v0.51.109...v0.51.110)

##### Added

- **PR [#&#8203;2747](nesquena/hermes-webui#2747 by [@&#8203;s010mn](https://github.com/s010mn) — `new_session()` now reads `display.personality` from `config.yaml` as the default for new conversations. Previously every new session started with `personality=None` and required an explicit `/personality <name>` slash command. Values `'none'`, `'default'`, `'neutral'`, and empty string are treated as no-personality. Case-insensitive — `personality: Taleb` normalizes to `taleb`. Config-read is wrapped in try/except so malformed config falls back to the prior behavior rather than crashing session creation. The `/personality` slash command still works for per-session overrides.
- **PR [#&#8203;2683](nesquena/hermes-webui#2683 by [@&#8203;jasonjcwu](https://github.com/jasonjcwu) — Sort providers so configured/custom entries appear first in both the model picker dropdown (`api/config.py::get_available_models`) and the Settings providers panel (`api/providers.py::get_providers`). Priority order: (1) the active provider, (2) `custom:*` providers from `custom_providers` config, (3) providers with configured API keys (credential pool or `config.yaml`), (4) all others alphabetical. Eliminates scrolling past 25+ unconfigured providers to find the one in active use.

### [`v0.51.109`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051109--2026-05-22--Release-CG-stage-402--2-PR-batch--sidebar-action-menu-click-stability--chat-panel-sidebar-resync-after-navigation)

[Compare Source](nesquena/hermes-webui@v0.51.108...v0.51.109)

##### Fixed

- **PR [#&#8203;2741](nesquena/hermes-webui#2741 by [@&#8203;ai-ag2026](https://github.com/ai-ag2026) — Keep the sidebar conversation actions menu open while session-list refreshes, stream updates, or panel-resync repairs arrive. Previously the three-dot menu beside chat titles could be torn down before the user finished clicking it because `renderSessionListFromCache()` rebuilt the row DOM (and the fixed-position menu's anchor) without checking whether the menu was open. The new early-return at the top of the refresh keeps the menu stable; destructive menu actions explicitly close the menu before they fire, so dismissal still works as expected.
- **PR [#&#8203;2736](nesquena/hermes-webui#2736 by [@&#8203;ai-ag2026](https://github.com/ai-ag2026) — Resync the chat sidebar after returning from Settings/Logs/other panels. The session list is virtualized, and the browser can clamp the preserved scrollTop during a panel transition; without a render after the chat view is visible again, stale virtual spacer/header DOM remained until the next manual scroll. The new `_resyncChatSidebarAfterPanelSwitch()` helper runs one guarded `requestAnimationFrame` after the panel becomes visible, bails if a rename input or action menu is open, and uses no polling.

</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/634
ai-ag2026 pushed a commit to ai-ag2026/hermes-webui that referenced this pull request May 24, 2026
…k, option 1)

Per @nesquena-hermes review on nesquena#2811: hermes-agent isn't published to
PyPI, so `pip install hermes-agent` finds nothing and start.ps1's
hermes_cli guard correctly bails out — leaving the previous workflow
unable to self-validate against release/stage-batch6.

This rework adopts option 1 from the review: drop the pip install,
stub a hermes_cli/ directory with a minimal __init__.py next to the
sibling hermes-agent/ folder, then run start.ps1 for 8 seconds and
assert that none of its own Write-Error guards (no Python, no agent
dir, bad port, missing hermes_cli, missing server.py) appeared in
stderr. /health is no longer probed — the server cannot boot on a
stub, and full-boot regressions stay covered by the Linux jobs and
docker-smoke.yml.

Scope intentionally narrower than the original: this workflow
validates start.ps1's PowerShell syntax + path discovery only. The
exact bug class PR nesquena#2805 caught (WOW64 ProgramFiles redirect) would
now light up red here pre-merge, which is the reason this gate exists.

Paths filter trimmed to `start.ps1` + the workflow itself; the broader
list (requirements.txt / bootstrap.py / server.py) was inherited from
the original full-boot scoping and isn't relevant for a path-discovery-
only run.

Verification: workflow runs on this PR via its own pull_request trigger.
The first CI run on this branch IS the verification.

CHANGELOG updated under [Unreleased] with a single bullet sized to the
surrounding density.
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.

3 participants