Skip to content

ci(windows): add native-Windows startup E2E workflow#2811

Closed
Koraji95-coder wants to merge 2 commits into
nesquena:masterfrom
Koraji95-coder:ci/native-windows-startup-e2e
Closed

ci(windows): add native-Windows startup E2E workflow#2811
Koraji95-coder wants to merge 2 commits into
nesquena:masterfrom
Koraji95-coder:ci/native-windows-startup-e2e

Conversation

@Koraji95-coder
Copy link
Copy Markdown
Contributor

Thinking Path

  • PR feat: native Windows community-guide link + start.ps1 launcher (#1952) #2783 adds start.ps1 + the native-Windows community-guide link. Right now there's no CI gate that exercises the native-Windows launch path, so regressions can land silently and only surface when a user tries to follow the README.
  • PR feat(start.ps1): expand hermes-agent candidate paths for Windows installers #2805 (start.ps1 candidate-path discovery) caught a WOW64 $env:ProgramFiles redirect bug only post-filing, via manual smoke-testing. Same class of bug could regress at any later refactor with no signal until users complain.
  • The fix is a windows-latest workflow that just runs the README's documented steps and asserts /health returns 200. Tiny, focused, becomes a permanent gate.
  • Out of scope: WSL2 path (already covered by existing Linux jobs), docker (covered by docker-smoke.yml).

What Changed

One new file: .github/workflows/native-windows-startup.yml (75 lines).

The workflow:

  1. Triggers on PRs that touch start.ps1 / requirements.txt / bootstrap.py / server.py / itself.
  2. actions/checkout + actions/setup-python@v5 with python-version: '3.11'.
  3. Follows the README native-Windows setup VERBATIM:
    • python -m venv venv
    • venv\Scripts\python.exe -m pip install -r requirements.txt
    • pwsh -NoLogo -File .\start.ps1 (in a background Start-Job)
  4. Polls http://localhost:8787/health for up to 60 s.
  5. Asserts both / and /health return HTTP 200.
  6. Kills the background server in always() cleanup.

CHANGELOG entry under [Unreleased]### Added.

No code changes. No new dependencies. Pure CI addition.

Why It Matters

  • Catches WOW64-class regressions before merge. The 32-bit PowerShell \$env:ProgramFiles redirect bug from feat(start.ps1): expand hermes-agent candidate paths for Windows installers #2805 was caught only after the PR landed because nobody had a local sandbox+smoke step. This workflow runs that smoke automatically on every relevant PR going forward.
  • Locks in the documented setup path. Today, the only thing preventing a regression in start.ps1's venv discovery is human vigilance. After this lands, the README's documented steps become a CI gate — if start.ps1 stops auto-discovering venv\Scripts\python.exe, this workflow fails before merge.
  • Concrete green signal in every relevant PR. Future PRs touching the listed files will get a green ✓ "Native Windows startup" check on the PR page, reducing maintainer review time and giving contributors immediate feedback.

Verification

  • YAML syntax: lints clean via actionlint locally (no schema errors).
  • Logic equivalence to the README: the workflow's steps are a 1:1 translation of the README's python -m venv venvpip install -r requirements.txtpwsh .\start.ps1 → wait for /health flow.
  • No local Windows Sandbox smoke transcript pasted: Windows Sandbox on a fresh Win11 image ships with Windows Defender Application Control in enforcement mode, which blocks third-party signed binaries (uv, python.org installer's PythonBA.dll) at process-launch time. Locally smoke-testing this workflow IN Windows Sandbox is therefore not possible without disabling WDAC (which defeats the sandbox's purpose). GitHub's windows-latest runners DON'T have WDAC enforcement, so the workflow runs cleanly there — which is the actual deployment target. The first CI run on this PR itself is the canonical verification.
  • First CI run on this PR: will run automatically when the PR is opened (the workflow's pull_request trigger fires on its own definition file). That's the verification.

No automated test changes — this IS the test.

Risks / Follow-ups

Model Used

  • Provider: Anthropic
  • Exact model: Claude Opus 4.7 (1M context)
  • Mode: Authored the workflow YAML + this PR description. The native-Windows-on-Sandbox WDAC limitation that ruled out a pre-filing local smoke transcript was identified during local debugging (Astral's uv.exe and the Python.org installer's PythonBA.dll both blocked by Windows Defender Application Control on a fresh Windows Sandbox image). The CI workflow's windows-latest target environment isn't subject to that restriction, so it's where verification happens.

Copilot AI review requested due to automatic review settings May 24, 2026 03:55
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 first-class support for running Hermes WebUI on native Windows by introducing a PowerShell launcher, documenting the community native-Windows setup, and adding CI coverage to prevent Windows-specific startup regressions.

Changes:

  • Add start.ps1 to launch server.py directly on Windows (bypassing bootstrap.py’s Windows refusal) with .env loading and agent discovery.
  • Update README with links and notes for the community-maintained native Windows setup.
  • Add a GitHub Actions workflow that boots the app on windows-latest and probes /health and /.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
start.ps1 New native Windows launcher that mirrors start.sh discovery and runs server.py directly.
README.md Adds community native Windows guide links and notes, including mention of start.ps1.
CHANGELOG.md Documents the new launcher and Windows startup CI workflow.
.github/workflows/native-windows-startup.yml Adds Windows E2E startup check that creates a venv, starts the server, and probes endpoints.

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

Comment thread start.ps1
if (-not $Python) {
foreach ($candidate in @('python3', 'python', 'py')) {
$cmd = Get-Command $candidate -ErrorAction SilentlyContinue
if ($cmd) { $Python = $cmd.Source; break }
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Out-of-scope for this PR — the py fallback discussed lives in start.ps1 which is added by PR #2783. This PR (the CI workflow) only includes start.ps1 in its diff because it stacks on #2783 against master. Once #2783 merges, this PR rebases to drop those lines. Valid concern though — worth tracking as a follow-up to start.ps1 itself; @('py','-3') or splitting $PythonCommand / $PythonArgs is the right shape.

Comment thread start.ps1 Outdated

Push-Location $RepoRoot
try {
& $Python $serverPath @args
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Duplicate of the comment on line 79 — same out-of-scope reasoning. The py launcher concern lives in start.ps1 (PR #2783's territory), not this CI workflow PR.

Comment on lines +45 to +52
- name: Start server (background) via start.ps1
shell: pwsh
run: |
$job = Start-Job -ScriptBlock {
param($repo)
Set-Location $repo
pwsh -NoLogo -File .\start.ps1
} -ArgumentList $PWD
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 02f05339 — added a 'Provision hermes-agent (sibling install)' step that pip-installs hermes-agent into a sibling venv, copies hermes_cli from site-packages so start.ps1's Test-Path hermes_cli -PathType Container guard passes, and exports HERMES_WEBUI_AGENT_DIR for start.ps1 to consume. Falls back to an empty hermes_cli dir if site-packages doesn't expose it — server.py would then surface a clearer runtime error if the CLI is actually needed at startup, which is the right signal.

Comment on lines +48 to +52
$job = Start-Job -ScriptBlock {
param($repo)
Set-Location $repo
pwsh -NoLogo -File .\start.ps1
} -ArgumentList $PWD
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 02f05339 — replaced Start-Job with Start-Process -PassThru capturing the wrapper PID. The cleanup step now runs taskkill /PID $env:SERVER_PID /T /F which walks the process tree (/T) and force-terminates (/F). Also added belt-and-suspenders: kill anything still bound to port 8787 if the taskkill missed something. Also captured start.ps1 stdout/stderr to $RUNNER_TEMP and dump them on /health timeout so future failures have actionable signal.

Comment on lines +80 to +87
- name: Stop background server
if: always()
shell: pwsh
run: |
if ($env:JOB_ID) {
Stop-Job -Id $env:JOB_ID -ErrorAction SilentlyContinue
Remove-Job -Id $env:JOB_ID -Force -ErrorAction SilentlyContinue
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same fix as above — addressed in 02f05339. The taskkill /T /F tree-walk is in the always() cleanup step and captures the spawned server.py child process via the wrapper PID.

Comment thread start.ps1 Outdated
Comment on lines +15 to +16
a fresh bootstrap. For first-time setup, run bootstrap.py inside
WSL2 once to create the venv, then this script can use that venv.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already resolved upstream — PR #2806 corrected this guidance in start.ps1's .DESCRIPTION block (the WSL2 venv claim was replaced with the native Windows python -m venv venv setup). #2806 has been merged. This PR shows the stale text only because it stacks on #2783 against master; once #2783 rebases onto master, the corrected text will appear.

Comment thread README.md Outdated
- **Memory:** community-measured ~330 MB native vs ~1080 MB with WSL2+Docker (varies by configuration).
- **What works:** chat, workspace browser, session management, all themes.
- **Known limitations:** some POSIX-style file paths surface in the workspace browser; bash-assuming agent tools may not work natively.
- **WSL2 relationship:** WSL2 is recommended *once* for first-time venv creation (since `bootstrap.py` currently refuses on native Windows). After the venv exists, `start.ps1` at the repo root runs the WebUI natively by invoking `server.py` directly — no WSL2 needed for day-to-day use.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already resolved upstream — PR #2806 split the dense WSL2 bullet into a 'Native Windows setup' bullet (python -m venv venvpip install -r requirements.txtpwsh .\start.ps1) and a separate WSL2-relationship one-liner that explicitly notes a WSL2-built venv is a Linux venv that won't be invokable by native Windows Python. #2806 has been merged. The stale text shows in this PR's diff only because it stacks on #2783 against master.

Koraji95-coder added a commit to Koraji95-coder/hermes-webui that referenced this pull request May 24, 2026
Two workflow-specific issues caught by Copilot's review:

1. **hermes-agent not provisioned.** start.ps1 errors out if it can't
   find %USERPROFILE%\.hermes\hermes-agent or a ../hermes-agent sibling.
   Added a "Provision hermes-agent (sibling install)" step that:
     - pip-installs hermes-agent into a sibling directory's venv
     - copies hermes_cli from site-packages to satisfy start.ps1's
       Test-Path hermes_cli -PathType Container guard
     - exports HERMES_WEBUI_AGENT_DIR for start.ps1 to consume
   Falls back to creating an empty hermes_cli dir if the package
   layout doesn't expose hermes_cli — server.py will then surface a
   clearer runtime error if the CLI is actually needed.

2. **Stop-Job doesn't tree-kill the spawned server child.** Replaced
   the Start-Job wrapper with Start-Process -PassThru capturing the
   wrapper PID, then `taskkill /PID $SERVER_PID /T /F` walks the
   process tree in the always() cleanup step. Belt-and-suspenders:
   also kill anything still bound to port 8787.

Also captured start.ps1's stdout/stderr to $RUNNER_TEMP and dumped
them on /health timeout so future failures have actionable signal
instead of an opaque "60s timeout" message.

Timeout-minutes bumped 10 → 12 to accommodate the hermes-agent venv
install step (which can add ~30-60s on cold cache).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
Copy link
Copy Markdown
Collaborator

Hi @Koraji95-coder — held this one out of the v0.51.124 batch after running the workflow against itself on release/stage-batch6. The workflow's Provision hermes-agent step uses python -m pip install hermes-agent, but hermes-agent isn't published to PyPI (it's distributed as a private repo / installed from source), so the step finds no hermes_cli/ directory and start.ps1's guard from #2783 correctly bails out.

Could you rework the provisioning step? Two viable options:

  1. Mock-only: drop the package install and just create a stub hermes_cli/ directory with a minimal __init__.py so start.ps1's existence check passes. The workflow's value would be PowerShell syntax + start.ps1 path-discovery validation, not a full server boot.
  2. Real install from source: git clone https://github.com/NousResearch/hermes-agent (or whatever the canonical repo URL is) — requires a deploy token for the runner if the repo is private.

Option 1 is probably what we want — it catches WOW64/path/PowerShell regressions without depending on a full agent install. The other 3 PRs (#2805/#2806/#2807) shipped in v0.51.124 today; this one stays open with the hold label until the workflow can self-validate. Thanks!

@nesquena-hermes nesquena-hermes added hold changes-requested Maintainer left detailed feedback requesting changes; PR is waiting on author to address labels 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.
@Koraji95-coder Koraji95-coder force-pushed the ci/native-windows-startup-e2e branch from 02f0533 to 6636cd4 Compare May 24, 2026 05:20
The path-discovery step succeeds on the first run, but the cleanup
step exits non-zero because `taskkill /PID 5560 /T /F` returns 128
("process not found") when server.py has already exited on the mock
hermes_cli stub. That's the expected steady state for this mock-only
workflow, not a failure.

Two-line fix: reset `$global:LASTEXITCODE = 0` after the taskkill
call, and explicit `exit 0` at the end of the step so any other
external-command exit codes don't bubble up. The try/catch wrapper
didn't help because taskkill writes its diagnostic to stderr without
raising a PowerShell exception — `catch` never fired.

Run 26352805510 on this branch shows the failure shape: "OK: start.ps1
path discovery - all guards passed." in the verify step, then
"ERROR: The process '5560' not found." in the cleanup step. Path
discovery is what this workflow exists to validate; cleanup just has
to not fail the job.
@Koraji95-coder
Copy link
Copy Markdown
Contributor Author

Koraji95-coder commented May 24, 2026

Reworked per your option 1 — pushed 11ae4456. The workflow now:

  • Drops the pip install hermes-agent step (hermes-agent isn't on PyPI as you noted)
  • Stubs a hermes_cli/ directory with a minimal __init__.py next to a sibling hermes-agent/ folder
  • Runs start.ps1 for 8 seconds and asserts that none of its Write-Error guards (no Python, no agent dir, bad port, missing hermes_cli, missing server.py) appeared in stderr
  • Does NOT probe /health — the server can't actually boot on a stub, so we don't pretend to

Scope is now strictly: PowerShell syntax + start.ps1 path discovery. The exact bug class PR #2805 caught post-filing (WOW64 ProgramFiles redirect) would light up red here pre-merge, which is the reason this gate exists.

Also rebased onto current master (the previous branch was on release/stage-batch6-era state — now squared with v0.51.124). CHANGELOG entry reduced from a 3-line bullet to a single bullet under [Unreleased], sized to the surrounding density.

Verification: run 26352903974 on this branch went green — both "Native Windows startup" and "Tests" passed on sha=11ae4456. The path-discovery step printed OK: start.ps1 path discovery - all guards passed. and the cleanup step completed clean (had a follow-up commit to swallow taskkill's 128-exit when the stub process is already gone — that's expected steady state, not a failure).

Ready for your review when you have a moment.


Edited 2026-05-24: corrected a broken verification link (was pointing at the fork's actions URL with the wrong run id; PR runs live on the upstream repo). The right URL is the one shown above. Caught by a human reviewer on our side before you saw the stale link.

@nesquena-hermes nesquena-hermes added the ready-for-review Held PR feedback addressed; awaiting maintainer to remove hold label May 24, 2026
@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Ready for re-review — pip install hermes-agent step removed, workflow self-tests green

Thanks @Koraji95-coder, this is exactly the rework I was hoping for. Re-checked your 11ae4456:

  • Provision hermes-agent step replaced with a stub hermes_cli/__init__.py next to a sibling hermes-agent/ folder — no more PyPI install dependency
  • start.ps1 runs for 8s and asserts none of the Write-Error guards fired (no Python, no agent dir, bad port, missing hermes_cli, missing server.py) — that's the right invariant set
  • ✅ Scope narrowed to PowerShell syntax + path discovery; no fake /health probe on a stub
  • ✅ Run 26352903974 is green
  • ✅ Rebased onto current master (was on release/stage-batch6-era)
  • ✅ CHANGELOG reduced to single bullet, matching surrounding density

The taskkill exit-128 cleanup follow-up was a nice catch.

Applying ready-for-review. Leaving hold + changes-requested for @nesquena to remove — this is a maintainer call. If approved, planning to stage into the next low-risk release batch alongside locale + CSS-only fixes.

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Shipped in v0.51.127 (Release CY) — https://github.com/nesquena/hermes-webui/releases/tag/v0.51.127

Thanks for the contribution! Your changes are now live on master. Closing this PR as merged-via-stage-branch.

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.
ai-ag2026 pushed a commit to ai-ag2026/hermes-webui that referenced this pull request May 24, 2026
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request May 25, 2026
…➔ 0.51.134) (#650)

This PR contains the following updates:

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

---

> ⚠️ **Warning**
>
> Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/567) for more information.

---

### Release Notes

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

### [`v0.51.134`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051134--2026-05-25--Release-DF-stage-batch16--single-PR-Windows-path-defaults)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.133...v0.51.134)

##### Fixed

- **PR [#&#8203;2897](https://github.com/nesquena/hermes-webui/issues/2897)** by [@&#8203;chouzz](https://github.com/chouzz) — On Windows, WebUI default state and config paths now align with Hermes Agent's `%LOCALAPPDATA%\hermes` convention instead of `%USERPROFILE%\.hermes`, so a fresh Windows install finds the same `~/.hermes/config.yaml` / `auth.json` / `webui/` state directory that the Hermes Agent created. POSIX behavior is unchanged (`~/.hermes` remains the default). `HERMES_HOME` and `HERMES_WEBUI_STATE_DIR` env overrides take precedence on both platforms. Closes [#&#8203;2840](https://github.com/nesquena/hermes-webui/issues/2840).

### [`v0.51.133`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051133--2026-05-25--Release-DE-stage-batch15--6-PR-contributor-batch--aux-task-validation--workspace-artifact-gating--update-apply-guard--Joplin-auth-header--prefill-cache-guard--notes-drawer-i18n)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.132...v0.51.133)

##### Fixed

- **PR [#&#8203;2891](https://github.com/nesquena/hermes-webui/issues/2891)** by [@&#8203;franksong2702](https://github.com/franksong2702) — Auxiliary model settings now reject unknown task slots instead of allowing arbitrary keys under `config.yaml`'s `auxiliary` block. Valid slots and the `__reset__` sentinel continue to work.
- **PR [#&#8203;2892](https://github.com/nesquena/hermes-webui/issues/2892)** by [@&#8203;franksong2702](https://github.com/franksong2702) — Workspace Artifacts now keeps read-only tool paths out of the "files changed" list by gating structured path extraction to known file-mutation tools.
- **PR [#&#8203;2893](https://github.com/nesquena/hermes-webui/issues/2893)** by [@&#8203;franksong2702](https://github.com/franksong2702) — Update Now no longer reports success or enters the restart wait flow when no WebUI or Agent update target is selected.
- **PR [#&#8203;2895](https://github.com/nesquena/hermes-webui/issues/2895)** by [@&#8203;franksong2702](https://github.com/franksong2702) — Cached WebUI agents no longer overwrite `prefill_messages` with an empty list when a later request does not include explicit prefill context.
- **PR [#&#8203;2894](https://github.com/nesquena/hermes-webui/issues/2894)** by [@&#8203;franksong2702](https://github.com/franksong2702) — Joplin notes drawer API calls now send the Web Clipper token in an `Authorization` header instead of placing it in the request URL query string.
- **PR [#&#8203;2896](https://github.com/nesquena/hermes-webui/issues/2896)** by [@&#8203;franksong2702](https://github.com/franksong2702) — Third-party notes drawer copy now uses localized strings in the supported non-English locale bundles (it, ja, ru, es, de, zh, zh-Hant, pt, ko, fr, tr) instead of reusing the English defaults.

### [`v0.51.132`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051132--2026-05-24--Release-DD-stage-batch14--4-PR-replayed-context--interrupted-response--shutdown-affordance--passkey-opt-in)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.131...v0.51.132)

##### Added

- **PR [#&#8203;2859](https://github.com/nesquena/hermes-webui/issues/2859)** by [@&#8203;AJV20](https://github.com/AJV20) — Optional passkey/WebAuthn sign-in for password-protected WebUI instances. Authenticated users can register/remove passkeys from Settings -> System, and `/login` shows a passwordless sign-in button only after a passkey exists. Password auth remains the default-off bootstrap and recovery path. **Opt-in default-off behind `HERMES_WEBUI_PASSKEY=1` env var or `webui_passkey_enabled: true` config flag** — when disabled, the UI block hides, all 6 `/api/auth/passkey/*` endpoints return 404, and `is_auth_enabled()` ignores any pre-existing credential file so the auth posture cannot silently flip if the flag is unset later.

- **PR [#&#8203;2824](https://github.com/nesquena/hermes-webui/issues/2824)** by [@&#8203;gavinssr](https://github.com/gavinssr) — A "Stop server" affordance in Settings → System that gracefully shuts down the local WebUI server. Useful when WebUI was launched via `./ctl.sh start` or the native macOS/Windows app and the user wants to stop it without context-switching to a terminal. Confirmation dialog before the actual shutdown. The `/api/shutdown` route is CSRF-gated and intended for local-loopback use. Originally a title-bar button; relocated to Settings per the project's deep-UX rule (default-hidden for niche destructive actions on always-visible surfaces).

##### Fixed

- **PR [#&#8203;2685](https://github.com/nesquena/hermes-webui/issues/2685)** by [@&#8203;LumenYoung](https://github.com/LumenYoung) — Prevent replayed context in chat reconciliation and metering. When a WebUI session is recovered (e.g., after a process restart, network drop, or browser reload), the sidebar/`state.db` reconciliation logic walks the sidecar transcript in order and only skips rows that can actually be aligned with the remaining sidecar context. The prior set-membership check was too broad: a legitimate fresh message that happened to share a key with any older repeated short message in the sidecar was mis-classified as already-seen and dropped from the replay, leading to lost context and inconsistent metering. Also caps the per-turn live-tool-prompt token estimate at 12,000 to prevent unbounded growth on bursts of large tool reads before exact provider accounting overrides.

- **PR [#&#8203;2739](https://github.com/nesquena/hermes-webui/issues/2739)** by [@&#8203;ai-ag2026](https://github.com/ai-ag2026) — Clarify `Response interrupted` recovery markers so they report that the live response stream stopped instead of asserting that the WebUI process restarted. The recovery path now records distinct interruption causes for real process restarts, stream/run split-brain, and lost worker bookkeeping; browser-side SSE transport failures show a separate `Connection interrupted` message, client-side `BrokenPipeError` disconnects no longer get logged as server 500s, and chat/gateway SSE errors emit rate-limited (30 events / 60s / 4KB body cap), sanitized client diagnostics to `/api/client-events/log` for future root-cause checks. The stream-status `terminal_state` value for lost-worker bookkeeping changes from `stale-from-restart` to `lost-worker-bookkeeping`, matching the new non-restart wording.

##### Notes

- **6,532 pytest passed** sequentially before Opus pass + locale parity fix; full re-run pending after Opus SHOULD-FIX patches.
- **Opus Advisor verdict: SHIP-WITH-SHOULD-FIXES applied.** Zero MUST-FIX. Four SHOULD-FIX items patched inline before tag:
  - `/api/auth/status` now gates `passkeys_enabled` / `passwordless_enabled` on the feature flag (fixes broken-affordance trap where passkey login button could show but endpoints returned 404)
  - Settings → System Passkeys block now starts `display:none` and only reveals when the server confirms the flag is on AND credentials are accessible
  - `/api/settings/save` refuses to set passwordless mode when the passkey feature flag is off (closes the auth-bypass path: user goes passwordless while flag on → admin unsets flag → restart serves WebUI fully unauthenticated)
  - CHANGELOG entries added for PR [#&#8203;2685](https://github.com/nesquena/hermes-webui/issues/2685) and PR [#&#8203;2824](https://github.com/nesquena/hermes-webui/issues/2824) (both originally missing despite functional code changes)
- Deferred to follow-up: per-turn cumulative live-tool-prompt token cap ([#&#8203;2685](https://github.com/nesquena/hermes-webui/issues/2685) only added per-call cap; aggregate across many tool calls is a separate refactor).
- **i18n parity**: 7 new shutdown-affordance keys added across all 11 non-en locales (it, ja, ru, es, de, zh, zh-Hant, pt, ko, fr, tr) so locale parity tests pass on first run.

### [`v0.51.131`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051131--2026-05-24--Release-DC-stage-batch13--6-PR-notes-drawer--context-parity--PWA-swipe--locale-polish)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.130...v0.51.131)

##### Added

- **PR [#&#8203;2868](https://github.com/nesquena/hermes-webui/issues/2868)** by [@&#8203;AJV20](https://github.com/AJV20) — Installed/mobile PWA sessions now support an edge swipe from the left side of the screen to open the mobile sidebar drawer, while preserving the existing hamburger and overlay controls. PWA-standalone-gated, edge X<28px, vertical-tolerance 48px, interactive-target exclusion. Defends against accidental triggers from text selection or button taps.

- **PR [#&#8203;2527](https://github.com/nesquena/hermes-webui/issues/2527)** by [@&#8203;AJV20](https://github.com/AJV20) — Default-off, read-only Third-party notes drawer in the Memory panel. Lists configured note/knowledge MCP sources (Joplin, Obsidian, Notion, llm-wiki) when explicitly enabled via `webui_external_notes_sources` config or `HERMES_WEBUI_EXTERNAL_NOTES_SOURCES=1`. Automatic session recall unchanged. 4 API endpoints (`/api/notes/sources`, `/api/notes/search`, `/api/notes/item`, plus `external_notes_enabled` in memory\_read response) all gated behind the feature flag.

- **PR [#&#8203;2547](https://github.com/nesquena/hermes-webui/issues/2547)** by [@&#8203;AJV20](https://github.com/AJV20) — SSE stream runtime diagnostics in deep health checks: active stream count, subscriber totals, and offline buffered-event counts for stuck or slow WebUI chat investigations. Non-sensitive payload only.

- **PR [#&#8203;2547](https://github.com/nesquena/hermes-webui/issues/2547)** by [@&#8203;AJV20](https://github.com/AJV20) — WebUI session prefill parity for bounded JSON files. Browser-originated chat turns can load configured prefill context from `prefill_messages_file`, pass it to Hermes Agent as ephemeral model context, and surface a compact context status event in the chat UI without exposing prefill message bodies. WebUI intentionally does not execute `prefill_messages_script`; executable recall should go through the existing MCP/tool surface. Backward-compatible: degrades gracefully on older agent builds that don't support the `prefill_messages` kwarg.

##### Changed

- **PR [#&#8203;2547](https://github.com/nesquena/hermes-webui/issues/2547)** by [@&#8203;AJV20](https://github.com/AJV20) — Browser-surface session context is now attached to WebUI agent turns so the agent can distinguish a WebUI chat from messaging-platform transcripts. Context is ephemeral (not saved to history). WebUI progress guidance now preserves the normal Hermes messaging style instead of encouraging extra browser-only status chatter.

##### Fixed

- **PR [#&#8203;2865](https://github.com/nesquena/hermes-webui/issues/2865)** by [@&#8203;AJV20](https://github.com/AJV20) — New WebUI sessions no longer persist `display.personality` into per-session `Session.personality`; only explicit personality changes remain durable, preventing stale global display defaults from overriding profile-scoped session behavior. Closes [#&#8203;2845](https://github.com/nesquena/hermes-webui/issues/2845).

- **PR [#&#8203;2882](https://github.com/nesquena/hermes-webui/issues/2882)** by [@&#8203;ycj](https://github.com/ycj) — zh-CN (Simplified Chinese) session-time relative labels are now clearer: `${n}分钟前`, `${n}小时前`, `${n}天前`, and the more natural last-week phrasing `上周` instead of the previous bare-unit shorthand. Also corrects a small indentation glitch in the zh-TW (Traditional Chinese) locale. (Cherry-picked onto fresh stage with `Co-authored-by` attribution — original PR was based on stale master.)

- **PR [#&#8203;2873](https://github.com/nesquena/hermes-webui/issues/2873)** by [@&#8203;Charanis](https://github.com/Charanis) — The WebUI launcher (`ctl.sh` + `bootstrap.py`) now preserves environment variables that have already been resolved by the shell (for example `HERMES_WEBUI_PORT`, `HERMES_WEBUI_STATE_DIR`, `HERMES_WEBUI_HOST`) instead of letting a repo-level `.env` clobber them mid-launch. The `.env` keeps working as a default-only source for unset variables, gated by `HERMES_WEBUI_PRESERVE_ENV=1` set by the launcher subshell.

##### Notes

- **6,503 pytest passed** (sequential mode; the test infrastructure uses a single test server that doesn't support xdist parallelism — known limitation, tracked separately).
- **Opus Advisor verdict: SHIP-AS-IS.** Zero MUST-FIX. Three SHOULD-FIX items filed as follow-up issues (incomplete locale coverage for notes-drawer i18n keys, `_joplin_api_get` URL-token defense-in-depth, prefill `setattr` cache-reuse safety net).
- **[#&#8203;2527](https://github.com/nesquena/hermes-webui/issues/2527) i18n coverage**: 10 of the 11 non-en locales currently ship the English string `'Third-party notes'` for the drawer header. Since the drawer is default-off, user impact is zero today; follow-up issue tracks proper translations before any default-on transition.

### [`v0.51.130`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051130--2026-05-24--Release-DB-stage-batch12--3-PR-profile-isolation--boot-precedence--workspace-Artifacts-tab)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.129...v0.51.130)

##### Fixed

- **PR [#&#8203;2827](https://github.com/nesquena/hermes-webui/issues/2827)** by [@&#8203;Koraji95-coder](https://github.com/Koraji95-coder) — Profile state-sync TLS-vs-thread fix (closes [#&#8203;2762](https://github.com/nesquena/hermes-webui/issues/2762)). When switching profiles via the WebUI cookie selector, session token-usage and title were being written to the *previously-active* profile's `state.db` instead of the cookie-switched one (sidecar messages + workspace files were already routed correctly; only the `state.db` sidecar sync leaked). Root cause: the cookie middleware sets `_tls.profile = '<cookie>'` on the HTTP request thread, but the daemon thread spawned in `_run_agent_streaming` doesn't inherit that TLS. When the streaming worker calls `sync_session_usage`, `_get_state_db → get_active_hermes_home → get_active_profile_name` finds no TLS profile, falls through to `_active_profile` (the process default), and opens the wrong DB. Fix plumbs the session's own `profile` field through `sync_session_usage(..., profile=...)` and `_get_state_db(profile=...)` rather than leaning on TLS that doesn't exist on the worker thread. Keeps the existing TLS path for callers that don't pass `profile=` explicitly, so external integrations don't regress. Also adds defensive `_validate_profile_name` rejecting `../etc`, leading-dash, whitespace, and over-long names (prevents path traversal via cookie tampering). Adds 11 regression tests covering explicit-profile honors, multi-thread profile preservation, unknown-profile-name fallback path, invalid-name refusal, and legacy-call-shape compatibility.

- **PR [#&#8203;2726](https://github.com/nesquena/hermes-webui/issues/2726)** by [@&#8203;starship-s](https://github.com/starship-s) — Boot model-default precedence follow-up (refines [#&#8203;2709](https://github.com/nesquena/hermes-webui/issues/2709)). The original v0.51.105 fix correctly preferred the profile/server default on fresh boot, but the implementation had two over-broad side effects flagged in post-merge review: (1) boot unconditionally cleared the persisted browser model state, even on restored sessions where that state should remain authoritative, and (2) `populateModelDropdown()` reapplied the default on every repopulate when no session model was present, which clobbered the in-page selection during ordinary dropdown refreshes. Fix is to gate the default-reapply behind an opt-in `{preferProfileDefaultOnFreshBoot: true}` parameter so boot keeps profile-default precedence, restored sessions keep their session model, and non-boot dropdown refreshes preserve the loaded session's model or the current in-page selection. Browser model state is no longer deleted just because the profile default wins this boot. Expanded the regression test coverage with a Node `select` / DOM shim that exercises the real `populateModelDropdown()` path for boot-default, restored-session, current-selection, and removed-model scenarios (+306 LOC tests).

##### Added

- **PR [#&#8203;2673](https://github.com/nesquena/hermes-webui/issues/2673)** by [@&#8203;AJV20](https://github.com/AJV20) — Workspace Artifacts tab (closes [#&#8203;2655](https://github.com/nesquena/hermes-webui/issues/2655)). New tab in the workspace panel that lists likely files mentioned, edited, or created during the active session. Prioritizes structured tool-call paths (file\_write, edit, patch, etc.), filters dependency/build noise (node\_modules, `__pycache__`, `.git`, lock files), and refreshes while live tool calls arrive. Artifact entries open through the existing workspace file preview flow. The MVP is frontend-scoped — backend ingestion uses the existing tool-call event stream rather than a new persistence path — so the maintainer can evaluate the UX before deciding whether artifact tracking should grow into a backend-backed feature. Refreshes alongside the file tree in `loadDir()` via a `typeof renderSessionArtifacts==='function'` guard so it composes cleanly with [#&#8203;2716](https://github.com/nesquena/hermes-webui/issues/2716)'s session stale-guard pattern. Adds `tests/test_issue2655_frontend.py`.

##### Notes

- **In-stage cherry-pick mechanics**: All 3 PRs were on stale-base merge-bases (master had advanced through 3 releases). Used `git apply --3way` of each PR's net delta vs its merge-base onto current stage HEAD, then resolved 2 small JS conflicts manually:
  - `static/boot.js` ([#&#8203;2726](https://github.com/nesquena/hermes-webui/issues/2726) vs [post-#&#8203;2716](https://github.com/post-/hermes-webui/issues/2716) master): kept PR's parameterized `populateModelDropdown({preferProfileDefaultOnFreshBoot:true})` call (the whole point of [#&#8203;2726](https://github.com/nesquena/hermes-webui/issues/2726)) on top of master's [#&#8203;2716](https://github.com/nesquena/hermes-webui/issues/2716) hydration flow.
  - `static/workspace.js` ([#&#8203;2673](https://github.com/nesquena/hermes-webui/issues/2673) vs [post-#&#8203;2716](https://github.com/post-/hermes-webui/issues/2716) master): kept master's `sessionId`-capture stale-session guard (closure-scoped sessionId check after `await`) AND added PR's `renderSessionArtifacts()` call to refresh the new Artifacts tab when the file tree updates. Wrapped in `typeof === 'function'` guard for defense-in-depth.
- **In-stage test fixes**: Patched 3 brittle source-string assertions to accept both [pre-#&#8203;2716](https://github.com/pre-/hermes-webui/issues/2716) and [post-#&#8203;2716](https://github.com/post-/hermes-webui/issues/2716) JS shapes (variable names changed during the cherry-pick, semantics preserved). Patched 1 schema mismatch in `tests/test_issue2762_state_sync_profile_kwarg.py::_read_session` helper — it queried `sessions.session_id` but the real `state.db` schema has `sessions.id` as primary key. Fix is mechanical: `SELECT id AS session_id` + `WHERE id = ?` so the helper queries the actual schema.
- Full pytest: pending re-run on this finalized stage. Touched-tests gate: 41 passed (covering [#&#8203;2827](https://github.com/nesquena/hermes-webui/issues/2827) + [#&#8203;2726](https://github.com/nesquena/hermes-webui/issues/2726) + [#&#8203;2673](https://github.com/nesquena/hermes-webui/issues/2673) surface areas).
- Agent self-verified: profile= kwarg threading on `_get_state_db` + `sync_session_usage`, production call site in `api/streaming.py:5078` passes `profile=getattr(s, 'profile', None)`, `populateModelDropdown` opt-in parameterization present, boot.js calls with `preferProfileDefaultOnFreshBoot:true`, workspace `renderSessionArtifacts()` defined + called.

### [`v0.51.129`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051129--2026-05-24--Release-DA-stage-batch11--4-PR-feature--perf-batch)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.128...v0.51.129)

##### Performance

- **PR [#&#8203;2836](https://github.com/nesquena/hermes-webui/issues/2836)** by [@&#8203;v2psv](https://github.com/v2psv) — HTTP/1.1 keep-alive for WebUI responses. Bumps `Handler.protocol_version` from the HTTP/1.0 default to `HTTP/1.1` so browsers can reuse TCP connections across normal API and static-file requests. Adds explicit `Content-Length` headers to hand-written responses that weren't already using shared `j()` / `t()` helpers. Adds `Content-Length: 0` to empty redirect / range-error responses. Switches SSE-style streaming endpoints from `Connection: keep-alive` to `Connection: close` (keep-alive is only safe when the response body is framed; SSE bodies have no fixed length). Significant first-paint / session-open improvements on high-RTT / VPN / proxied paths — author reports \~47% faster first paint and \~30-40% improvements on panel-load flows on a typical remote-host setup.

  **Opus pre-release advisor caught one missing framing site** in the on-the-fly folder ZIP download path (`/api/folder/download`): the body has no known length, doesn't use chunked encoding, and was relying on HTTP/1.0 connection-close-equals-EOF. Under HTTP/1.1 this would have left clients hanging waiting for the next response after the central-directory bytes finished. Patched inline before tag: add `Connection: close` header to mirror the SSE-endpoint pattern. Opus verified this was the ONLY remaining streaming response in the codebase that needed the header — all 12 hand-written response paths + 8 SSE streams + j()/t() helpers + auth flow were already correctly framed by the PR.

##### Added

- **PR [#&#8203;2680](https://github.com/nesquena/hermes-webui/issues/2680)** by [@&#8203;mccxj](https://github.com/mccxj) — Auxiliary Models settings card in Settings → Preferences. Lets users configure per-task model routing for 9 canonical side-task slots: vision, web extract, compression, session search, approval, MCP tool reasoning, title generation, skills hub, curator. Each slot exposes a provider dropdown + model dropdown plus an "auto (use main model)" / "auto (use provider default)" pair so users can keep aux routing implicit when they don't care. New endpoints: `GET /api/model/auxiliary` returns current assignments; `POST /api/model/set` writes assignments (`scope=auxiliary` for aux slots, `scope=main` for the default chat model) and supports `task="__reset__"` to reset all slots back to auto. 16 new i18n keys added across all 12 locales (en, it, ja, ru, es, de, zh, zh-Hant, pt, ko, fr, tr — Turkish translations added in-stage to cover the sibling-PR collision with v0.51.127's Turkish locale baseline). 24 source-level test assertions covering HTML structure, JS logic, i18n parity, and route registration.

- **PR [#&#8203;2842](https://github.com/nesquena/hermes-webui/issues/2842)** by [@&#8203;AJV20](https://github.com/AJV20) — PWA polish for installed launches. New `static/pwa-startup.js` is loaded synchronously in `<head>` before the main UI bundle, so the page knows whether it's running standalone / in-browser / on iOS / offline before first paint. Marks `pwa-standalone`, `pwa-browser`, `pwa-ios`, `pwa-offline`, and short-lived `pwa-resumed` classes on `<html>`. Exposes `window.HermesPWA.{isStandalone, syncMode, launchAction, promptInstall}` helpers and captures `beforeinstallprompt` / `appinstalled` early enough that any future install-prompt UI can chain off them. Manifest gains app identity / scope / `display_override` (`window-controls-overlay` → `standalone` → `minimal-ui`) and a "New conversation" PWA App Shortcut. Service worker pre-caches the startup helper, switches navigation and shell-asset fetches to `cache: 'no-store'` before falling back to CacheStorage. Boot path wires `?source=pwa&action=new-chat` to start a fresh chat instead of reopening the last saved session. The viewport meta now sets `maximum-scale=1, user-scalable=no` for native-feel — acknowledged trade-off against WCAG 2.1 1.4.4 (Resize text), intentionally kept for the PWA-installed feel of this user base.

- **PR [#&#8203;2794](https://github.com/nesquena/hermes-webui/issues/2794)** by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) — Runtime adapter route selection harness. Routes explicit adapter-mode chat starts through `build_runtime_adapter(...)` and keeps `legacy-direct` as the default `/api/chat/start` path. Continues the [#&#8203;1925](https://github.com/nesquena/hermes-webui/issues/1925) RFC slice progression: this is slice 4e, the default-off chat-start route-selection seam. Returns a bounded `501 Not Configured` response when `runner-local` is explicitly selected before a supervised runner client exists, instead of silently starting a legacy WebUI-owned run. New `_chat_start_response_from_run_start(...)` helper whitelists legacy-compatible chat-start response fields and keeps adapter-internal `run_id`, `status`, and `active_controls` out of public responses. Updates `docs/rfcs/hermes-run-adapter-contract.md` to mark [#&#8203;2744](https://github.com/nesquena/hermes-webui/issues/2744) shipped and define slice 4e.

##### Notes

- Full pytest: **6,467 passed / 6 skipped / 3 xpassed / 8 subtests passed**.
- Opus pre-release advisor reviewed all 7 risk areas (HTTP framing surface completeness, PWA startup ordering, sibling-PR `api/routes.py` interaction, service worker cache invalidation, viewport-meta trade-off, runtime adapter response shape, locale-counter brittleness). Verdict: **1 MUST-FIX patched inline** (folder ZIP `Connection: close` header), **0 inline SHOULD-FIX**, 1 follow-up suggested (`set_auxiliary_model` could validate `task` against `AUX_TASK_SLOTS` whitelist — auth-gated, low severity, filing as follow-up).
- Agent self-verified: protocol\_version bumped, SSE Connection-close + Content-Length plumbing, Auxiliary Models API surface (config + endpoints + frontend), PWA helpers + manifest shortcuts + display\_override, Runtime adapter wiring + whitelisting, i18n parity for all 12 locales on the 16 new aux keys.
- Browser-verified at 1920×1080: Auxiliary Models card renders correctly under Settings → Preferences, 9 task slots with provider/model dropdowns, "Reset all to auto" button, layout consistent with surrounding Settings cards, no clutter or clipping. PWA classes populate on `<html>` and HermesPWA namespace populates with 4 helpers as expected.
- In-stage commits added Turkish translations for [#&#8203;2680](https://github.com/nesquena/hermes-webui/issues/2680)'s 16 `settings_aux_*` / `settings_label_auxiliary_models` / `settings_desc_auxiliary_models` keys to close the sibling-collision gap with v0.51.127's Turkish locale ([#&#8203;2772](https://github.com/nesquena/hermes-webui/issues/2772)). Bumped `test_auxiliary_models_settings.py::test_all_locales_have_auxiliary_keys` from `count == 11` to `count == 12` (the locale set grew when Turkish landed).

### [`v0.51.128`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051128--2026-05-24--Release-CZ-stage-batch10--2-PR-perf--correctness-batch)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.127...v0.51.128)

##### Fixed

- **PR [#&#8203;2830](https://github.com/nesquena/hermes-webui/issues/2830)** by [@&#8203;franksong2702](https://github.com/franksong2702) — Pin state synchronization between persisted index and in-memory sessions (closes [#&#8203;2821](https://github.com/nesquena/hermes-webui/issues/2821)). Three coupled bugs:

  - **Bug A (load-bearing):** `/api/session/pin` pre-snapshot used `getattr(session, "pinned", False)` which always returned `False` for dict-backed index rows from `all_sessions()`. With \~55-session profiles and LRU eviction churn, pinned counts routinely under-counted because the persisted snapshot was effectively empty. New `_session_field(session, field, default)` helper resolves both dict-backed and Session-object snapshots correctly.
  - **Bug B:** Removed stale client-side `pinLimitReached` short-circuit in the sidebar action menu that could block pin clicks before the server saw them, based on `_allSessions` data that was stale mid-render. Server now enforces the cap; the toast surfaces the 400 response.
  - **Bug C recovery:** Pin/unpin failure path (4xx response from `/api/session/pin`) now triggers `renderSessionList()` to refresh `_allSessions` from the server, so the sidebar never gets stuck on stale optimistic state.

  Adds `tests/test_issue2821_session_pin_state_sync.py` (70 LOC) covering the `_session_field` helper, the persisted-pinned snapshot, the removed `pinLimitReached` reference, and the failure-catch refresh path. Companion fix to [#&#8203;2782](https://github.com/nesquena/hermes-webui/issues/2782) (server-side 404→200 transition for missing CLI-synced sessions) which remains out of scope.

##### Performance

- **PR [#&#8203;2716](https://github.com/nesquena/hermes-webui/issues/2716)** by [@&#8203;dobby-d-elf](https://github.com/dobby-d-elf) — Six independent perf nudges plus one correctness fix. nesquena-APPROVED on 2026-05-22 after a deep-review iteration; cherry-picked onto post-v0.51.127 master via 3-way apply with sibling-PR composition resolution.

  - **Metadata-only `/api/session` correctness fix.** Refactors the prior inline reconciliation into `_metadata_only_message_summary(sid, profile=None)` helper that runs the full `merge_session_messages_append_only()` path. Pre-fix shortcut could over-count stale state.db replay rows that the merge intentionally filters out, producing false "transcript newer than loaded conversation" signals (same bug class as [#&#8203;2705](https://github.com/nesquena/hermes-webui/issues/2705) / [#&#8203;2686](https://github.com/nesquena/hermes-webui/issues/2686)). The new helper threads `profile=` through to `get_state_db_session_messages` to preserve [#&#8203;2827](https://github.com/nesquena/hermes-webui/issues/2827)'s TLS-vs-thread profile fix on background-thread reads.
  - **Batched persisted-session checks in sidebar indexing.** One `SESSION_DIR.glob('*.json')` snapshot per call replaces per-row `_index_entry_exists()` filesystem lookups during `all_sessions()` pruning. Fallback to the per-row helper preserved when the glob raises.
  - **Deferred render-cache signature.** `cachedRenderSignature` closes over the lookup-time signature so the cache STORE path reuses it without recomputing. `_messageRenderCacheSignature()` continues to include the content hash per [#&#8203;2692](https://github.com/nesquena/hermes-webui/issues/2692), preserving the cache-invalidation invariant.
  - **Hoisted assistant tool-activity index.** Footer-rendering loop now uses an `O(1)` Set lookup instead of `S.toolCalls.some(...)` per message — \~30× fewer comparisons for a 100-message conversation with 30 tool calls.
  - **Workspace stale-session guards.** `loadDir` and `_refreshGitBadge` in `static/workspace.js` capture `sessionId` at call time and check it after each `await` (including the catch path of `_refreshGitBadge` — without it, a late 404 from the previous session would hide the git badge on the current session).
  - **Background model-catalog prime.** `_startBootModelDropdown` fires fire-and-forget on boot via `setTimeout(0)` so the live catalog hydrates without blocking. The existing `await` on the saved-session restore path is preserved (re-applies the saved session's model after hydration so the chip never shows the stale static default).
  - **Failed hydration retryable.** `window._modelDropdownReady = null; throw e;` lets the next caller refetch instead of being stuck on a permanent failure.

  Adds 76 LOC of new tests across `test_session_metadata_fast_path.py`, `test_webui_state_db_reconciliation.py`, `test_session_index.py`, `test_issue1539_provider_removal_dropdown_invalidation.py`, `test_issue1785_workspace_preview_breadcrumb.py`, `test_parallel_session_switch.py`.

##### Notes

- PR [#&#8203;2716](https://github.com/nesquena/hermes-webui/issues/2716) had been pending merge since 2026-05-22 due to a rebase blocker against the rapidly-advancing master (10+ intervening releases). Cherry-picked via `git apply --3way` of the PR's net delta vs its original merge-base (`f9302601`); 12 of 14 files applied cleanly. Two files had genuine conflicts requiring resolution: `api/routes.py` (took the PR's helper extraction AND added `profile=` threading to preserve [#&#8203;2827](https://github.com/nesquena/hermes-webui/issues/2827)'s fix), and `tests/test_webui_state_db_reconciliation.py` (kept BOTH master's pre-existing `test_api_session_reload_drops_stale_cached_user_tail_after_saved_assistant` AND the PR's new `test_metadata_fast_path_matches_reconciliation_for_restamped_replays` — they pin different invariants).
- Opus pre-release advisor reviewed all 6 risk areas (helper extraction correctness, sibling-PR composition, `Session.load` profile-safety, test coverage, deferred Bug D, stale-line-number cleanup nit). Verdict: **SHIP AS-IS** — no MUST-FIX, no inline SHOULD-FIX. Two follow-up issues to file post-tag (Bug D startup index rebuild perf; multi-profile state.db test for the `profile=` threading invariant).
- Full pytest: **6,434 passed / 6 skipped / 3 xpassed / 8 subtests passed** in 2m43s.
- Agent self-verified the producer→consumer channel for `_metadata_only_message_summary` with unmocked invocation against a real session-load path (per skill rule Trigger A + E for mocked-consumer test patterns).
- Closes: [#&#8203;2821](https://github.com/nesquena/hermes-webui/issues/2821) (pin state sync), and `get_state_db_session_summary` dead-code removed ([#&#8203;2716](https://github.com/nesquena/hermes-webui/issues/2716)).

### [`v0.51.127`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051127--2026-05-24--Release-CY-stage-batch9--7-PR-low-risk-batch--brick-class-Linux--brick-class-update-apply--composer-wide-screen--Turkish-locale--MCP-toggle--SSE-settlement--Windows-CI)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.126...v0.51.127)

##### Fixed

- **PR [#&#8203;2854](https://github.com/nesquena/hermes-webui/issues/2854)** by [@&#8203;nesquena-hermes](https://github.com/nesquena-hermes) — Embedded terminal opens then immediately closes with `[terminal closed]` on every Linux install past `71d8a8fb`. Root cause: `_terminal_shell_preexec_fn` set `PR_SET_PDEATHSIG=SIGTERM` on the PTY shell so orphans would die when WebUI crashed, but `PR_SET_PDEATHSIG` is **per-thread**, not per-process. WebUI uses `ThreadingHTTPServer`, so each HTTP request runs in its own short-lived worker thread; when the request handler returns and the worker thread exits, the kernel sees the pdeathsig-parent thread has died and SIGTERMs the PTY shell within \~10ms. macOS users were unaffected because `libc.prctl` doesn't exist there. Fix: drop the `preexec_fn` entirely; rely on `atexit.register(close_all_terminals)` for graceful shutdown and explicit `close_terminal` for user-driven close. Adds `tests/test_terminal_process_cleanup.py::test_pty_shell_survives_when_spawning_thread_exits` (real PTY shell spawned via worker thread, asserts shell alive after 500ms grace) plus static-check that `preexec_fn` cannot be re-introduced. Closes [#&#8203;2853](https://github.com/nesquena/hermes-webui/issues/2853).

- **PR [#&#8203;2855](https://github.com/nesquena/hermes-webui/issues/2855)** by [@&#8203;nesquena-hermes](https://github.com/nesquena-hermes) — "Update Now" loops for every user past the latest tag ([#&#8203;2846](https://github.com/nesquena/hermes-webui/issues/2846)). After [#&#8203;2758](https://github.com/nesquena/hermes-webui/issues/2758) the update check correctly fell through to branch comparison when `HEAD` had moved past the latest `v*` tag, but `_select_apply_compare_ref` still returned `tags[0]` — so `git pull --ff-only v2026.5.16` no-op'd, the server bounced, and the banner reappeared unchanged. `apply_force_update` had the same bug except worse (would `git reset --hard v2026.5.16` and rewind the checkout 254 commits). Fix: extract `_head_is_past_latest_tag(path, current_tag)` and have both check and apply paths consult it. Opus pre-release review caught a "case D" parameter-asymmetry drift (HEAD on older tag + commits + newer tag exists → predicate flipped between the two callsites) and patched the apply-side predicate to use `current_tag` + a `behind == 0` gate, exactly mirroring the check-side rule. Adds `test_select_apply_compare_ref_case_d_older_tag_with_commits_and_newer_tag_exists`. Closes [#&#8203;2846](https://github.com/nesquena/hermes-webui/issues/2846).

- **PR [#&#8203;2852](https://github.com/nesquena/hermes-webui/issues/2852)** by [@&#8203;ai-ag2026](https://github.com/ai-ag2026) — Chat `stream_end` handler now settles from the persisted session when `done` was not received or replayed, instead of leaving the active pane with live `Thinking` / assistant DOM and inflight state projected indefinitely. Reconnect / journal / replay paths can deliver `stream_end` without preceding `done`; the prior code treated `stream_end` as transport-only close. Duplicate / replayed `done` events are also made idempotent before completion sound / final render side effects. Opus pre-release review added a post-await race guard inside `_restoreSettledSession` to catch the case where a late `done` event runs the finalize path while the settlement is awaiting the `/api/session` roundtrip. Adds 4 new regression tests across `tests/test_1694_terminal_cleanup_ownership.py` covering both `stream_end`-without-`done` and duplicate-`done` paths.

- **PR [#&#8203;2811](https://github.com/nesquena/hermes-webui/issues/2811)** by [@&#8203;Koraji95-coder](https://github.com/Koraji95-coder) — Native-Windows startup E2E workflow now self-tests on PR push (closes the [post-#&#8203;2783](https://github.com/post-/hermes-webui/issues/2783) gap where Windows-only regressions like the WOW64 ProgramFiles redirect could only be caught after release). Reworked per maintainer feedback to use a stub `hermes_cli/__init__.py` next to a sibling `hermes-agent/` folder rather than `pip install hermes-agent` (which is not on PyPI). Workflow runs `start.ps1` for 8s and asserts none of its `Write-Error` guards fired (no Python, no agent dir, bad port, missing `hermes_cli`, missing `server.py`). PowerShell syntax + path discovery is the testable surface; the server can't actually boot on a stub. `taskkill` exit-128 swallowed when the stub process is already gone.

##### Changed

- **PR [#&#8203;2812](https://github.com/nesquena/hermes-webui/issues/2812)** by [@&#8203;Koraji95-coder](https://github.com/Koraji95-coder) — Composer max-width is now responsive on wide displays. Pre-change `.composer-box` had a fixed `max-width: 780px` that pinched footer chips (workspace name, model picker, reasoning chip, context ring) against each other on 1440p+ monitors. Switched to `max-width: clamp(780px, 60vw, 1100px)` — the 780px floor preserves byte-identical layout at 1280px (Aron's laptop reference width); 1440px viewports gain \~84px (864px composer); 1920px viewports gain \~320px (1100px composer cap). Mobile responsive logic untouched. Single-line CSS change in `static/style.css`.

##### Added

- **PR [#&#8203;2772](https://github.com/nesquena/hermes-webui/issues/2772)** by [@&#8203;vaur94](https://github.com/vaur94) — Complete Turkish (`tr`) locale across `static/i18n.js` (\~1,182 keys matching existing locale coverage). Adds Turkish login page strings in `api/routes.py` `_LOGIN_LOCALE`. Settings → Language now offers **Türkçe**; speech recognition uses `tr-TR`. Stage build absorbed a sibling-PR i18n collision with [#&#8203;2776](https://github.com/nesquena/hermes-webui/issues/2776) below (9 missing keys: `mcp_enable_server`, `mcp_disable_server`, `mcp_enabled_toast`, `mcp_disabled_toast`, `mcp_toggle_failed`, `open_in_vscode`, `open_in_vscode_failed`, `settings_label_ignore_agent_updates`, `settings_desc_ignore_agent_updates`) — Turkish translations added in-stage so locale-parity test passes. Closes [#&#8203;2537](https://github.com/nesquena/hermes-webui/issues/2537) as superseded (byzuzayli's earlier Turkish PR with narrower scope).

- **PR [#&#8203;2776](https://github.com/nesquena/hermes-webui/issues/2776)** by [@&#8203;roryford](https://github.com/roryford) — New `PATCH /api/mcp/servers/{name}` endpoint accepts `{"enabled": bool}`, writes `mcp_servers.<name>.enabled` to `config.yaml`, calls `reload_config()`, returns `{"ok": true, "name": "<name>", "enabled": <bool>}`. Each MCP server row in the panel now shows a clickable Enabled/Disabled toggle. Also fixes a pre-existing bug: `_handle_mcp_server_delete` and `_handle_mcp_server_update` were defined at line \~11656 but never wired into the HTTP router — DELETE wired into `handle_delete`, PUT wired via new `handle_put` / `do_PUT` in `server.py`. CORS preflight `Access-Control-Allow-Methods` updated to include `PUT` (Opus pre-release review nit). Adds 5 i18n keys to all 11 locales (en, it, ja, ru, es, de, zh, zh-Hant, pt, ko, fr, tr via in-stage parity fix). 7 new tests covering enable, disable, 404, empty-name, missing-field, response payload, URL-decoded names.

##### Notes

- Two PRs ([#&#8203;2854](https://github.com/nesquena/hermes-webui/issues/2854), [#&#8203;2855](https://github.com/nesquena/hermes-webui/issues/2855)) are brick-class fixes — every Linux install was unable to use the embedded terminal, and every install past the latest agent tag was stuck in an Update Now loop. They land in the same low-risk batch as cosmetic / locale / CI changes because both fixes are mechanical, well-tested, and the brick-class severity made deferring impossible.
- Opus pre-release advisor reviewed all 5 risk areas (PR\_SET\_PDEATHSIG removal, update apply path symmetry, MCP toggle wiring, composer clamp, stream\_end settlement). 1 MUST-FIX + 3 SHOULD-FIX all addressed inline before tag. Net: +69/-9 across 5 files for the Opus fixes.
- Full pytest: 6,424 passed / 6 skipped / 3 xpassed / 8 subtests passed.
- UX evidence for [#&#8203;2812](https://github.com/nesquena/hermes-webui/issues/2812) captured at 1280/1440/1920/mobile (iPhone 14 emulation); Telegram-approved.
- File a follow-up issue for pdeathsig-on-supervisor-thread hardening ([#&#8203;2854](https://github.com/nesquena/hermes-webui/issues/2854) deferred Option B) and French-locale `open_in_vscode` parity gap (predates this batch, Opus advisor flagged).

### [`v0.51.126`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051126--2026-05-24--Release-CX-stage-batch8--2-PR-low-risk-batch--kanban-markdown--live-activity-timeline)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.125...v0.51.126)

##### Added

- **PR [#&#8203;2819](https://github.com/nesquena/hermes-webui/issues/2819)** by [@&#8203;humayunak](https://github.com/humayunak) — Kanban task descriptions and comments now render as full GFM Markdown instead of plain-text. `_kanbanRenderMarkdown()` in `static/panels.js` rewrote the line-per-`<p>` wrapper as a block-parsing pipeline supporting headings, code blocks (fenced + indented), ordered/unordered lists, task lists with checkboxes, tables, blockquotes, horizontal rules, and strikethrough. `_kanbanRenderMarkdownInline()` gains `~~strikethrough~~` and tightens the italic regex to avoid mid-identifier `*` matches. CSS adds table borders, code-block background, checkbox styling, blockquote accent, and heading sizing scoped to `.hermes-kanban-md`. Frontend-only, scoped to the kanban panel. 95 existing kanban tests pass.

##### Changed

- **PR [#&#8203;2847](https://github.com/nesquena/hermes-webui/issues/2847)** by [@&#8203;AJV20](https://github.com/AJV20) — Live chat Activity disclosure now shows observable run telemetry instead of an empty `Thinking…` placeholder when no reasoning text is available (squashed from 2 author commits). New baseline rows surface run-start metadata (model, profile), `Waiting on model` / `Waiting on tool result` / `Working for …` status, tool start/finish in the timeline alongside the existing compact tool cards, and a `No recent activity for …` state after quiet periods. Frontend-only telemetry derived from existing stream events — no new backend event types. Adds `tests/test_live_activity_timeline.py` (4 tests). The compact/calm default Activity disclosure is preserved; it only becomes informative when expanded.

### [`v0.51.125`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051125--2026-05-24--Release-CW-stage-batch7--10-PR-low-risk-batch--UIUX-polish--bug-fixes--diagnostics)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.124...v0.51.125)

##### Fixed

- **PR [#&#8203;2839](https://github.com/nesquena/hermes-webui/issues/2839)** by [@&#8203;tn801534](https://github.com/tn801534) — Kanban worker log endpoint constructed URLs with a double query string (`?board=<slug>?tail=65536`) when a non-default board was active. The frontend was appending `?tail=65536` directly to a URL that already had `?board=...` from `_kanbanBoardQuery()`. Fix: pass `{tail: 65536}` as the `extra` argument to `_kanbanBoardQuery()` so it composes both params into a single valid query string. One-line, narrow scope.

- **PR [#&#8203;2832](https://github.com/nesquena/hermes-webui/issues/2832)** by [@&#8203;franksong2702](https://github.com/franksong2702) — Malformed HTTP request logging in `server.py` falls back to `"-"` for missing `command` or `path` instead of raising `AttributeError`. Defensive `getattr(self, 'command', None) or '-'` matches the pattern already used for `_req_t0` elsewhere in the handler. Adds `tests/test_issue2775_log_request.py` covering the malformed-request-before-path-assigned case.

- **PR [#&#8203;2818](https://github.com/nesquena/hermes-webui/issues/2818)** by [@&#8203;humayunak](https://github.com/humayunak) — Approval and clarify cards no longer steal focus from the composer textarea (`#msg`) when the user is mid-type. `showApprovalCard()` and `showClarifyCard()` now guard the `focus()` call on `document.activeElement !== $('msg')`, matching the pattern already used elsewhere for focus-sensitive paths. The clarify card also moves the focus call out of `setTimeout` for snappier UX. Silently dropped keystrokes during streaming are eliminated.

- **PR [#&#8203;2826](https://github.com/nesquena/hermes-webui/issues/2826)** by [@&#8203;Koraji95-coder](https://github.com/Koraji95-coder) — Composer footer chip wraps no longer overlap at narrow widths (closes [#&#8203;2740](https://github.com/nesquena/hermes-webui/issues/2740)). The five chip wraps (`.composer-profile-wrap`, `.composer-ws-wrap`, `.composer-model-wrap`, `.composer-reasoning-wrap`, `.composer-toolsets-wrap`) had `flex: 0 1 auto` + `min-width: 0` so they would compress past their content's natural width when the composer narrowed, causing visual overlap of the profile / workspace / model / reasoning chips. Switched to `flex: 0 0 auto` via a single grouped selector. Each chip now keeps its natural width and the existing `overflow-x: auto` on `.composer-left` handles overflow via horizontal scroll. Default-width layout unchanged; only affects the overflow regime. Mobile-specific rules (already `flex: 0 0 auto`) untouched.

- **PR [#&#8203;2829](https://github.com/nesquena/hermes-webui/issues/2829)** by [@&#8203;franksong2702](https://github.com/franksong2702) — Workspace Markdown previews fall back to plain text for very large files (>64 KB or >1500 lines) instead of synchronously running the full rich Markdown renderer on the browser main thread, which could lock up the tab for several seconds on multi-megabyte `.md` files. Plain-text preview shows file size + line count in the status line so users know why rich rendering was bypassed; Edit mode still shows raw content as before. Closes [#&#8203;2823](https://github.com/nesquena/hermes-webui/issues/2823). Supersedes [#&#8203;2828](https://github.com/nesquena/hermes-webui/issues/2828) (same scope, less polished).

- **PR [#&#8203;2837](https://github.com/nesquena/hermes-webui/issues/2837)** by [@&#8203;franksong2702](https://github.com/franksong2702) — CSRF rejections now distinguish origin/proxy mismatches from expired session tokens, so provider-key removal and other protected requests show actionable diagnostics instead of the generic "Cross-origin request rejected" error. Adds `tests/test_issue2572_csrf_diagnostics.py` covering both failure modes.

- **PR [#&#8203;2834](https://github.com/nesquena/hermes-webui/issues/2834)** by [@&#8203;franksong2702](https://github.com/franksong2702) — Workspace Markdown `mailto:` and `tel:` links now render as clickable links, and sandboxed HTML preview links open outside the iframe (via injected `<base target="_blank">`) instead of navigating the preview into a browser-blocked page. Adds `tests/test_issue2768_workspace_links.py`.

- **PR [#&#8203;2838](https://github.com/nesquena/hermes-webui/issues/2838)** by [@&#8203;franksong2702](https://github.com/franksong2702) — Tasks panel surfaces a warning when the Hermes gateway is not configured or not running, so Docker users know scheduled jobs need the gateway daemon to tick while away. The single-container Docker boundary is also clarified in `docs/docker.md`. Adds `tests/test_issue2785_gateway_cron_guidance.py`.

##### Added

- **PR [#&#8203;2820](https://github.com/nesquena/hermes-webui/issues/2820)** by [@&#8203;tangerine-fan](https://github.com/tangerine-fan) — Clarify user choice is now echoed as a visible message in the conversation transcript. After the user responds to a clarify prompt, a synthetic user message with the chosen value is inserted into `S.messages` (marked `_clarify_response: true` so downstream consumers can filter if needed). Previously the choice was only visible in the transient clarify card; now the chat history preserves the decision.

- **PR [#&#8203;2843](https://github.com/nesquena/hermes-webui/issues/2843)** by [@&#8203;AJV20](https://github.com/AJV20) — New Settings preference "Ignore Agent updates" keeps WebUI update notices, banners, and update actions enabled while suppressing Hermes Agent update checks. Default `False` (current behavior). Useful when running an unreleased agent build or pinning to a specific agent commit.

</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/650
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changes-requested Maintainer left detailed feedback requesting changes; PR is waiting on author to address hold ready-for-review Held PR feedback addressed; awaiting maintainer to remove hold

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants