Skip to content

fix(chat): align WebUI context with messaging sessions - #2547

Merged
13 commits merged into
nesquena:masterfrom
AJV20:fix/webui-context-parity
May 24, 2026
Merged

13 commits merged into
nesquena:masterfrom
AJV20:fix/webui-context-parity

Conversation

@AJV20

@AJV20 AJV20 commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds WebUI browser-surface metadata as ephemeral context so the agent can distinguish WebUI sessions from messaging-platform transcripts without persisting that metadata to history.
  • Loads configured prefill_messages_file or prefill_messages_script for WebUI chat turns and passes it to Hermes Agent as ephemeral prefill context, matching the recall behavior operators expect from other Hermes surfaces.
  • Adjusts WebUI progress guidance to preserve the normal Hermes messaging style and avoid extra browser-only status chatter while still allowing concise progress updates for long tool runs.

Test Plan

  • python3 -m py_compile api/streaming.py api/routes.py api/config.py
  • python3 -m pytest tests/test_webui_surface_context.py tests/test_webui_prefill_context.py tests/test_sprint42.py -q (35 passed)

Notes

This is a focused context/tone parity PR for browser-originated chat turns. It keeps prefill message bodies out of the browser-visible context status event and out of saved WebUI transcripts.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Read the PR head (540292a) end to end: api/streaming.py diff at lines 174–330 / 3653–3663 / 4027–4039, static/messages.js:1523-1538 + the run‑journal event list update at 2014, the two new test files, and the agent‑side contracts at agent/agent_init.py:118 / 397, agent/conversation_loop.py:803-814 / 1730-1734, gateway/run.py:2254-2266, hermes_cli/config.py:1239 / 2796. Wiring is clean and the agent side already accepts prefill_messages= at AIAgent construction plus tolerates older builds via the _agent_kwargs.pop('prefill_messages', None) guard at streaming.py:3796 — good defensive layering.

Three points to consider before merge.

1. prefill_messages_script executes on every turn — cache or gate it

_load_webui_prefill_context() is called inside _run_agent_streaming at streaming.py:3655 for every chat turn, and the script branch runs subprocess.run([exe, path], …, timeout=20.0). For a script that reads Joplin notes or hits a remote memory service, that's a 20s‑bounded synchronous call per assistant turn on the streaming worker before the SSE starts producing tokens. Two follow‑ups worth doing in this PR (or a fast follow):

  • Add a small TTL cache keyed on (path, mtime) so a recall.py that's cheap‑idempotent doesn't re‑shell per turn. 5–10s is plenty for typing cadence.
  • Document the perf contract in the prompt block (file vs script) and in CHANGELOG: file = cheap each turn, script = caller pays per‑turn cost.
# api/streaming.py:282-308 — runs unconditionally per turn
if script_raw:
    path = _resolve_prefill_path(script_raw)
    ...
    proc = subprocess.run(cmd, cwd=str(path.parent), env=env, ..., timeout=timeout)

2. env=None inherits the full WebUI process environment

_load_webui_prefill_context is called at _run_agent_streaming with no env= kwarg, so the subprocess inherits the WebUI server's entire environment — including every provider API key, OAuth token, gateway secret, and HERMES_* knob loaded into the parent. The script is user‑configured, so this is "trusted code" by definition, but the test you added at tests/test_webui_prefill_context.py:18-22 already demonstrates the safer pattern (env={"PATH": os.environ.get("PATH","")}). I'd promote that to the production call:

# api/streaming.py:3655 — current
_prefill_context = _load_webui_prefill_context(_cfg)

# suggested
_prefill_context = _load_webui_prefill_context(
    _cfg,
    env={"PATH": os.environ.get("PATH", ""), "HOME": os.environ.get("HOME", "")},
)

That keeps recall scripts from accidentally logging the parent's API keys into their own debug output, and matches the existing AGENTS.md guidance about not printing tokens through tool output.

3. _SECRET_SHAPED_RE is re.compile(...), please confirm rendering

The diff display shows the assignment compressed (_SECRET_SHAPED_RE=*** in some viewers) — I confirmed via raw bytes that the source is _SECRET_SHAPED_RE = re.compile(\n r"(?i)(api[_-]?key|...)..."\n) so this is just a GitHub diff‑renderer quirk, but worth knowing. Two minor regex notes:

  • Lone bearer‑style keys like a single sk-… string aren't matched — only when preceded by api[_-]?key|token|password|secret\s*[:=], or when shaped like a 3‑part JWT. A leaked sk-proj-… in a script's stderr would pass through.
  • The status text is already truncated to 240 chars by _redact_prefill_status_text, which limits exposure surface even if the regex misses.

Not blocking; the redactor is already much better than echoing raw stderr.

Other reads that look good

  • surface_context block at streaming.py:213-220 correctly skips empty Workspace/Profile lines, matching tests/test_webui_surface_context.py:28-37. The "not the same live transcript as Telegram, Discord, Slack" guardrail is well‑targeted for the "WebUI thinks it's the gateway" failure mode.
  • context_status is included in the run‑journal cursor list at static/messages.js:2014, so a reconnecting browser doesn't see a stale composer status. Good.
  • The ephemeral_system_prompt change is non‑persistent (verified at agent/system_prompt.py:220 and conversation_loop.py:803-806), so the new surface block won't bloat saved transcripts.
  • The prefill_messages injection at agent/conversation_loop.py:810-814 inserts after the system prompt and before history with pfm.copy() — no mutation hazard.

Test plan beyond the 35 already passing: (a) configure prefill_messages_script pointing to a 21s‑sleep script and verify the 20s timeout produces a status="error" payload (not a worker hang); (b) configure a prefill_messages_file whose JSON has a mix of valid and invalid roles and confirm _valid_prefill_messages drops the bad ones without poisoning the good ones; (c) verify _public_prefill_context_status never carries messages (already covered in test 2 by negative absence, but worth an explicit assertion).

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Phase 0 re-assessment — same subprocess surface concern as closed #2526

Thanks @AJV20 for the consolidated take that combines the surface-context block from the original #2523 with the prefill loader from #2526 into a single coherent PR. The agent review on this head (540292a) caught three good performance/security notes that should still be addressed (per-turn subprocess cost, env=None leaks the parent's keys, regex completeness).

I want to re-raise the architectural concern I raised on the closed #2526, because the surface here is the same — and the three flagged review notes are downstream symptoms of that surface choice:

_load_webui_prefill_context() runs subprocess.run([exe, path], …) on every WebUI chat turn based on whatever path is in prefill_messages_script in config.yaml or HERMES_PREFILL_MESSAGES_SCRIPT in the env. That's a new always-on per-turn shell-out path off a config value. Three things follow:

  1. Anyone with write access to config.yaml (or the env) gets code execution on every turn. WebUI already has the MCP layer for "run a thing when the agent asks" — adding a second always-on, per-turn, untyped shell-out off config means there are now two surfaces a misconfiguration or malicious config edit can hit.

  2. The agent's review notes — TTL caching, env= scoping, secret-regex completeness — are mitigations for a surface we probably shouldn't have added in this shape. Caching reduces the cost but doesn't address the surface; scoping env= reduces leak but the surface remains. If we do take this path, we need all three mitigations plus documentation that this is a power-user feature.

  3. The agent contract change (prefill_messages= kwarg) is still cross-repo coupling. The defensive _agent_kwargs.pop('prefill_messages', None) guard at streaming.py:3796 catches the absence gracefully, but it means the feature is silently dead-code on hermes-webui master until a paired hermes-agent release ships. We've avoided that shape before (see how MCP, memory-providers, and skills are wired) — they go through stable hermes-agent ABCs that already exist.

Suggested paths

  1. MCP path: route session recall through an MCP server (the standard "do a thing on demand" surface) instead of a per-turn subprocess. The agent already wires MCP tools; the user configures Joplin/equivalent as an MCP server. Surface-context still ships separately as the small ephemeral prompt block, no subprocess.
  2. File-only path: keep prefill_messages_file (bounded read, no execution), drop prefill_messages_script. Also keep the surface-context block. This is the minimal viable slice.
  3. Reject the prefill loader, keep the surface-context block: split fix(chat): align WebUI context with messaging sessions #2547 into two PRs. The surface-context block (4 fields, no execution surface) is a clean self-contained ship; the prefill loader gets its own design conversation.

Applying maintainer-review and pinging @nesquena for direction. The surface-context portion of this PR is a clean ship in any path forward — the question is just whether the prefill subprocess path goes with it.

Note: this isn't a re-litigation of feedback that's been addressed — the three agent review notes are valid and would still need to land. It's a layer above those: are we adding the right surface at all?

@nesquena-hermes nesquena-hermes added the maintainer-review Maintainer fit-assessment needed — may not merge even with fixes label May 18, 2026
@AJV20

AJV20 commented May 19, 2026

Copy link
Copy Markdown
Contributor Author

Updated this branch in 35da27b after the latest master merge.

What changed:

  • Resolved the CHANGELOG.md merge conflict against current origin/master.
  • Kept the existing prefill/surface-context hardening changes intact.

Verification:

  • python3 -m py_compile api/streaming.py
  • python3 -m pytest tests/test_webui_prefill_context.py tests/test_webui_surface_context.py tests/test_sprint42.py -q -o addopts= → 38 passed
  • GitHub Actions matrix is green for 3.11 / 3.12 / 3.13

@AJV20

AJV20 commented May 22, 2026

Copy link
Copy Markdown
Contributor Author

Updated this PR branch in 19565b60 after the latest master merge.

What changed:

  • Merged current origin/master into fix/webui-context-parity.
  • Resolved the CHANGELOG.md conflict by preserving this PR's ## [Unreleased] entry and keeping the full upstream release history underneath it.

Verification:

  • git diff --check
  • python3.11 -m py_compile api/streaming.py
  • python3.11 -m pytest tests/test_webui_prefill_context.py tests/test_webui_surface_context.py tests/test_sprint42.py -q -o addopts= → 38 passed

Current pushed head: 19565b602276

@AJV20

AJV20 commented May 22, 2026

Copy link
Copy Markdown
Contributor Author

Updated again in 171d93ae after master advanced to v0.51.115.

What changed:

  • Merged current origin/master into fix/webui-context-parity.
  • Resolved the new CHANGELOG.md conflict by preserving this PR's Unreleased entry above the latest release history.

Verification:

  • git diff --check
  • python3.11 -m pytest ... (context parity tests → 38 passed)

Current pushed head: 171d93aecf4f

@AJV20

AJV20 commented May 22, 2026

Copy link
Copy Markdown
Contributor Author

Ready for maintainer merge. I refreshed this branch against current master, resolved the changelog conflict while preserving the PR-specific ## [Unreleased] entry and upstream release history, and GitHub currently reports the PR as clean with Python 3.11/3.12/3.13 checks passing.

I attempted a direct squash merge, but this account does not appear to have permission to merge in this repository.

@AJV20

AJV20 commented May 22, 2026

Copy link
Copy Markdown
Contributor Author

Updated in 50195c22 to address the maintainer-review concern about the per-turn subprocess surface.

What changed:

  • Removed prefill_messages_script execution from the WebUI streaming path.
  • Kept the bounded JSON-file prefill_messages_file path for ephemeral prefill context.
  • Left executable/session-recall workflows to the existing MCP/tool surface instead of adding an always-on per-turn shell-out before SSE starts.
  • Updated the changelog and prefill tests to reflect the file-only contract.

Verification:

  • git diff --check
  • python3.11 -m py_compile api/streaming.py
  • python3.11 -m pytest tests/test_webui_prefill_context.py tests/test_webui_surface_context.py tests/test_sprint42.py -q -o addopts=36 passed in 2.10s

@AJV20

AJV20 commented May 22, 2026

Copy link
Copy Markdown
Contributor Author

Maintenance update: refreshed this branch against current origin/master in d9e1a9c4 and resolved the CHANGELOG.md conflict by preserving this PR's ## [Unreleased] entry plus the current upstream release history.

Local verification:

  • /Users/abdiel/.hermes/hermes-agent/venv/bin/python3.11 -m py_compile api/streaming.py
  • /Users/abdiel/.hermes/hermes-agent/venv/bin/python3.11 -m pytest tests/test_webui_prefill_context.py tests/test_webui_surface_context.py tests/test_sprint42.py -q -o addopts= — 36 passed
  • git diff --check

GitHub checks for the pushed head are green across Python 3.11, 3.12, and 3.13, and GitHub now reports the PR mergeable/clean.

@AJV20

AJV20 commented May 23, 2026

Copy link
Copy Markdown
Contributor Author

Maintenance update: refreshed this branch against current origin/master in a263abdf and resolved the CHANGELOG.md conflict by preserving this PR's ## [Unreleased] entries above the new v0.51.118 release notes.

Verification run locally:

  • python3 -m py_compile api/streaming.py
  • pytest tests/test_webui_prefill_context.py tests/test_webui_surface_context.py tests/test_sprint42.py -q -o addopts= — 36 passed
  • git diff --check

GitHub Actions are green on Python 3.11, 3.12, and 3.13; mergeability is now clean.

@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 — refreshed against master, CI green

Thanks @AJV20. Verified your a263abdf refresh:

  • ✅ Rebased clean against current origin/master (post-v0.51.118)
  • CHANGELOG.md conflict resolved — PR's [Unreleased] entries preserved above the new v0.51.118 entry
  • ✅ GitHub Actions green across Python 3.11/3.12/3.13
  • mergeable: CLEAN
  • ✅ Local pytest: 36 passed on the focused suites you ran

Applying ready-for-review. Leaving hold + maintainer-review for @nesquena — the maintainer-review here is for the architectural fit-decision (WebUI context ↔ messaging session alignment), not for mechanics. Mechanics are clean.

@AJV20

AJV20 commented May 24, 2026

Copy link
Copy Markdown
Contributor Author

Maintenance update: refreshed this branch against current origin/master in d7b98d87 and resolved the merge conflicts by preserving this PR's scoped changes plus the current upstream release history.

Verification run locally:

  • /Users/abdiel/.hermes/hermes-agent/venv/bin/python3.11 -m py_compile api/streaming.py
  • /Users/abdiel/.hermes/hermes-agent/venv/bin/python3.11 -m pytest tests/test_webui_prefill_context.py tests/test_webui_surface_context.py tests/test_sprint42.py -q -o addopts= — 36 passed
  • git diff --check

Leaving any existing maintainer-review / hold / draft state unchanged.

@nesquena-hermes nesquena-hermes closed this pull request by merging all changes into nesquena:master in e50d6cc May 24, 2026
pull Bot pushed a commit to JamesWilliam1977/hermes-webui that referenced this pull request May 24, 2026
fix(chat): align WebUI context with messaging sessions
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
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
fix(chat): align WebUI context with messaging sessions
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
fix(chat): align WebUI context with messaging sessions
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hold maintainer-review Maintainer fit-assessment needed — may not merge even with fixes 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.

2 participants