--- # Hermes Web UI -- Changelog ## [Unreleased] ## [v0.51.212] — 2026-06-02 — Release GF (stage-batch2 — i18n regenerate-title strings + self-restart argv + todos cold-load) ### Fixed - Localized the five `session_title_regenerate*` session-menu strings (the "Regenerate title" action, its description, and the regenerating/regenerated/failed states) that shipped as English text in every non-English locale. Translated across it, ja, ru, es, de, zh, zh-Hant, pt, ko, fr, and tr, matching each locale's existing terminology; `zh`/`zh-Hant` keep the `\u`-escaped style of those blocks (#3396, @vanshaj-pahwa, closes #3364). - Self-update re-exec now distinguishes source checkouts from frozen/packaged builds: a frozen binary (`sys.frozen`) re-execs with `sys.argv` as-is, while source checkouts keep the `[sys.executable] + sys.argv` CPython idiom. Previously the frozen path re-inserted the binary as `argv[1]`, turning re-exec into a no-op that left the WebUI stuck "offline" after every self-update (#3395, @PatrickNoFilter). - The Todos panel now hydrates correctly on a cold session load (page refresh) even when the latest todo tool result is outside the truncated display window: `/api/session` derives a compact `todo_state` sidecar from the full settled transcript, and an explicit empty todo list is honored as the current state instead of falling through to an older non-empty write. A malformed historical tool message can never break session loading (#3373, @v2psv). ## [v0.51.211] — 2026-06-02 — Release GE (stage-batch1 — reasoning heuristics + /model shortest-match + Copilot env-token filter) ### Fixed - Generalized reasoning-effort capability checks in `_candidate_supports_reasoning` to target whole model families (GPT-5+, Claude 4/3.7, Qwen-3, Kimi, Minimax, Mimo, GLM, Step, and DeepSeek) instead of anchoring on hardcoded version numbers or vendor formats. This prevents the thinking-level configuration selector from being hidden on custom providers, new model releases, or when names carry suffixes like `-free` or `:free` (common on integrations such as Kilo Code or OpenCode Zen). The GPT heuristic is now version-anchored (5+) to avoid falsely enabling reasoning_effort for gpt-4o/4.1/3.5 on aggregator providers (#3379, @b3nw, closes #3377). - The `/model` slash command no longer selects a longer model variant when a shorter name is a prefix of it (e.g. `/model mimo-v2.5` selecting `mimo-v2.5-pro`). The fuzzy fallback now prefers an exact id/label match and otherwise the shortest matching option, applied to both the main and bare-name (`provider/...`) fallbacks (#3394, @vanshaj-pahwa, closes #3368). - `GITHUB_TOKEN` and `GH_TOKEN` environment variables are now filtered from the Copilot credential pool alongside the seeded `gh`-CLI token, so a classic PAT (`ghp_*`) auto-detected from the environment no longer makes Copilot appear in the model picker when the Copilot API can't use it. User-specific `COPILOT_GITHUB_TOKEN` is still respected (#3382, @happy5318). ## [v0.51.210] — 2026-06-02 — Release GD (stage-batch1 — model-picker multi-slash fix + extensionless preview highlighting) ### Fixed - Model picker no longer snaps to the wrong model when multiple multi-slash model IDs from the same proxy provider share the same base name. Exact-match priority in `_findModelInDropdown` and first-segment-only stripping in `_normalizeConfiguredModelKey` / `_norm_model_id` prevent collisions in selection, badge assignment, and configured-entry dedup (#3360, @b3nw). - Workspace file previews now syntax-highlight common code/config filenames without useful extensions, including `Dockerfile`, `Dockerfile.*`, `Makefile`, `GNUmakefile`, `CMakeLists.txt`, `.gitignore`, and `.dockerignore` (#3365, @AJV20). ## [v0.51.209] — 2026-06-02 — Release GC (WebUI dashboard plugin system with iframe isolation) ### Added - WebUI dashboard plugins: plugins that ship a UI under `~/.hermes/plugins//dashboard/` (with a `manifest.json`) now appear as opt-in cards in Settings → Plugins (default off). Once enabled, an **Open** button renders the plugin page inside a sandboxed iframe (`sandbox="allow-scripts allow-forms allow-popups"` — no `allow-same-origin`, so plugin JS/CSS/modals stay fully isolated from the parent app). New `/plugins/` (shared assets) and `/dashboard-plugins//` (per-plugin assets) static routes serve only built `dist/`/`static/` files with path-traversal, dotfile, and extension-allowlist protection (plugin source/config such as `plugin_api.py`/`manifest.json`/`.env` is never served), and both the page and asset routes are gated server-side on the enable state + an HTTP `sandbox` CSP + `nosniff`. Plugin `name` and `tab.path` are validated at load. Display-only — no plugin backend/subprocess execution (#2622, @pix0127). ## [v0.51.208] — 2026-06-02 — Release GB (workspace upload hardening hotfix) ### Fixed - Hardened the workspace file-upload surface (#3104 follow-up): (1) a negative `Content-Length` no longer bypasses the size cap and triggers an unbounded `rfile.read(-1)` — the length is now validated `[0, MAX_UPLOAD_BYTES]` centrally in `parse_multipart` for every upload handler; (2) `.tar`, `.tbz2`, and `.txz` archives now auto-extract (the upload handler's archive-suffix set was narrower than `extract_archive`'s, so those silently landed as raw files); (3) a rejected archive (zip-slip / zip-bomb / corrupt / too-many-members) now surfaces an error toast in the workspace panel instead of a misleading "Uploaded" success; (4) an in-workspace symlink subpath can no longer make the upload target `mkdir`/write outside the workspace root. Regression tests added. ## [v0.51.207] — 2026-06-02 — Release GA (Edge TTS as an alternative speech engine) ### Added - Added an optional server-side **Edge TTS** speech engine (Microsoft neural voices) selectable in Settings → Preferences → TTS Engine, alongside the existing browser speech synthesis. The voice list switches to the Edge neural voices when selected. A new `POST /api/tts` endpoint streams the audio, gated by the same-origin CSRF check + session auth, a per-client rate limit, a 5000-character cap, and a voice allowlist. `edge-tts` is an optional dependency — the endpoint returns a clear install hint (503) when it isn't present, so existing installs are unaffected (#2931, @liuqiangweb-svg). ## [v0.51.206] — 2026-06-02 — Release FZ (workspace file upload + drag-and-drop with archive extraction) ### Added - Workspace file panel: an **Upload** button and drag-and-drop that POST to a new `/api/workspace/upload` endpoint. Files land in the session workspace (resolved via the trusted-workspace guard), are de-duplicated with `-1`/`-2` suffixes, and archives (`.zip`/`.tar.*`) are auto-extracted into the target subdirectory with zip-bomb (size-cap + member-count-cap) and zip-slip (path-containment) protections. The extraction size cap is tunable via `HERMES_WEBUI_MAX_EXTRACTED_MB` (defaults to 10× the upload cap). Extraction errors are surfaced to the frontend instead of being silently swallowed, and the archive is removed on failure (#3104, @antoniocarlos97ss). ## [v0.51.205] — 2026-06-01 — Release FY (stage-hi1 — workspace syntax highlighting + generated-image cards + manual title regeneration) ### Added - Workspace file previews now render with syntax highlighting via Prism.js (already loaded for chat code blocks), covering common languages (Python, JS/TS, CSS, JSON, SQL, shell, and more) and degrading gracefully to plain text for unknown/plain files and when offline. The preview code surface uses a single uniform background across light and dark themes (#3337, @mysoul12138). - Generated local image artifacts now render as a clean inline image (with click-to-zoom lightbox) plus a hover/focus-revealed **Download** action overlaid on the image, served through authenticated `/api/media` URLs — matching the common AI-chat pattern of letting the image be the hero rather than wrapping it in a permanent card (#3220, @AJV20). - The session action menu can regenerate conversation titles on demand from the saved transcript, updating the sidebar without touching conversation chronology and syncing the new title through to state.db when Insights sync is enabled. The menu was also streamlined to a compact icon + label layout (descriptions move to hover tooltips). Closes #3106 (#3223, @AJV20). ## [v0.51.204] — 2026-06-01 — Release FX (stage-batch17 — project/session operations honor the session's own profile) ### Fixed - Project and session operations (project create/rename/recolor/delete/unassign, session move, and the profile chip label) now key on the session's own profile (`S.session.profile`) instead of the global active profile, so switching between sessions from different profiles no longer causes silent 404s, misleading chip labels, or project-picker entries from the wrong profile. The project picker also filters to the session's profile and surfaces an error toast on failure instead of a silent no-op (#3331, @PINKIIILQWQ). ## [v0.51.203] — 2026-06-01 — Release FW (stage-batch15 — sticky manual unpin for streaming chat scroll) ### Changed - Streaming chat scroll now uses a sticky manual-unpin model: once you scroll up to read earlier content during a streaming response, the view stays put and no longer auto-follows the live tail until you scroll back to the bottom (near-bottom hysteresis on downward motion) or click the scroll-to-bottom control. Tool cards, token updates, and layout growth no longer re-pin the viewport after a reading pause. This replaces the #3250 upward-intent timeout and supersedes the v0.51.199 proximity-re-pin (#3330), matching the streaming-scroll behavior of ChatGPT/Claude/Codex. Fresh streams reset the follow state on attach (#3343, @pamnard). ## [v0.51.202] — 2026-06-01 — Release FV (stage-batch14 — filter interrupted-recovery control text from visible transcript) ### Fixed - Interrupted SSE-recovery control text (the synthetic `stale_interrupted_event` run-journal payload) is now kept out of the visible chat transcript instead of being replayed as a message: it's marked `recovery_control` on the backend and filtered across the `msgContent()` render path, the SSE settle/error handlers, and final transcript filtering, so platform-only control state no longer leaks into the conversation (#3321, @franksong2702). ## [v0.51.201] — 2026-06-01 — Release FU (stage-batch13 — colored diff lines in tool-card snippets) ### Added - Tool-card result snippets that contain a unified diff now render with the same green/red/cyan diff coloring already used for diffs in chat messages (reusing the existing `.diff-block` styles), with an expand/collapse toggle that preserves the coloring. Non-diff snippets are unchanged (#3336, @mysoul12138). ## [v0.51.200] — 2026-06-01 — Release FT (stage-batch12 — remote-gateway health probe + ephemeral-turn-field preservation) ### Fixed - The Tasks/Cron panel no longer shows a spurious "Gateway not configured" banner in multi-container Docker deployments where the WebUI image doesn't ship the `gateway` Python package: agent-health now probes the remote gateway via `HERMES_API_URL` before falling back to the local `gateway.status` import. Closes #3281 (#3312, @Sanjays2402). - Force-reloading the active session (`loadSession(sid, {forceReload:true})`) no longer drops ephemeral turn fields (`_turnUsage`, `_turnDuration`, `_turnTps`, `_gatewayRouting`, `_statusCard`): the ephemeral-field carry-forward now reads the prior `S.messages` before it's reset, so the token-usage badge and status cards survive an external refresh. Closes #3306 (#3313, @Sanjays2402). ## [v0.51.199] — 2026-06-01 — Release FS (stage-batch11 — pinned-scroll recovery + inline-math currency false-positive) ### Fixed - Pinned chat now recovers its scroll position after a DOM rebuild: `_setMessageScrollToBottom` retries on the next layout frame, and `scrollIfPinned` re-pins when the pane has drifted more than 500px from the bottom, so a message-list rebuild no longer leaves a pinned conversation stranded mid-scroll. Closes #3319 (#3330, @jianongHe). - The `$...$` inline-math renderer no longer treats currency like `$1,000 xuống ~$95` as math: the opening `$` followed by a digit is now rejected (aligning with smd's `se()` guard), so dollar amounts render as plain text. Digit-leading inline math (e.g. `$2x = 4$`) should now use the LaTeX-style `\(2x = 4\)` or display `$$2x = 4$$` delimiters (#3311, @toanalien). ## [v0.51.198] — 2026-06-01 — Release FR (stage-batch10 — custom-provider reasoning model-id normalize + profile skill counts + run-adapter RFC slice) ### Fixed - Reasoning-effort detection for named `custom:*` providers now normalizes non-slash model ids before applying its fallback family heuristics, so separator variants such as `deepseek.v3.2`, `deepseek_v4_flash`, and vendor-namespaced ids like `vendor.deepseek.v3.2` resolve the same way as `deepseek-v4-flash`. The keyword fallback is now token-aware rather than substring-based, preserving names like `model-thinking-preview` without falsely enabling reasoning for unrelated prefixes such as `thinkinghub.llama-3.1-70b` (#3327, @Carry00). - Profile cards now show enabled vs compatible skill counts (computed with an 8s TTL cache that clears on profile switch) instead of a single ambiguous count. Closes #3339 (#3341, @b3nw). ### Changed - The #1925 runtime-adapter RFC now marks the configured runner-client boundary as shipped in v0.51.188 (#3073 / #3274) and defines the next Slice 4g gate for a supervised local runner process harness: real runner-owned `AIAgent` execution, restart/reattach proof, bounded runner health diagnostics, and no new WebUI runtime-surrogate globals (#3334, @Michaelyklam). ## [v0.51.197] — 2026-06-01 — Release FQ (stage-batch9 — stop agent replaying edited/undone messages) ### Fixed - Editing or undoing a message no longer lets the agent replay the original pre-edit content from `state.db`: the truncation-watermark filter now also skips replaced/stale rows whose timestamp sorts *below* the watermark, and `POST /api/session/truncate` truncates `context_messages` in sync with `messages` so the agent's context matches the visible transcript after Edit/Regenerate. The earlier `_clamp_context_to_watermark()` approach (which turned the watermark into a permanent ceiling that dropped every new turn) is removed. Closes #2914 (#3102, @AlexeyDsov). ## [v0.51.196] — 2026-06-01 — Release FP (stage-batch8 — file-manager external sessions + artifacts tool metadata + edge-toggle icon + type hints) ### Fixed - File manager (folder download, raw file fetch, and related handlers) now falls back to a `state.db` lookup for sessions created by Telegram/CLI rather than the WebUI, resolving them against the active WebUI workspace instead of returning a 404. Closes #3280 (#3314, @Sanjays2402). - Artifacts tab now detects files from structured `tool_calls` (OpenAI format) and `tool_use` content blocks (Anthropic format) on messages, not just text-mined diff fences, so artifacts surface even when `S.toolCalls` is cleared after a reload; display paths are trimmed of the workspace prefix (#3329, @mysoul12138). - Workspace panel edge-toggle chevron now points left (toward the panel it reveals) instead of right (#3318, @xz-dev). ### Internal - `api/state_sync.py` now uses `Optional[T]` annotations for parameters defaulting to `None` instead of the implicit `T = None` form (#3323, @kuishou68). ## [v0.51.195] — 2026-06-01 — Release FO (stage-batch7 — hide attachment path markers in chat UI) ### Fixed - Uploaded image attachment path context (`[Attached files: …]`) remains available to the agent in the stored message, but the chat transcript, sidebar display title, and server-derived provisional titles no longer show the raw path suffix to the user (#3296, @AJV20). ## [v0.51.194] — 2026-06-01 — Release FN (stage-batch6 — profiles config-import-cycle fix) ### Fixed - Profile startup now shares platform-default Hermes home resolution through a small `api/paths.py` helper instead of importing the full `api.config` module from `api.profiles`, so importing profiles before config no longer hits a latent circular-load that silently skipped active-profile initialization. Closes #3283 (#3303, @AJV20). ## [v0.51.193] — 2026-06-01 — Release FM (stage-batch5 — ctl dotenv opt-out + workspace inline-open + gateway reply polish) ### Fixed - `ctl.sh` now honors `HERMES_WEBUI_NO_DOTENV=1`, letting tests and scripted launches opt out of repo-local `.env` loading so host-specific `HERMES_WEBUI_STATE_DIR` values do not make the ctl test suite flaky. Closes #3246 (#3304, @AJV20). - Workspace **Open in browser** now opens HTML files inline (with the same `inline=1` + CSP sandbox isolation as the file preview) instead of forcing a download, and uses `noopener` for the new tab (#3305, @xz-dev). - Gateway-backed chat now carries the same WebUI final-answer polish guidance as the in-process chat paths, so terse scratchpad fragments such as "Need script" are not encouraged as visible assistant replies (#3301, @AJV20). ## [v0.51.192] — 2026-05-31 — Release FL (stage-batch4 — per-model context_length default-only guard) ### Fixed - A global `model.context_length` cap (set in config for the default model, e.g. 232000) no longer silently shrinks **non-default** models' real context windows. The cap is now applied only when the session model equals `model.default`; other models (e.g. a 1M-context variant) keep their real metadata window. The guard is applied consistently across the session context-length resolver (`api/routes.py`), the per-turn persistence path, and the live SSE usage payload, and the auto-compress `threshold_tokens` is rescaled to the real cap so the context-window indicator and compression trigger reflect the actual window. The live-usage perf path caches the resolved per-model window once per stream (it runs ~10×/sec during streaming) so non-default-model streams don't take a config/metadata lookup on every metering tick. Backend-only; default-model sessions are unaffected. Closes #3256 (#3263, @allenliang2022). ## [v0.51.191] — 2026-05-31 — Release FK (stage-batch3 — skills-detail markdown styling + launchd duplicate-start guard) ### Fixed - Skills detail view now renders `SKILL.md` markdown with the same `.preview-md` typography used by Memory and Notes, instead of unstyled `renderMd()` output. Linked markdown skill files opened from the detail view use the same wrapper plus scoped post-render `highlightCode` / KaTeX enhancement (#3284, @pamnard). - `ctl.sh start` now refuses to launch a second WebUI instance when a launchd-managed job already owns it (macOS), instead of racing the launchd instance into repeated `Address already in use` churn on port 8787. The guard is macOS/launchd-only, no-ops on every non-launchd path, and can be overridden with `HERMES_WEBUI_CTL_ALLOW_LAUNCHD_CONFLICT=1`; `docs/supervisor.md` documents launchd as the single source of truth. Closes #3289 (#3291, @andrewkangkr). ## [v0.51.190] — 2026-05-31 — Release FJ (stage-batch2 — Windows upgrade state-stranding hotfix + gateway banner + quiet tool previews) ### Fixed - **Windows upgrade no longer strands WebUI state (data-loss-class, priority).** v0.51.134 moved the Windows default Hermes home from `%USERPROFILE%\.hermes` to `%LOCALAPPDATA%\hermes` without a migration, so upgrading users opened an empty app (sessions, pins, UI settings appeared lost — actually just at the address the new build no longer read). `_platform_default_hermes_home()` now prefers the legacy `%USERPROFILE%\.hermes` **only** on the exact post-upgrade fingerprint (legacy holds real `webui/` state AND the new location is not yet established), and `api/profiles.py` delegates to the same resolver so the two can never drift. Non-destructive and self-healing — no files are moved; affected users recover on next launch with no action. Markers key on WebUI-owned `webui/` state only (not agent `auth.json`/`config.yaml`) so a long-time agent user installing WebUI fresh isn't wrongly diverted. Closes #2905 (#3279). - **"Gateway not configured" banner on two-container Docker first deploy.** `GET /api/gateway/status` treated all `alive is None` health payloads as unconfigured-unless-`identity_map`, so a freshly-started gateway that hadn't ticked `updated_at` yet (and had no conversations) reported "not configured." A stale-but-**running** gateway (reason `gateway_stale_running_state`, or a `gateway_state == "running"` detail) now reports `configured = True`; a stale-**stopped** gateway deliberately still falls through to the `identity_map` signal so a stopped root gateway reads as "not configured" per #1944. Closes #3194 (#3279). - Collapsed tool-call previews stay quiet: instead of falling back to raw result JSON (which made tool-heavy turns look like debug logs), a settled collapsed tool card now shows a compact argument summary (with verbose/secret-bearing keys like `content`/`patch`/`api_key`/`token` excluded) or a short status, keeping the full result inside the expandable detail body (#3267, @ai-ag2026). ## [v0.51.189] — 2026-05-31 — Release FI (stage-batch1 — ruff lint gate + SSE refresh dedupe + tooltip i18n) ### Added - **Forward-looking Python lint gate (ruff).** A curated `[tool.ruff]` ruleset (E9 syntax/runtime + F pyflakes + B bugbear — high-signal correctness rules, no style/formatting families) now gates **new and changed Python code** in CI (`tests.yml` `lint` job) and as part of the maintainer pre-release pre-gate. It is the Python twin of the existing ESLint runtime guard for `static/*.js`. Crucially it is **line-scoped** (`scripts/ruff_lint.py --diff`): it flags violations only on lines a change adds or modifies, so it keeps incoming code clean **without** reformatting the existing tree's cosmetic backlog (a separate, deferred, maintainer-run decision). `tests/test_ruff_forward_lint.py` additionally holds the whole tree free of E9 errors and skips cleanly when ruff isn't installed. See TESTING.md > "Python lint gate (ruff)". Closes #3273 (#3275). ### Fixed - Gateway SSE reconnect no longer triggers a phantom "new dialog created" sidebar refresh: the initial sessions snapshot pushed on every reconnect is now compared against the current gateway-session set and `renderSessionList()` is skipped when nothing changed (#3270, @PINKIIILQWQ). - Raw-audio recording mic tooltip now uses a recording-specific i18n key instead of the dictation "Stop" label; sidebar lineage/child tooltip suffixes are localized across the locale catalog; and the localized read-only title hover hint for imported sessions is restored. Closes #3242, #3214 (#3272, @ai-ag2026). ## [v0.51.188] — 2026-05-31 — Release FH (stage-batchH — configured runner-client boundary, default-off) ### Added - **Configured runner-client boundary** for the `runner-local` runtime adapter (RFC `hermes-run-adapter-contract`, tracking #1925, Slice 4c/4d). When — and only when — an operator sets `HERMES_WEBUI_RUNNER_BASE_URL`, WebUI delegates the agent run to that external/supervised runner over a small JSON HTTP client (start / observe / status / cancel / approval / clarify / queue / goal) and bridges the runner's events through the existing SSE stream route, instead of owning the run in the main WebUI process. **Default-off and fully reversible:** with no endpoint configured, the factory preserves the existing bounded "runner-local not configured" path and the live in-process streaming path is unchanged — no behavior change for existing users. New `api/runner_client.py` (`HttpRunnerClient` + `runner_client_configured()`) plus additive `_runner_*` SSE-bridge helpers in `api/routes.py`; the legacy `_run_agent_streaming` control flow is untouched (#3073, @AJV20). ## [v0.51.187] — 2026-05-31 — Release FG (stage-batchG — workspace-preview persistence + scroll-intent window) ### Fixed - Workspace file preview no longer closes when a chat response finishes and the UI refreshes the workspace file tree. Background `loadDir('.')` on stream `done` now preserves an open preview instead of always calling `clearPreview()`, and reloads the open file when a write/edit tool touched that path during the turn (skipping reload while the preview has unsaved local edits) (#3262, @pamnard). - During streaming, scrolling up to read earlier content no longer snaps back to the bottom after a brief pause: the upward-scroll intent window was widened from 450ms to 2000ms so DOM-layout changes from the markdown parser / tool-card insertions are still recognized as co-occurring with user intent and don't re-pin the view. Downward scroll, the scroll-to-bottom button, and trackpad-momentum protection are unaffected (#3250, @emanon312). ## [v0.51.186] — 2026-05-31 — Release FF (stage-batchF — update-checker ff-reachability fall-through + utf-8 git-output test coverage) ### Fixed - Agent self-update no longer advertises or applies unreachable release tags when the checkout tracks `main` past an older tag but the newest published tag lives on a divergent side branch (for example `v2026.5.29` → `v2026.5.29.2`). The update checker and apply path now fall through to the configured upstream branch when `git pull --ff-only ` cannot fast-forward, matching the existing #2653/#3140 release-vs-branch routing (#3257, @pamnard). - Added regression coverage pinning `_run_git()`'s UTF-8 decoding (`encoding='utf-8'`, `errors='replace'`) and its defensive `None`-stdout guard, so version detection cannot crash on non-UTF-8 Windows console output (#3254, @zapabob). ## [v0.51.185] — 2026-05-31 — Release FE (stage-batchE — clarify-card bug-fix batch: identical-prompt dedup + autofill guard + GBK startup crash) ### Fixed - The clarify popup could remain visible with stale input state after sending a response when the next queued clarification prompt was text-identical to the previous one. The dedupe signature now includes the prompt's `clarify_id`, so a newly queued identical prompt is treated as new instead of being mistaken for the one already answered (#3245, closes #3241). - Chrome's password manager could autofill the clarify-card input with saved credentials (typically a provider base URL), causing a phantom "Clarification closed" toast on every session completion and injecting the saved URL into the main composer. The clarify input now carries `autocomplete="off"` plus a `readonly` guard that is lifted only when an actual clarification prompt is shown (or on focus), so the browser's heuristic autofill skips it (#3247). - Prevent a server startup crash on non-UTF-8 Windows locales (e.g. Chinese GBK codepage): `_run_git()` now decodes git subprocess output as UTF-8 with `errors='replace'` and defensively guards against `None` streams, instead of letting a `UnicodeDecodeError` on binary `git diff --binary` output leave `stdout=None` and take down `import api.updates` with an `AttributeError` (#3249). ## [v0.51.184] — 2026-05-31 — Release FD (stage-batchD — raw audio upload mode + scroll-preserve + non-POSIX test skip) ### Added - Optional **raw audio upload mode** (Settings → Sound, off by default): when enabled, the composer mic button sends the recorded audio as a file attachment instead of transcribing it locally, so you can use external STT, raw-audio/emotion analysis, or multimodal models. Classic push-to-talk dictation is unchanged when the toggle is off. The mic tooltip reflects the active mode; localized across all 12 locales (#3169). ### Fixed - Transcript scroll position is now preserved during same-session CLI/gateway import SSE refreshes (and the active session's metadata is synced from the refreshed transcript), instead of jumping to the bottom on each refresh (#3237). ### Changed - `tests/test_terminal_process_cleanup.py` (POSIX terminal coverage that imports `fcntl` at module load) is now skipped at collection time on non-POSIX hosts instead of erroring (#3235). ## [v0.51.183] — 2026-05-31 — Release FC (stage-batchC — inline file:// media artifacts + /api/media state-file confinement) ### Fixed - Render assistant-emitted `file://` image artifact links inline through the authenticated `/api/media` route, so browser clients can view generated local images instead of seeing unusable server-local file paths. Only bare (line-start or whitespace-delimited) `file://` URLs are rewritten — `[label](file://...)` markdown anchors keep the normal link path (#3219). ### Security - `/api/media` now hard-denies WebUI state and secret/config files even when they fall under an allowed root (the WebUI state dir, `settings.json`, `state.db`, `auth.json`, `auth.lock`, `config.yaml`, `.env`, signing/PBKDF2 keys, the `sessions`/`memories`/`profiles` state subdirs). Previously the whole Hermes home was an allowed root, so an authenticated session viewing attacker-influenced agent output that emitted a `file://`/`MEDIA:` link to such a file could fetch it. Hardened the boundary at the route for every entry path (bare `file://`, markdown anchors, and `MEDIA:` tokens), closing #3234. ## [v0.51.182] — 2026-05-31 — Release FB (stage-batchB2 — headless browser smoke gate + consolidated client-disconnect handling) ### Added - CI now runs a headless browser smoke test (`tests/browser_smoke.py`, `.github/workflows/browser-smoke.yml`) on every PR and push: it boots the real server agent-free and loads the key pages in Chromium, failing on any console error or uncaught JS exception. This catches the runtime-JS brick class (e.g. a `const` reassigned at runtime as in #3162, or a `function`/`window` name collision as in #2715/#2771) that `node --check`, ESLint, and the mocked test suite cannot see because they only manifest when a real browser executes the page. The smoke is credential-free — it strips `*_API_KEY` from the environment and drives no real model (#3231). ### Fixed - Client disconnects during response writes (browser tab close, SSE reconnect races, mobile network switches, half-closed sockets) are now handled through a single `_CLIENT_DISCONNECT_ERRORS` set and a `_safe_write` helper instead of ad-hoc per-call-site `try/except`, so an expected disconnect no longer surfaces as a misleading server 500 in the logs. The error-response path is itself wrapped so a disconnect while sending a 500 is swallowed quietly rather than cascading. A bare `TimeoutError` from the Joplin notes integration's `urlopen` is now converted to a clean "not reachable" error at the route rather than escaping to the dispatch-level disconnect handler (#3210). ## [v0.51.181] — 2026-05-30 — Release FA (stage-batchA — agent-cache eviction teardown + streaming finalize race) ### Fixed - Cached agents evicted after session-identity mismatches, unsafe runtime refresh, credential self-heal, or skipped compression migration now go through the normal session-boundary teardown path, committing pending memory and closing provider/session resources instead of silently dropping the cache entry (#3218, closes #3215). - Assistant streaming text is no longer lost when a stream completes while you have switched to a different session tab: the SSE `done` handler now sets the stream-finalized flag immediately (before the fade window), so a `stream_end` event arriving mid-fade can no longer trigger `_restoreSettledSession()` and overwrite the live messages with a stale server snapshot (#3201, closes #3195). ## [v0.51.180] — 2026-05-30 — Release EZ (stage-batch62 — session/agent cache ownership hardening) ### Fixed - Guard session and agent caches against compression/continuation id drift: `GET /api/session` now evicts a cached `Session` whose own `session_id` no longer matches the requested key (instead of trusting the LRU), the background title-update/refresh paths only adopt a cached session when its identity matches, and the compression checkpoint migration no longer re-stores a stale object under the old lineage id. Prevents a stale cached object from making `/api/session?session_id=` return an older transcript segment, which looked like a disappeared session (#3191). - Evicted cached agents are now torn down cleanly at the WebUI session boundary: pending session memory is committed first, and only if the lifecycle entry is clean afterward does the agent get unregistered and its memory provider shut down via `shutdown_memory_provider(messages)` (closing provider-owned clients such as Hindsight's aiohttp session) before the session DB is closed — instead of leaking those resources until garbage collection (#3166). ## [v0.51.179] — 2026-05-30 — Release EY (stage-batch61 — custom-provider reasoning efforts + clearer sidebar tooltips) ### Fixed - Reasoning effort selector now appears for thinking-capable models served through custom API aggregators (New API, One API, etc.) that use non-standard model naming — bare names like `deepseek-v4-flash` or dot-separated `moonshotai.kimi-k2.5` rather than the OpenRouter-style `vendor/model` slash format. The heuristic now also strips a dot-vendor prefix and recognizes a `thinking`/`reasoning` token anywhere in the model name; plain non-reasoning models stay hidden as before (#3202). - Sidebar session row tooltips now explain the fork, prior-turn, child-session, and running/unread status badges, and hovering a truncated chat title shows the full title instead of the old "Double-click to rename" hint (#3203). The localized pending-approval/clarify attention tooltip retains precedence over the generic running/unread state tooltip on the status dot, and the fork tooltip keeps its localized "Forked from" base. ## [v0.51.178] — 2026-05-30 — Release EX (stage-batch60 — parallel sharded CI test runs) ### Changed - CI: the test suite now runs in 3 parallel shards per Python version (9 jobs total) via `pytest-shard`, cutting wall-clock test time roughly in half (slowest shard ~70s vs ~180s sequential). To make sharding safe, several tests that asserted a pristine default while a sibling test mutated shared process/server state were fixed to establish their own preconditions: onboarding-completed flag reset (`test_onboarding_mvp`), password-hash cache invalidation (`test_issue693_system_health_panel`), authoritative sessions-file path (`test_auth_session_persistence`), and — the root cause of the worst leak — `test_profile_env_isolation` no longer deletes + re-imports `api.profiles` (which poisoned the module's cached base-home global for every later test); it now points the cached path via `monkeypatch.setattr`. A conftest fixture also restores `HERMES_HOME`/`HERMES_BASE_HOME` after each test as defense-in-depth. Completes the test-sharding half of #3197 (the Docker-cache half shipped in v0.51.177). ## [v0.51.177] — 2026-05-30 — Release EW (stage-batch59 — Docker smoke-test layer caching) ### Changed - CI: the Docker smoke-test workflow now builds the image once and caches its layers via the GitHub Actions cache (`type=gha`), then each compose variant restores from that cache instead of rebuilding from scratch — saving ~1-3 minutes per variant. The image is still built from the PR's local Dockerfile (`load: true`), so PR changes are tested, not the released image. (Partial adoption of #3197 — the Docker half; the test-sharding half is deferred pending test-suite shard-safety work.) ## [v0.51.176] — 2026-05-30 — Release EV (stage-batch58 — sidebar attention indicators) ### Added - Sidebar session rows now surface pending approval and clarify work with a color-coded status dot (red for approvals, amber for clarifies) plus a matching left rail and tinted background, so inactive conversations that need a permission decision or an answer are easy to spot at a glance. A distinct two-tone attention sound also plays for approval/clarify prompts, separate from the completion sound (#3190). ## [v0.51.175] — 2026-05-30 — Release EU (stage-batch57 — internal conversation links) ### Added - Internal conversation links: a "Copy conversation link" action copies a Markdown reference (`session://`) for any conversation, and `session://` references render as same-origin in-app links that open the target conversation without a full page reload (#3179). Links are sanitized through the existing safe-URL allowlist (rewritten to `/session/`, label escaped, sid URL-encoded — verified against quote-breakout and script-injection payloads). - Conversation filtering now recognizes pasted session references directly: raw session IDs, `session://...` references, `/session/...` URLs, and Markdown session links surface the target conversation while preserving normal content-search hits (#3179). ## [v0.51.174] — 2026-05-30 — Release ET (stage-batch56 — CLI/gateway session usage in Insights) ### Added - The Insights page now includes CLI and gateway sessions (Telegram, Discord, cron, TUI) from the Hermes agent's `state.db` in usage totals, model breakdown, and daily activity — not just WebUI-native sessions (#3189). WebUI sessions are de-duplicated so they are counted once, not double-counted against their `state.db` row. ## [v0.51.173] — 2026-05-30 — Release ES (stage-batch55 — Windows path/journal safety + pin-quota snapshot fix + tool-card paging anchor + sidebar dedupe + quieter tool cards) ### Fixed - Windows native runs now skip the POSIX-only turn-journal directory fsync instead of raising `AttributeError` for missing `os.O_DIRECTORY` on every submitted turn (#3170). - `/api/media` now treats Windows cross-drive `commonpath()` comparisons as non-matches instead of 500ing when media paths and allowed roots live on different drives (#3171). - Hidden pre-compression snapshots no longer keep stale pin state or count toward the visible pinned-session quota (#3181). - Tool-call cards stay anchored when scrolling back through paginated history; legacy session-level tool-call indices are rebased to the returned message window and the browser refreshes tool-call anchors whenever a larger history window is loaded (#3120). - Avoid duplicate sidebar rows when a compressed session completes after both the preserved snapshot id and continuation id are already present in the session list. ### Changed - Restored the legacy compact tool-call card chrome by removing the persistent "Tool output" badge and returning the left rail to the muted border treatment. This keeps tool activity visually quieter while preserving the existing collapsible tool details. ## [v0.51.172] — 2026-05-30 — Release ER (stage-batch54 — model-label fallback + dev cache-bust hash + tilde workspace completion + cron project-chip sessions) ### Fixed - Session model labels now fall back to the friendly `getModelLabel()` form instead of the raw model id when gateway routing info is unavailable (#3174). - Dev-build cache-busting now includes a short hash of the tracked dirty diff, so local asset URLs change on each edit instead of staying at a constant `-dirty` suffix (#3159). - Workspace path autocomplete now preserves `~/` suggestions while browsing under the user's home directory (#3173). - CLI-sourced cron sessions that were squeezed past the default sidebar window now stay addressable under their project chip via a dedicated cron-only lookup pass (#3172). ## [v0.51.171] — 2026-05-30 — Release EQ (stage-batch53 — tool-output card badge + Neon opt-in skin) ### Added - Tool-call output cards now carry a persistent "Tool output" badge and accent rail so tool output stays visually distinct from final assistant responses without requiring hover (#2867). - New opt-in "Neon" cyberpunk skin (dark-first, purple/cyan accents). Default-off; select it from the skin list like Catppuccin or Nous. ## [v0.51.170] — 2026-05-30 — Release EP (stage-batch52 — run-aware SSE replay cursors) ### Fixed - SSE run-journal replay cursors are now run-aware: a stale `after_seq` from an interrupted prior stream can no longer suppress replay events in a newer stream whose sequence numbers reset from 1. The reconnect cursor now carries a run-scoped `after_event_id` (`run_id:seq`) and the server ignores it when the run id differs, falling back to same-run `after_seq` dedupe (#3124). ## [v0.51.169] — 2026-05-30 — Release EO (stage-batch51 — skill-toggle profile scoping + update-tag filter + Docker docs) ### Fixed - Docker docs now explain host-localhost URLs (`host.docker.internal` / `host.containers.internal`) and the `sudo docker compose` `$HOME=/root` bind-mount pitfall for users whose WebUI cannot reach host APIs or see `~/.hermes` (#3012, #3006). - Skills panel disabled/enabled state and toggle writes now resolve `config.yaml` from the active WebUI profile instead of the process default Hermes home or startup config override (#3066). - Update checks no longer advertise a newer release tag when a main-tracking checkout already contains that tag; the banner now falls through to the branch comparison path instead of offering an update that cannot fast-forward (#3140). ## [v0.51.168] — 2026-05-30 — Release EN (stage-batch50 — hotfix: mobile "Failed to load conversation messages") ### Fixed - Fixed a `TypeError` in `_ensureMessagesLoaded` that surfaced as a "Failed to load conversation messages" toast on mobile after most messages: a `const msgs` binding was reassigned by the #3018 ephemeral-field carry-forward (introduced v0.51.161), which throws at runtime. Changed to `let`. Mobile triggered it most because SSE/visibility events fire the session-reload path more aggressively (#3162). ### Added - Static JS runtime-error lint guard (`eslint.runtime-guard.config.mjs` + `tests/test_static_js_runtime_lint.py`): a curated, zero-false-positive ESLint check (`no-const-assign`, `no-import-assign`) over `static/**/*.js` that catches the brick-class of runtime errors `node --check` and source-presence tests miss. Runs in the test suite when ESLint is present and skips gracefully otherwise. See `TESTING.md` > "Static JS runtime lint". ## [v0.51.167] — 2026-05-30 — Release EM (stage-batch49 — iOS-style swipe actions for touch devices + session-list FLIP reflow) ### Added - Touch devices now support iOS-mail-style swipe actions on session rows: swipe left to reveal a Delete action, swipe right to reveal Archive (Restore for already-archived sessions). Swipe is gated to touch/coarse-pointer input, so desktop click, context-menu, and drag behavior are unchanged. Delete still routes through the existing confirmation dialog. The session list also gains FLIP-based reflow animation when rows are archived, deleted, or reordered, honoring `prefers-reduced-motion`. ## [v0.51.166] — 2026-05-30 — Release EL (stage-batch48 — shared OpenCode runtime key + cron project-chip sessions) ### Fixed - OpenCode provider key lookup now honors the shared `OPENCODE_API_KEY` fallback for both Zen and Go runtime paths, matching model-picker detection when provider-specific keys are absent (#3145). - Agent-side cron sessions imported from state.db now remain available to their assigned project chip as `default_hidden` rows instead of being filtered out before project reveal logic can see them (#3134). ## [v0.51.165] — 2026-05-30 — Release EK (stage-batch47 — stop EventSource reconnect storm on long-lived SSE streams) ### Fixed - The session-events and gateway SSE streams no longer emit `Connection: close`, which browsers interpreted as a signal that the EventSource lifecycle had ended — triggering an instant reconnect loop that thrashed the session list roughly once per second and forced repeated re-renders / scroll-to-bottom (#3103, regression from the HTTP/1.1 keep-alive change in 598fd4ff). Finite responses that lack a `Content-Length` (e.g. the on-the-fly workspace ZIP download) keep `Connection: close` for unambiguous HTTP/1.1 message framing. ## [v0.51.164] — 2026-05-30 — Release EJ (stage-batch46 — passive performance hardening: refresh coalescing + draft-save dedup + activity placeholders + bounded restart-safety wait) ### Fixed - Coalesced duplicate in-flight sidebar/project refreshes and suppressed overlapping approval/clarify fallback polls, reducing repeated passive requests while preserving latest-refresh-wins sidebar state. - Skipped duplicate composer-draft writes when the normalized autosave payload is unchanged, avoiding redundant full session JSON rewrites during debounced input/focus churn. - Refined empty Activity waiting placeholders to distinguish stream creation, first-token wait, post-tool model wait, and running-tool wait states. - Self-update restart safety now checks active agent runs as well as open SSE streams and waits for in-flight work before re-exec, avoiding update-triggered interruption when a run outlives its browser stream. The wait is bounded (300s) with a logged fallback to re-exec so a long-running or stuck agent run cannot soft-jam the self-update indefinitely. - Documented the WebUI prefill context budget in README and architecture notes so operators can keep new-browser-turn startup context compact. ## [v0.51.163] — 2026-05-30 — Release EI (stage-batch45 — session duplicate/branch field propagation) ### Fixed - Duplicating or branching a session now carries over the fields that were previously dropped, so the copy behaves identically to the original until further edits: `truncation_watermark`, model-facing `context_messages` (deep-copied for independence), gateway routing + routing history, context-engine state, cache-token counters, composer draft, LLM-title flag, and per-session settings (model provider, project, personality, toolsets, context length, threshold). Compression anchors and last-prompt-token counts are intentionally not carried so the copy re-derives them. Prevents data-loss scenarios where, e.g., editing a message in a duplicated session would drop messages during state.db merge. ## [v0.51.162] — 2026-05-30 — Release EH (stage-batch44 — conversation-filter clear button + code-only title-language regression coverage) ### Added - Conversation filtering now shows a clear button inside the search field whenever text is present, letting users clear the filter with one click. ## [v0.51.161] — 2026-05-29 — Release EG (stage-batch43 — 3-PR live-display fixes: jump-to-question on intermediate assistant messages + per-turn usage badge persistence + stale unread/compression-timer/tool-card dedup) ### Fixed - The jump-to-question button now appears on every assistant message that has a resolvable question target, not only the turn-final one. Multi-step turns (tool call → assistant → tool call → assistant) previously stripped the navigation affordance from intermediate assistant bubbles. - Per-turn ephemeral fields (`_turnUsage`, `_turnDuration`, `_turnTps`, `_gatewayRouting`) are now carried forward when a session refresh replaces the in-memory message list with fresh server data, so the usage badge / duration / gateway-routing pill no longer flash and disappear after a compaction restore, external active-session poll, or SSE error recovery (#3018). - The sidebar unread dot no longer sticks on a session after it has been viewed: syncing the viewed count now clears any stale completion-unread marker, and an actively-viewed session syncs its count instead of being flagged unread on tab switch (#3020). - The auto-compression card's elapsed timer is now cleared on completion/error, so a replaced card is no longer treated as a still-running compression; a background-session completion no longer kills the active session's compression timer (#2973). - Tool cards no longer duplicate the result text in both the header and the detail row: the completed tool result is routed to the detail snippet (falling back to the header only when no progress text was streamed), and the detail row is suppressed when the snippet equals the header preview. ## [v0.51.160] — 2026-05-29 — Release EF (stage-batch42 — 3-PR low-risk cleanup: OpenCode shared-key detection + skills-panel profile-aware disabled read + session-index metadata refresh perf) ### Fixed - Detect a shared `OPENCODE_API_KEY` as enabling both OpenCode Zen and OpenCode Go provider groups, matching Hermes Agent bridge environments that expose one OpenCode credential. - The skills panel now reads each skill's disabled state from the active WebUI profile's `config.yaml` (checking `skills.platform_disabled.webui` then falling back to `skills.disabled`) instead of the process-global `HERMES_HOME`, so non-default profiles show the correct enabled/disabled state and stay consistent with the skill-toggle write path. ### Changed - WebUI session-sidebar metadata refresh is faster on large session directories: the persisted session-id listing is cached by directory mtime instead of re-globbing under the sessions lock on every `/api/sessions` poll, metadata-only loads skip the per-row session-index read when the sidecar already carries an authoritative `message_count`, and only runtime/lineage-shaped rows are overlaid with fuller sidecar metadata (historical transcripts are no longer scanned on every refresh). ## [v0.51.159] — 2026-05-29 — Release EE (stage-batch41 — 5-PR low-risk cleanup: Gateway tool-progress forwarding + shutdown diagnostics + CLI snippet-limit parity + numpad-Enter submit + sync-chat notes guardrail) ### Changed - WebUI's durable-notes guardrail now also applies to sync chat and explicitly asks agents to leave external notes and durable memory unchanged unless a turn contains an explicit capture or reusable durable signal; durable note writes should be summarized back to the user. ### Fixed - Gateway-backed browser chat now forwards Hermes Gateway `hermes.tool.progress` SSE events into WebUI's live tool/activity stream, so Gateway runs no longer appear idle while server-side tools are running. - WebUI now logs structured shutdown diagnostics when the server exits or `/api/shutdown` is called, including active stream IDs to help diagnose interrupted turns after restarts. - The chat composer now treats the numeric keypad Enter key as a submit shortcut even when the send-key preference is set to Ctrl/Cmd+Enter, while preserving regular Enter-as-newline behavior in that mode. - The CLI tool-result snippet limit in the browser now matches the backend (`_TOOL_RESULT_SNIPPET_MAX = 4000`), so longer non-diff CLI tool output is no longer truncated to 200 characters before reaching the tool card. ## [v0.51.158] — 2026-05-29 — Release ED (stage-batch40 — 5-PR low-risk cleanup: numpad/keyboard composer fixes + Joplin search auth + provider-qualified model preservation + SSE fallback poll throttle + assistant-reply polish) ### Changed - WebUI chat instructions now explicitly prevent terse scratchpad/planning fragments from appearing in visible assistant replies, while still allowing clear user-facing progress updates during tool-heavy work. ### Fixed - The chat composer no longer forces mobile newline-on-Enter behavior on touch-primary devices that also have a fine pointer present (tablet plus Bluetooth keyboard, detachable Surface, iPad plus Magic Keyboard), so Enter submits with desktop semantics when a real keyboard is in the picture. - The active-session external-refresh fallback poll now fires every 30 s instead of every 5 s. The SSE session-events stream already pushes invalidations in real time, so the poll is only a fallback; the slower interval removes visible scroll jitter and a network/CPU floor on long sessions. - The model picker no longer fuzzy-matches a provider-qualified model id (`@provider:model` or slash-qualified `vendor/model`) to a nearby curated sibling once exact lookup fails, preserving the raw typed value so uncatalogued models stay routable instead of silently snapping to a different model. - Joplin notes search now keeps the `Authorization` header and adds a query-token compatibility shim only for Web Clipper `/search` calls, covering clipper builds that return HTTP 403 for header-only search auth while keeping other Joplin API URLs token-free. ## [v0.51.157] — 2026-05-28 — Release EC (stage-batch39 — 5-PR mixed-risk cleanup: gateway prefill forward + prefill budget + compressed-continuation sidebar + browser-transcript memory guidance + reasoning max parity) ### Added - The reasoning-effort selector now offers a `max` level, matching the agent's `hermes_constants.VALID_REASONING_EFFORTS`. This restores parity with the underlying set (the WebUI mirror previously stopped at `xhigh`) so providers such as Anthropic that support the `max` thinking level are selectable from the composer dropdown and the `/reasoning` command. ### Changed - WebUI's browser-session surface prompt now explicitly tells agents not to dump browser transcripts into external notes or durable memory by default; it limits saving to explicit captures and clearly reusable durable signals such as preferences, decisions, blockers, and runbook-worthy workflows. ### Fixed - Gateway-backed WebUI chat now forwards configured prefill/session-recall context and a compact WebUI session-context block into delegated Gateway turns, so browser sessions retain note recall, connected-platform awareness, and delivery hints instead of sending only the latest user message. If the dynamic prefill script fails, WebUI falls back to the configured static router prefill when available. - Oversized WebUI startup prefill payloads now respect a configurable context budget (`webui_prefill_context_max_chars` / `HERMES_WEBUI_PREFILL_CONTEXT_MAX_CHARS`, default 12,000 chars). When a dynamic prefill script exceeds the budget and a compact static prefill file is configured, WebUI falls back to the compact file; otherwise it injects a small retrieval instruction instead of dumping the full note/body payload into every new chat. - Sidebar now keeps the newest active continuation visible when it has more recent activity than an older fuller pre-compression snapshot in the same lineage. Adds lineage-aware dedupe for WebUI-origin state-db projections, restores normal context-only turns into the visible transcript after compression while preserving order, and recognizes `[Session Arc Summary]` as a compression marker so it isn't backfilled into the chat transcript. ## [v0.51.156] — 2026-05-28 — Release EB (stage-batch38 — 2-PR Tier B cleanup: WebUI request/runtime hardening + chat-start provider fallback) ### Fixed - Hardened WebUI request/session/runtime edges: malformed request body lengths are rejected before reads, session writes reject unsafe IDs, auth session/login-attempt maps avoid unsynchronized mutation, and successful password login clears stale rate-limit failures. - Hardened frontend startup and navigation fallbacks: early storage access now survives blocked `localStorage`, stale session recovery preserves subpath mounts, session URL generation removes both legacy session query aliases, canceling a stream closes the local EventSource, and the PWA shell precaches same-origin markdown/KaTeX vendor assets. - Added missing i18n keys used by command, cron, provider, search/default, and session-rename UI paths across supported locales so missing translations fall back to labels instead of raw key names. - Made workspace Git tests pin their temporary repository branch to `master` so the suite is independent of the host Git default-branch setting. - Browser chat start and queued-turn payloads now fall back to the selected/persisted provider only when it belongs to the same model being sent, preventing fresh sessions from sending a dropdown-selected model with `model_provider=null`. ## [v0.51.155] — 2026-05-28 — Release EA (stage-batch37 — 3-PR very low-risk cleanup: passive timeout toasts + sidecar order + subsecond timestamps) ### Fixed - Passive background refreshes such as sidebar/project polling, health checks, cron-status watches, and client-event logging no longer surface generic timeout toasts; explicit user actions still show timeout errors. (Related to #3024) - Messaging/session display merges now preserve sidecar transcript order when the sidecar already contains at least as many rows as the mirrored state store, avoiding role/content fallback sorting when timestamp precision collapses. - Gateway-backed turns and compacted/reconciled message batches now keep subsecond timestamp ordering instead of assigning the same integer-second timestamp to multiple transcript rows. ## [v0.51.154] — 2026-05-28 — Release DZ (stage-batch36 — 9-PR medium-risk cleanup: cron project chip + KaTeX streaming + recovery + .env keys + discoverability repair + media MEDIA tokens + gateway 401 + notes prefill + cron filter) ### Added - Session discoverability audit now has a default-dry-run `--repair-safe` routine for deterministic cleanup: stale persisted WebUI-as-CLI flags can be cleared from sidecars/index entries, and messageful WebUI rows present only in `state.db` can be materialized into sidecars/index entries when `--apply --backup-dir ` is explicitly provided. ### Changed - The third-party notes drawer's "Recently used by AI" list now follows the provider-neutral WebUI-specific `HERMES_WEBUI_PREFILL_MESSAGES_SCRIPT` / `webui_prefill_messages_script` hook when configured, including argv-style hooks such as `[python3, /path/to/recall.py]` and command strings such as `python3 /path/to/recall.py`, before falling back to the legacy generic `prefill_messages_script`. Configured third-party notes sources such as Joplin, Obsidian, Notion, and llm-wiki remain visible even before runtime tool inventory hydrates. ### Fixed - Streaming KaTeX render passes now skip parser-owned equation placeholders that may still be receiving text, preventing long equations from being marked rendered before the final parser flush completes. (#2976) - Cron sessions assigned to the dedicated Cron Jobs project now remain hidden from the default sidebar while still appearing when that project chip is selected. - Compression parent sessions are no longer repaired as stale interrupted turns when a continuation already exists, preventing false "Response interrupted" markers and hidden continuation rows after auto-compression session rotation. (Refs #2361) - Empty partial activity rows preserved from cancelled turns no longer define sidebar recency, anchor the initial paginated message window, or get restored after newer completed turns. Long sessions with old activity-only partials after recent replies now stay grouped by their latest real message and open on the recent readable transcript. (#3057) - Local `MEDIA:` image tokens in chat history now include the current session id and can render exact image paths already present in that session transcript, so agent-generated artifacts outside the active workspace no longer show as broken thumbnails while arbitrary local paths remain blocked. - Gateway-backed browser chat now turns Gateway API Server 401s into a specific `gateway_auth_error` explaining that `HERMES_WEBUI_GATEWAY_API_KEY` must match `API_SERVER_KEY`, instead of surfacing the Gateway's generic "Invalid API key" body as if the model provider key failed. The browser error renderer recognizes this event type as "Gateway authentication failed" instead of falling back to a generic "Error" heading. `/api/health/agent` also reports redacted gateway-chat configuration status (`enabled`, backend, base URL configured, API key configured) as an operator diagnostic payload; it is not currently rendered as a user-facing health banner. - New profiles with an API key supplied at create time now write the key to the profile's `.env` under the correct provider-specific variable (e.g. `KIMI_API_KEY`, `DEEPSEEK_API_KEY`) at mode 0o600, instead of writing it to `config.yaml` where Hermes Agent never reads it. ## [v0.51.153] — 2026-05-28 — Release DY (stage-batch35 — 11-PR low-risk cleanup: title-language + clarify SSE + upload filename + discoverability + SSE reconnect + gateway image + docker docs) ### Changed - Local fallback title generation no longer has a German-only `Session Bilder` special case; it now uses the same generic topic extraction path as other fallback titles. (Refs #3040) - Title-generation prompts now use the same language-neutral "match the user language" instruction for every locale instead of adding German-only exemplars. (Refs #3040) - Session discoverability audit findings for stale persisted WebUI-as-CLI flags now report whether an API-visible lineage representative already covers the hidden snapshot, including the representative session id in JSON and Markdown output. ### Fixed - Title-language detection no longer treats common English tech/jargon text such as "session die" or DAS/DER references as German just because of shared tokens. (Refs #3040) - Clarify prompt SSE fallback polling now preserves its owner session id, matching approval polling behavior so terminal events from another session cannot stop the active clarify fallback poller. - Duplicate chat uploads now report the actual stored filename in `/api/upload` responses, so suffixed files such as `photo-1.png` do not appear under the original basename in WebUI attachment metadata. - Visible but unfocused chat windows now still attempt the immediate SSE reconnect for the current session; only a real session switch skips the reconnect path. (Refs #3040) - Gateway-backed WebUI chat now forwards current-turn image attachments as OpenAI-style multimodal `image_url` parts when native image input is enabled, matching the legacy WebUI runtime's image handoff. - New chat sessions reset `_messagesTruncated` / `_oldestIdx` so a fresh conversation never displays the stale "Scroll up or click to load older messages" indicator inherited from a previously-paginated session. - `openai-codex` reasoning-effort resolution now lets the existing `models.dev` metadata pass set the supported levels (including `xhigh`) instead of being silently clipped through the Copilot model heuristic. ### Documentation - Clarify two Docker onboarding traps: `sudo docker compose` can mount `/root/.hermes` instead of the user's Hermes home on Linux, and Linux Docker Engine users should use a `host-gateway` alias such as `api.local` for host-local model servers instead of configuring `localhost` inside the container. (#3006, #3012) ## [v0.51.152] — 2026-05-28 — Release DX (stage-batch34 — single-PR optional gateway-backed browser chat) ### Added - Browser chat can now opt into a default-off `HERMES_WEBUI_CHAT_BACKEND=gateway` bridge that routes new WebUI turns through a running Hermes Gateway API server while preserving the existing WebUI chat start/stream contract. Strict enable: only the literal values `gateway`, `api_server`, or `api-server` activate the bridge — generic truthy strings like `1` or `true` keep the legacy in-process WebUI runtime. Configurable via `HERMES_WEBUI_GATEWAY_BASE_URL` (default `http://127.0.0.1:8642`) and `HERMES_WEBUI_GATEWAY_API_KEY` (falls back to `API_SERVER_KEY`). New `api/gateway_chat.py` module isolates the bridge logic; existing direct WebUI chat path unchanged when the env/config is not set. (#3021) ## [v0.51.151] — 2026-05-28 — Release DW (stage-batch33 — 3-PR mid-risk batch: SSE reattach + title-lang + composer cap) ### Fixed - Live SSE stream now reattaches when returning to a session that lost its connection during a session switch, closing the connection-leak window where stale `EventSource`s could accumulate. Also fixes a `_dirty_suffix` correctness path and yields the GIL after every SSE put so the HTTP server stays responsive under burst load. (#2924, #2925) - Generated session titles now stay in the conversation language by adding an explicit title-generation instruction to the auxiliary prompt. Prevents the default prompt from drifting into English for non-English conversations. (#2984) ### Changed - Composer box max-width is now capped at 1600px on ultrawide viewports (≥1600px) so chips stay anchored against a content-sized boundary instead of stretching across 3440px+ displays. Maintainer-confirmed cap from the #2856 thread. (#2946) ## [v0.51.150] — 2026-05-28 — Release DV (stage-batch32 — single-PR reasoning-effort agent metadata) ### Fixed - Reasoning-effort capability detection now consults Hermes Agent's `models.dev` metadata before falling back to WebUI-local provider/model prefix heuristics. xAI OAuth Grok models (e.g. `grok-4.3`) and other native/provider-specific catalogs that Hermes Agent already knows about now show the reasoning-effort chip without requiring a per-provider WebUI allowlist update. Existing exact-resolver paths (ACP unsupported, Copilot/GitHub effort subsets, OpenAI Codex, LM Studio live probing) keep their authoritative behavior — the new metadata lookup sits between those resolvers and the broad heuristic fallback. Metadata `supports_reasoning=False` is treated as authoritative so known non-reasoning variants stay hidden. Also: the no-query boot path of `/api/reasoning` now hydrates against the configured default model so the chip can populate before the front-end has session model context. (#3017) ## [v0.51.149] — 2026-05-28 — Release DU (stage-batch31 — hyphenated session ids + prefill role consistency) ### Fixed - Session IDs containing hyphens (e.g. API-issued `api-*` and gateway-issued `reachy-voice-*`) are now accepted by every filesystem-touching session validator: `Session.load`, `Session.load_metadata_only`, `_repair_stale_pending`, `/api/session/delete`, and `/api/session/worktree/remove`. Previously the load path accepted hyphens but the delete and worktree-remove routes rejected them with HTTP 400, producing a confusing "visible in sidebar but undeletable" UX. Refactors the duplicated character-class check into a shared `api.models.is_safe_session_id` helper with regression coverage at every call site. (#3023, #3024) - Plain-text `webui_prefill_messages_script` output is now wrapped as a `user` prefill message instead of a `system` message, so dynamic recall context from notes/Obsidian/Joplin scripts becomes ordinary turn context rather than an extra system instruction. The JSON message-list escape hatch is unchanged: scripts that emit explicit `[{"role": "system", "content": "..."}]` still produce a system message. Avoids provider-specific multi-system-message footguns (Anthropic concatenation, OpenAI Responses-API divergence). (#3009) ## [v0.51.148] — 2026-05-28 — Release DT (stage-batch30 — single-PR Insights skill-usage reader) ### Added - Insights page now shows a Skill Usage card after the LLM Wiki card, displaying per-skill cumulative invocation counts (uses / views / patches / share-%) from the agent-owned `.usage.json`. WebUI reads only; the agent (`tools/skills_tool.py`, `tools/skill_manager_tool.py`) is the single writer with `fcntl` locking, so there is no double-counting or write race. Empty-state shows when no skills have been used yet. Includes i18n keys for the 12 new strings across all supported locales. (#3008) ## [v0.51.147] — 2026-05-28 — Release DS (stage-batch29 — single-PR streaming ownership-cleanup follow-up) ### Fixed - Settled stream cleanup helpers (`_restoreSettledSession`, `_handleStreamError`, `_deferStreamErrorIfPageHidden`, `_reattachOrRestoreAfterDeferredStreamError`) now thread the owning `EventSource` instance through every async deferred path, so a late error or settle callback from an older source can no longer tear down a newer reconnect source. Completes the ownership-aware cleanup pattern introduced by `closeLiveStream(sessionId, streamId, source)`. (#2930, #3010) ## [v0.51.146] — 2026-05-28 — Release DR (stage-batch28 — 6-PR low-risk safety+contrast batch) ### Fixed - Dark-mode panel header save buttons now use a theme-aware foreground token, keeping workspace and other detail-pane check icons visible on the default gold accent. (#2998, #3022) - Custom provider `/v1/models` discovery now uses a short per-endpoint timeout and gracefully skips slow or unreachable providers, reducing cold `/api/models` cache rebuild latency. (#3024, #3025) - Messaging (Telegram-resumed) sessions: the `?messages=0` metadata fast path now routes through the same display merge as the full-message path, so `message_count` and `last_message_at` match the rendered transcript and stop triggering refresh-loops that reset scroll and close open dropdowns. (#3003) - WebUI sessions mirrored into `state.db` for long-history retention now stay in the WebUI sidebar tab instead of being misclassified as CLI rows. Adds regression coverage for both directions of the WebUI vs CLI source-tab invariant. (#3027) - Sidebar projection now keeps at least one messageful representative visible per non-background conversation when normal filters would otherwise hide every row, rescuing discoverability for sessions with stale snapshot/lineage metadata. Rescued rows are marked `discoverability_warning: rescued_messageful_hidden_session` for auditability; intentional background/cron sessions stay hidden. (#3028) ### Added - New read-only `api.session_discoverability` audit module cross-checks JSON sidecars, `_index.json`, `state.db`, and the live sidebar response to classify messageful sessions without a visible representative, stale WebUI-as-CLI source flags, missing sidecars, and lineage segments without a visible tip. Diagnostic surface only; does not repair, restart, or mutate any state. (#3029) ## [v0.51.145] — 2026-05-26 — Release DQ (stage-batch27 — sidebar running-state preservation) ### Fixed - Sidebar session rows now preserve the server-reported running state when merging stale optimistic first-turn cache entries, so active background sessions keep their spinner and can later transition to unread correctly. (#2999, #3001) ## [v0.51.144] — 2026-05-26 — Release DP (stage-batch26 — single-PR terminal supervisor hardening) ### Fixed - Embedded workspace terminals now use a dedicated supervisor thread for shell spawning to eliminate timeout-vs-spawn race conditions. If a `start_terminal()` call times out (5s) but the supervisor's `subprocess.Popen` completes later, the late-arriving process is now reaped via `_reap_abandoned_spawn()` (SIGHUP → wait → SIGKILL escalation) instead of being orphaned. Adds 8 Linux-only concurrency regression tests covering concurrent spawns, Popen-failure recovery, repeated-failure-survival, and the timeout-race late-commit path. (#2880) ## [v0.51.143] — 2026-05-26 — Release DO (stage-batch25 — single-PR workspace:// markdown scheme) ### Added - Chat markdown links using `workspace://path/to/file` now open the target in the workspace preview pane instead of navigating away from the WebUI. Renderable via both the settled `renderMd()` and live `streaming-markdown` paths. Workspace path existence is verified via `/api/list` before opening; missing files surface a `file_open_failed` status toast. (#2881, #2938) ## [v0.51.142] — 2026-05-26 — Release DN (stage-batch24 — 4-PR fresh-today batch) ### Added - New `GET /api/crons/delivery-options` endpoint surfaces the agent's full delivery-platform registry. The cron-create UI now reads delivery options dynamically (including telegram, discord, slack, feishu, wecom, signal, etc.) instead of a 4-option hardcoded select, and the deliver field is editable for existing jobs. (#2996) ### Fixed - Browser chat fallback now merges `fallback_providers` with the legacy `fallback_model` in Hermes CLI/gateway order, so WebUI can continue to a later non-Codex fallback when the primary Codex provider path fails. Duplicate provider/model/base_url routes are de-duplicated. (#2993) - WebUI streaming sessions created under a non-default profile now attach `session_search` to that profile's `state.db` instead of the server's default profile database. (#2965, #2995) - `client_secret` deny-list entry in the models-cache auth.json fingerprint now carries an inline rationale comment, and the `_write_auth` test helper docstring matches its actual contract. Follow-up to #2964 review nits. (#2994) ## [v0.51.141] — 2026-05-26 — Release DM (stage-batch23 — 4-PR second hold-bucket pass) ### Added - WebUI can now opt into a `webui_prefill_messages_script` / `HERMES_WEBUI_PREFILL_MESSAGES_SCRIPT` hook for dynamic browser-turn prefill context from local notes or recall systems. The script output is capped at 256 KiB, normalized to ephemeral prefill messages, and browser status still hides message bodies while redacting script errors. - Added a read-only WebUI/CLI session source switch in the chat sidebar when agent session sync is enabled. WebUI conversations stay in the default list, while imported CLI/agent sessions are surfaced under a separate `CLI sessions` tab with counts so large CLI histories do not clutter the normal conversation list. (Refs #2351) ### Fixed - Compact tool activity now keeps visible interim assistant progress in the live Session timeline instead of making that progress effectively collapsed-only inside Activity details. The interim assistant stream path creates and flushes a visible assistant segment before resetting for later tool/compression activity. ## [v0.51.140] — 2026-05-26 — Release DL (stage-batch22 — 5-PR hold-bucket reassessment) ### Fixed - Live streamed assistant Markdown now treats underscores in ordinary text and identifiers literally, matching the settled message renderer while preserving asterisk-based emphasis. - Models cache no longer churns every ~14 minutes when the auth.json credential pool refreshes. The auth.json fingerprint now hashes provider-identity fields (active_provider, credential_pool ids, base_url, auth_type) and excludes pure credential-rotation fields (access_token, refresh_token, expires_at, last_status, request_count, updated_at). Real provider/model-set changes still invalidate the cache. (RCA t_16551f61) - Paginated `/api/session?msg_limit=N` windows now anchor on the newest renderable transcript row instead of the raw message-array tail. Long sessions whose newest rows are only hidden tool-result entries no longer open to an empty visible transcript with non-empty `S.messages`. - `_loadOlderMessages()` now requests a larger cumulative tail window (`msg_limit=current+30`) rather than a separate `msg_before` page. Suffix-continuity check guards against races and falls back to the legacy index-page request when the loaded transcript is no longer the suffix of the new server response. ### Performance - `renderMessages()` now caches `renderMd()` / `_renderUserFencedBlocks()` output keyed on message text, caches the visible-message scan across renders, scopes the question→assistant lookup to the visible window, and Prism-highlights only newly-mounted code blocks. Long sessions with many messages no longer freeze the browser tab on every render. ## [v0.51.139] — 2026-05-25 — Release DK (stage-batch21 — 5-PR tier-2 batch) ### Added - Image lightbox now supports prev/next navigation when multiple images are present in the same message. Click `‹` / `›` buttons or use `←` / `→` keyboard arrows to browse; an image counter (`1 / 5`) is shown at the bottom. (#2967) ### Fixed - Session switching now keeps the initial metadata fetch on `/api/session?messages=0&resolve_model=0` and defers stale model/provider repair until after the new session is assigned, so first paint does not block on cold model catalog hydration. - Metadata-only session loads now use a cheap state summary instead of full transcript reconciliation, while still detecting real external state.db growth and ignoring restamped replay rows that would otherwise retrigger refresh polling. - Session transcript reconciliation now precomputes visible-duplicate lookup state instead of recomputing loose-content normalization for every state.db row, reducing long-session tail-load latency without changing the append-only merge contract. - Vendored KaTeX CSS, JavaScript, and fonts locally so math rendering no longer triggers CSP font reports for `cdn.jsdelivr.net` font files. - Vendored `js-yaml` locally so YAML tree-view loading no longer triggers CSP script reports for `cdnjs.cloudflare.com`. - Session delete now prunes the deleted row from `_index.json` in place instead of discarding the whole index, and composer draft saves skip the sidebar index entirely. Both reduce churn on new-session sends after a delete or while typing. ### Changed - Sidebar chat search now highlights matching title text and shows a subtle content preview for body-only matches. ## [v0.51.138] — 2026-05-25 — Release DJ (stage-batch20 — 7-PR ultra-safe batch) ### Added - **PR #2972** by @Michaelyklam (refs #1925) — Advance the runtime-adapter RFC after the Slice 4e route-selection harness shipped in v0.51.129. The RFC now defines the Slice 4f supervised local runner client backend gate: replace the bounded `runner-local` 501 only when explicitly configured, prove restart/reattach from durable runner/journal state, preserve the public chat-start field whitelist, require cancel as the first live runner-owned control, and keep active-run discovery/supervision out of new WebUI process-local runtime-surrogate globals. ### Changed - Contributor guidance now requires explicit `Contract Routing` for contract-affecting PRs and `Contract Change` when a PR intentionally changes an existing product, runtime, or review contract. Contract tests must move with the corresponding docs instead of silently redefining behavior by themselves, with the current static coverage documented as advisory rather than a GitHub policy gate. - Removing a provider key now surfaces the server's specific CSRF rejection reason ("Session expired - reload the page", "Cross-origin mismatch - check reverse proxy headers", or the fallback "Cross-origin request rejected") when the underlying POST is rejected with 403, instead of swallowing all three into one generic toast. (Refs #2572) - Tool cards in the transcript now use a slightly stronger border and a 2px left edge so tool output stays visually distinct from final assistant prose without requiring hover. (#2867) - Tasks panel "Gateway not configured" banner now includes a direct link to the new `docs/docker.md#scheduled-jobs-and-the-gateway-daemon` section that walks through running the gateway container so scheduled cron jobs actually tick. (Refs #2785) - WebUI structured request logs now include `remote` (client IP) and an optional `forwarded_for` field when an `X-Forwarded-For` header is present, making failed-login and unauthorized-access logs usable by downstream security tooling like fail2ban behind a reverse proxy. ### Internal - Test cleanup in `tests/test_issue1894_provider_overlap.py`: canonicalize the `opencode-go` base URL to `opencode.ai/zen/go/v1` (matching `hermes_cli/auth.py` and `api/config.py`), drop a vestigial `# noqa: N801`, and convert section banner comments to per-test docstrings. ## [v0.51.137] — 2026-05-25 — Release DI (stage-batch19 — 6-PR medium-risk batch) ### Added - Operators can now set `HERMES_WEBUI_CSP_CONNECT_EXTRA` to append validated extra origins to the report-only CSP `connect-src` directive for reverse-proxy or tunnel deployments. ### Fixed - Trim session-level tool call payloads to the returned message window for paginated `/api/session` loads, so long tool-heavy sessions do not send historical tool call summaries during ordinary session switching. - Sidebar compression lineage collapse now prefers the current continuation tip over a preserved parent snapshot when both rows share the same backend segment count. This keeps reloads after context compression from reopening the older parent transcript and making the active conversation appear to disappear. - Reloading a stale `/session/` compression URL now resolves to the visible continuation tip from the sidebar payload instead of reopening the archived parent snapshot. - Undo, retry, and explicit session truncation now persist a sidecar truncation watermark, preventing older `state.db` rows from reappearing after the WebUI transcript was intentionally shortened. - Chat uploads with the same filename in one session now keep distinct attachment files instead of overwriting the earlier upload. - Compression reference card no longer disappears behind the "Load earlier messages" cutoff after subsequent turns. The post-compression anchor is now calculated from the position of the last `[CONTEXT COMPACTION]` marker in the transcript instead of pointing at the visible tail, so the anchor stays at the compression boundary regardless of how many turns have been added since. ## [v0.51.136] — 2026-05-25 — Release DH (stage-batch18 — 5-PR streaming + session index batch) ### Fixed - When the session index is missing, WebUI now starts a background rebuild while preserving the first sidebar full-scan result, so the index is primed for later requests without temporarily hiding existing sessions. - Live token-usage hints now cap the cumulative in-flight tool-result prompt estimate per assistant turn, preventing many large tool callbacks from temporarily inflating the context ring before exact provider accounting arrives. - Streaming checkpoint saves now run under the session's profile environment, so periodic background checkpoints no longer risk falling back to the process-global profile when a WebUI tab is using a non-default profile. - Chat streaming now keeps a single live EventSource registered per session/stream, preventing reconnect or context-compaction paths from stacking subscribers and rendering one assistant token stream multiple times. - Streaming visible-progress text emitted simultaneously through token, reasoning, and interim-assistant callbacks now renders once in the chat transcript instead of duplicating inside Thinking cards. ## [v0.51.135] — 2026-05-25 — Release DG (stage-batch17 — 9-PR small-fix batch) ### Added - Added a proposed canonical session resolution RFC covering URL routes, query parameters, localStorage, sidebar rows, and compression-lineage IDs so future session-routing fixes have one review contract. ### Fixed - Browser session links that use the API-style `?session_id=` query parameter now open the requested conversation instead of falling back to the last locally stored session. - Gateway status now treats existing messaging-session metadata as configured when `gateway.status` is unavailable, avoiding a misleading "Gateway not configured" warning for multi-container deployments with active gateway sessions. - Session sidebar Archive/Delete menu actions now repaint from local sidebar state immediately after the server confirms the mutation, instead of waiting for the full `/api/sessions` refresh before the row disappears. - Clarification dialogs now reserve transcript space while open or collapsed, so the question prompt no longer covers the assistant text needed to answer it. - Chat uploads now send the absolute server-side path for image attachments in the agent text context, restoring immediate tool access (e.g. `vision_analyze`) to files uploaded in the current turn. - Pending uploaded-file user turns no longer double-render when both the optimistic bubble and the server's pending-message hydration produce the same `[Attached files: ...]` suffix. ## [v0.51.134] — 2026-05-25 — Release DF (stage-batch16 — single-PR Windows path defaults) ### Fixed - **PR #2897** by @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 #2840. ## [v0.51.133] — 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) ### Fixed - **PR #2891** by @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 #2892** by @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 #2893** by @franksong2702 — Update Now no longer reports success or enters the restart wait flow when no WebUI or Agent update target is selected. - **PR #2895** by @franksong2702 — Cached WebUI agents no longer overwrite `prefill_messages` with an empty list when a later request does not include explicit prefill context. - **PR #2894** by @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 #2896** by @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] — 2026-05-24 — Release DD (stage-batch14 — 4-PR replayed-context + interrupted-response + shutdown affordance + passkey opt-in) ### Added - **Cursor ACP provider integration** — Add `cursor-acp` to the WebUI model picker and route slash model IDs (for example `cursor/composer-2.5`) through explicit `@cursor-acp:` provider hints so they do not fall through to the configured default HTTP provider. - **PR #2859** by @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 #2824** by @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 ### Fixed - **Reasoning effort chip visibility** — `/api/reasoning` now accepts `model` and `provider` query params and returns `supported_efforts` so the composer chip hides for models without configurable reasoning levels (for example Cursor Composer) while remaining available for models like GPT-5.5. Model picker changes now re-sync the chip after the session model/provider update instead of querying with stale session state. Composer dropdown selections now pass the provider id into `selectModelFromDropdown()` so duplicate bare model ids (for example `gpt-5.5` under OpenAI Codex vs OpenRouter) no longer fall back to the profile default provider when refreshing the chip. - **Cursor ACP routing and new-chat defaults** — New conversations now carry the visible composer picker selection into `POST /api/session/new`, persist model changes before a session exists, and evict cached session agents when the model/provider changes mid-session. - **PR #2685** by @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 #2739** by @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 #2685 and PR #2824 (both originally missing despite functional code changes) - Deferred to follow-up: per-turn cumulative live-tool-prompt token cap (#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] — 2026-05-24 — Release DC (stage-batch13 — 6-PR notes-drawer + context-parity + PWA-swipe + locale polish) ### Added - **PR #2868** by @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 #2527** by @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 #2547** by @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 #2547** by @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 #2547** by @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 #2865** by @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 #2845. - **PR #2882** by @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 #2873** by @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). - **#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] — 2026-05-24 — Release DB (stage-batch12 — 3-PR profile-isolation + boot-precedence + workspace Artifacts tab) ### Fixed - **PR #2827** by @Koraji95-coder — Profile state-sync TLS-vs-thread fix (closes #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 = ''` 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 #2726** by @starship-s — Boot model-default precedence follow-up (refines #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 #2673** by @AJV20 — Workspace Artifacts tab (closes #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 #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` (#2726 vs post-#2716 master): kept PR's parameterized `populateModelDropdown({preferProfileDefaultOnFreshBoot:true})` call (the whole point of #2726) on top of master's #2716 hydration flow. - `static/workspace.js` (#2673 vs post-#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-#2716 and post-#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 #2827 + #2726 + #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] — 2026-05-24 — Release DA (stage-batch11 — 4-PR feature + perf batch) ### Performance - **PR #2836** by @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 #2680** by @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 #2842** by @AJV20 — PWA polish for installed launches. New `static/pwa-startup.js` is loaded synchronously in `` 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 ``. 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 #2794** by @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 #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 #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 `` and HermesPWA namespace populates with 4 helpers as expected. - In-stage commits added Turkish translations for #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 (#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] — 2026-05-24 — Release CZ (stage-batch10 — 2-PR perf + correctness batch) ### Fixed - **PR #2830** by @franksong2702 — Pin state synchronization between persisted index and in-memory sessions (closes #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 #2782 (server-side 404→200 transition for missing CLI-synced sessions) which remains out of scope. ### Performance - **PR #2716** by @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 #2705 / #2686). The new helper threads `profile=` through to `get_state_db_session_messages` to preserve #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 #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 #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 #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: #2821 (pin state sync), and `get_state_db_session_summary` dead-code removed (#2716). ## [v0.51.127] — 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) ### Fixed - **PR #2854** by @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 #2853. - **PR #2855** by @nesquena-hermes — "Update Now" loops for every user past the latest tag (#2846). After #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 #2846. - **PR #2852** by @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 #2811** by @Koraji95-coder — Native-Windows startup E2E workflow now self-tests on PR push (closes the post-#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 #2812** by @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 #2772** by @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 #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 #2537 as superseded (byzuzayli's earlier Turkish PR with narrower scope). - **PR #2776** by @roryford — New `PATCH /api/mcp/servers/{name}` endpoint accepts `{"enabled": bool}`, writes `mcp_servers..enabled` to `config.yaml`, calls `reload_config()`, returns `{"ok": true, "name": "", "enabled": }`. 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 (#2854, #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 #2812 captured at 1280/1440/1920/mobile (iPhone 14 emulation); Telegram-approved. - File a follow-up issue for pdeathsig-on-supervisor-thread hardening (#2854 deferred Option B) and French-locale `open_in_vscode` parity gap (predates this batch, Opus advisor flagged). ## [v0.51.126] — 2026-05-24 — Release CX (stage-batch8 — 2-PR low-risk batch — kanban markdown + live activity timeline) ### Added - **PR #2819** by @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-`

` 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 #2847** by @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] — 2026-05-24 — Release CW (stage-batch7 — 10-PR low-risk batch — UI/UX polish + bug fixes + diagnostics) ### Fixed - **PR #2839** by @tn801534 — Kanban worker log endpoint constructed URLs with a double query string (`?board=?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 #2832** by @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 #2818** by @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 #2826** by @Koraji95-coder — Composer footer chip wraps no longer overlap at narrow widths (closes #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 #2829** by @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 #2823. Supersedes #2828 (same scope, less polished). - **PR #2837** by @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 #2834** by @franksong2702 — Workspace Markdown `mailto:` and `tel:` links now render as clickable links, and sandboxed HTML preview links open outside the iframe (via injected ``) instead of navigating the preview into a browser-blocked page. Adds `tests/test_issue2768_workspace_links.py`. - **PR #2838** by @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 #2820** by @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 #2843** by @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. ## [v0.51.124] — 2026-05-24 — Release CV (stage-batch6 — 3-PR Windows-only stack — agent paths / docs / port hardening) ### Added - **PR #2805** by @Koraji95-coder — `start.ps1`: expand hermes-agent candidate paths for Windows installers. The launcher now searches `$env:USERPROFILE\.hermes\hermes-agent`, the dev-checkout sibling, and the Windows installer roots (`$env:LOCALAPPDATA\hermes\hermes-agent`, `${env:ProgramW6432}\hermes\hermes-agent`, `${env:ProgramFiles}\hermes\hermes-agent`, `${env:ProgramFiles(x86)}\hermes\hermes-agent`) with `Select-Object -Unique` to collapse WOW64 ProgramFiles redirection collisions on 32-bit PowerShell processes. Adds `-PathType Container` to the `HERMES_WEBUI_AGENT_DIR` guard so a file named `hermes_cli` doesn't false-positive. Null-guards `${env:ProgramFiles(x86)}` for constrained environments where it's missing. Zero impact on Linux/macOS — file is `start.ps1`, never loaded by `start.sh` or `bootstrap.py`. ### Documentation - **PR #2806** by @Koraji95-coder — Native Windows venv path corrected in `start.ps1` doc-comment and `README.md`. The previous text suggested "run bootstrap.py inside WSL2 once to create the venv, then this script can use that venv" — but a WSL2-created venv is `venv/bin/python` (ELF) and cannot be invoked by native Windows Python. The corrected guidance is to create a Windows venv natively (`python -m venv venv` from PowerShell), then `start.ps1` auto-discovers `venv\Scripts\python.exe`. WSL2 remains useful as a parallel install for the full `bootstrap.py` + Linux runtime path. ### Hardened - **PR #2807** by @Koraji95-coder — `start.ps1`: `HERMES_WEBUI_PORT` env-var parsing uses `[int]::TryParse` + range guard (1-65535) instead of a bare `[int]` cast that threw `InvalidCastException` with no context on typos or accidental shell expansion. Server-process exit code is captured into `$script:serverExitCode` and emitted via `exit` AFTER the `try/finally` cleanup, so `Pop-Location` always runs (avoids leaving the caller stuck at `$RepoRoot` in interactive or dot-sourced sessions). Also drops a non-functional `@args` splat that PowerShell doesn't populate under `[CmdletBinding()]` — the launcher's existing use case is env-var-driven, no pass-through args needed. ## [v0.51.123] — 2026-05-24 — Release CU (stage-batch5 — 2-PR low-risk batch — gzip+ETag static caching / Open in VS Code) ### Performance - **PR #2779** by @v2psv — Static asset serving negotiates gzip, emits ETags, and uses `immutable` cache headers for fingerprinted URLs. `_serve_static()` in `api/routes.py` previously sent every `/static/*` response with `Cache-Control: no-store` and no `Content-Encoding`, so a page reload over a slow link re-downloaded the full ~2.4 MB JS+CSS shell on every visit. The fix layers three changes inside the same function: (1) gzip the body when the client opts in via `Accept-Encoding`, gated to compressible MIME types and files >1 KB; (2) emit a weak ETag derived from `(size, mtime_ns)` and short-circuit conditional GETs to `304 Not Modified`; (3) send `Cache-Control: public, max-age=31536000, immutable` when the URL carries a non-empty `?v=…` fingerprint (the `__WEBUI_VERSION__` token already substituted by the index template and referenced from `static/sw.js`'s `SHELL_ASSETS`), falling back to `public, max-age=300` otherwise. Raw bytes, compressed bytes, and ETags are cached in-process keyed by `(size, mtime_ns)` so a redeploy is picked up without a restart, while missing/random paths never enter the cache and image/font types skip gzip to avoid wasted CPU on already-compressed payloads. Measured against an asyncio TCP proxy that injects RTT + bandwidth caps for representative VPN scenarios: cold loads improve 2.7-3.1× (e.g. 80 ms RTT / 10 Mbps WireGuard goes from 4.0 s to 1.3 s), warm reloads improve 3.3-4.0× via 304 responses, and bytes-on-the-wire drop 74% on cold loads. Loopback (already fast) still benefits 2.4×. Scope is strictly `/static/*`: `/api/*`, `/stream`, `/`, `/index.html`, `/session/*`, and login/auth routes are served by independent handlers and continue to send `no-store` exactly as before — no change to CSRF, session payloads, SSE buffering, or login flows. 11 regression tests pin gzip negotiation, ETag/304 round-trip including `Vary: Accept-Encoding`, fingerprint-driven cache policy including empty `?v=`, image/tiny-file skip rules, redeploy invalidation, and the existing path-traversal sandbox. ### Added - **PR #2787** by @munim — "Open in VS Code" action in workspace file browser (resolves #2735). Right-clicking any file, folder, or the workspace root now shows an **Open in VS Code** menu item alongside the existing Reveal in File Manager action. The action calls a new `POST /api/file/open-vscode` endpoint which resolves the workspace-relative path via the existing `safe_resolve` traversal guard, then launches VS Code via `subprocess.Popen` (fire-and-forget, consistent with `_handle_file_reveal`). The endpoint resolves the executable via `shutil.which()` first, then falls back to a hardcoded list of common install locations (macOS: `/usr/local/bin/code` and the app-bundle CLI; Linux: `/usr/bin/code`, `/snap/bin/code`; Windows: `%LOCALAPPDATA%\Programs\Microsoft VS Code\bin\code.cmd` and the `%PROGRAMFILES%` variants) so the action works even when the server process inherits a minimal PATH. Configurable via a new optional `vscode` block in `config.yaml`: `command` overrides the default `code` executable; `host_path_prefix` + `container_path_prefix` enable Docker/container host-path translation. If the command cannot be found anywhere, a descriptive error is returned instead of a bare OS error. i18n keys `open_in_vscode` and `open_in_vscode_failed` added with full translations in all 10 locales. 26 new tests in `tests/test_2735_open_in_vscode.py` pin source wiring, command-resolution logic, i18n completeness, translated strings, and live endpoint error paths. ## [v0.51.122] — 2026-05-24 — Release CT (stage-batch4 — 4-PR low-risk batch — stale cache tail / inflight UI / segment flush / reasoning accumulator) ### Fixed - **PR #2802** by @ai-ag2026 — Drop stale inactive cached user tails when `/api/session` reloads a conversation whose saved sidecar already ends on an assistant answer. Supersedes #2733 (held due to async-compression interaction): the new guard adds a `len(cached_messages) <= len(disk_messages)` filter so it never fires when the cache has genuine new concurrent edits beyond the disk state — only when the cache has an unsaved user row past the saved assistant tail. Adds `api/models._inactive_cache_tail_needs_disk_check()` + `_cache_has_stale_unsaved_user_tail()` helpers and 5 new tests in `tests/test_webui_state_db_reconciliation.py`. Previously-held test `test_session_compress_async_reports_stale_session_guard` now passes (verified). Closes umbrella #2361 partially. - **PR #2796** by @ai-ag2026 — Clear stale inflight UI state before starting a new send so blocked composer busy-state from failed/incomplete prior turns doesn't divert new turns into the invisible queue. Five-commit squashed fix: (1) drop stale optimistic sidebar rows once canonical session data arrives, (2) clear stale busy state before send via `_clearStaleBusyStateBeforeSend()`, (3) preserve server idle rows over stale optimistic local rows, (4) let `/api/chat/start` survive non-fatal pre-start UI errors via `_runOptionalPreStartUiStep()`, (5) keep those warnings console-only instead of throwing. Adds `_shouldKeepLocalOnlyOptimisticSessionRow()` in `static/sessions.js` and 8 new tests in `tests/test_inflight_send_start_race.py`. Closes #2795. Authorship preserved via `--author`. - **PR #2777** by @b3nw — Flush pending render before segment reset at tool/interim_assistant boundaries so live tokens that arrived in the 66ms rAF throttle window don't get lost from the DOM when `_resetAssistantSegment()` clears `assistantBody`. New `_flushPendingSegmentRender()` helper writes via `smd`, `renderMd`, or `esc` fallback (same paths as `_doRender`) only when `_renderPending` is true. Completed transcripts were never affected — `renderMessages` rebuilds from the full `assistantText` accumulator on `done`. Adds `tests/test_issue2713_streaming_segment_flush.py`. Closes #2713. - **PR #2778** by @b3nw — Reset reasoning accumulator per turn and prefer `reasoning_content` over `reasoning` on read. Two related bugs: (1) `reasoningText` was initialized once when the SSE stream opened and never reset between turns, so the `done` event would assign the union of every turn's reasoning to the last assistant message in multi-turn agent sessions; now reset at both turn boundaries (`tool` + `interim_assistant`). (2) `static/ui.js renderMessages` preferred `m.reasoning` (potentially corrupted by bug 1) over `m.reasoning_content` (the clean per-turn backend value); the fallback now reads `m.reasoning_content || m.reasoning`. Updates `tests/test_streaming_race_fix.py` to scope the reconnect-accumulator guard to the `_wireSSE` preamble only (turn-boundary resets inside event listeners are intentional). Adds `tests/test_issue2565_reasoning_accumulation.py`. Closes #2565. ## [v0.51.121] — 2026-05-24 — Release CS (stage-batch3 — 4-PR low-risk batch — state.db merge / display counts / compression marker / Windows launcher) ### Fixed - **PR #2788** by @Carry00 — Prevent `state.db` messages being silently dropped during sidecar merge. Two related bugs were combining to discard historical messages: (1) `get_state_db_session_messages()` was selecting `role, content, timestamp` but NOT `id`, so every row was assigned a `("legacy", ...)` merge key instead of `("message_id", ...)`; (2) when a WebUI-origin session was continued via another Hermes surface (Gateway, CLI), the reader was always hitting the *active* profile's `state.db` rather than the session's own profile. Symptom: a 189-message session showed only 50 in the WebUI. Fix: include `id` in the SELECT when the column exists, and accept an optional `profile=` arg so cross-profile reads use the right database. Both callers in `api/routes.py handle_get` now thread `profile=getattr(s, 'profile', None)` through. - **PR #2797** by @ai-ag2026 — Align messaging session display counts with deduped display messages. The `message_count` returned by `/api/session` is the display coordinate space used for pagination and the header badge. Messaging-thread `state.db` metadata can carry raw duplicate transport rows (blank assistant separators between Discord/Slack thread turns) that `_merged_session_messages_for_display()` intentionally dedupes for rendering. The advertised count was the raw row count, so the frontend expected phantom messages after dedupe — `len(display_msgs) < message_count` triggered "load older" UI states that immediately returned nothing. Fix: `raw["message_count"] = _merged_message_count` for messaging sessions, computed from the same merge that produced the displayed messages. Adds `tests/test_gateway_sync.py::test_messaging_session_message_count_matches_deduped_display_messages` covering the regression. - **PR #2803** by @simjak — Compression-summary cards no longer use ordinary tool output that merely mentions context compression. The streaming auto-compression path was using a local broad substring matcher that fired on any message containing the strings "context compaction" / "context compression" / "context was auto-compressed" / "active task list was preserved across context compression", including skill/tool JSON output and ordinary user discussion about compaction. The strict predicate at `api/compression_anchor._is_context_compression_marker()` was already correctly scoped to synthetic marker prefixes on non-tool messages. Fix: expose the strict predicate as `is_context_compression_marker()` (public name) and route `api/streaming._is_context_compression_marker` through it as a backward-compatible alias. Tool/skill output that mentions compression no longer seeds `compression_anchor_summary` cards. ### Added - **PR #2783** by @Koraji95-coder — Native Windows launcher and community-guide README link (squashed from 3 commits). `start.ps1` is a PowerShell equivalent of `start.sh` that bypasses `bootstrap.py`'s `ensure_supported_platform()` refusal and invokes `server.py` directly on native Windows. It mirrors `start.sh`'s discovery (load optional `.env` with the same readonly-var filter for `UID`/`GID`/`EUID`/`EGID`/`PPID`, find Python via `HERMES_WEBUI_PYTHON` env → `python3` → `python` → `py`, validate `HERMES_WEBUI_AGENT_DIR` on disk before use, prefer the agent's `venv\Scripts\python.exe`, set `HERMES_WEBUI_HOST` / `HERMES_WEBUI_PORT` / `HERMES_WEBUI_STATE_DIR` / `HERMES_HOME` defaults). The README adds a community-maintained native Windows setup section pointing to @markwang2658's `hermes-windows-native-guide` and `hermes-windows-native` repos with the documented memory delta (~330 MB native vs ~1080 MB WSL2+Docker). Closes both halves of #1952. Assumes Python + agent venv are already set up — first-time setup still needs WSL2 once to create the venv (`bootstrap.py` still refuses on native Windows). ## [v0.51.120] — 2026-05-24 — Release CR (stage-batch2 — 3-PR low-risk batch — Bedrock provider / update check past-tag / CORS preflight) ### Added - **PR #2786** by @munim — Surface AWS Bedrock as a configurable provider in the WebUI model picker. `api/config.py` registers `"bedrock": "AWS Bedrock"` in `PROVIDER_LABELS`, adds 6 default Bedrock model IDs (Claude Opus 4.7 / 4.6 / 4.5, Sonnet 4.6 / 4.5, Haiku 4.5) to `DEFAULT_MODELS["bedrock"]`, and teaches `_build_configured_model_badges()` to detect Bedrock when both `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are present (IAM-style auth, not single-API-key). Static fallback list is overridden at runtime by `hermes_cli.models.provider_model_ids("bedrock")` when the live AWS model list is reachable. Adds `tests/test_issue2720_bedrock_model_picker.py` with 11 test cases covering registry, defaults, env-detection, and runtime override. Resolves #2720. ### Fixed - **PR #2789** by @munim — Update check no longer falsely reports "Up to date" when HEAD has moved hundreds of commits past the latest tag. The hermes-agent repository keeps committing to master between tagged releases, and the old `_check_repo_release()` returned `behind=0` (since `current_tag == latest_tag`) and stopped — so the user saw "Up to date" while the working tree was hundreds of commits behind. The fix: when `behind == 0`, run `git describe --tags --always`; if the result contains the `-N-gSHA` suffix (HEAD past tag), return `None` so `_check_repo_branch()` runs and reports the real commit gap. Adds 8 new test cases in `tests/test_updates.py` covering past-tag detection, equal-tag-and-HEAD pass-through, untagged-repo behavior, and the agent-cadence #2653 scenario. Resolves #2653. - **PR #2790** by @weidzhou — Add `do_OPTIONS()` handler in `server.py` so CORS preflight requests return `200 OK` with appropriate `Access-Control-Allow-*` headers instead of `501 Not Implemented`. Browsers sending a preflight OPTIONS for cross-origin API calls previously hit the BaseHTTPRequestHandler default and the entire CORS exchange was blocked. The handler narrowly responds only to OPTIONS — no broader CORS posture change to other endpoints. Resubmit of closed #2750 (which bundled unrelated session-index changes); this PR is the minimal preflight-only split that @nesquena-hermes and @AJV20 requested. ## [v0.51.119] — 2026-05-24 — Release CQ (stage-batch1 — 3-PR low-risk batch — tool cards / 404 recovery / Hepburn skin) ### Fixed - **PR #2801** by @ai-ag2026 — Preserve settled tool cards across stream completion. The streaming `done` handler now derives anchored settled tool cards from message-level tool metadata (`message.tool_calls`, `message._partial_tool_calls`, or `content[].type === 'tool_use'`) when present, instead of unconditionally falling back to session-level `d.session.tool_calls`. The fallback could overwrite the per-message anchors after pagination/windowing because session-level coordinates may not line up with the active message array, causing tool cards to disappear on the final `done` render. Fixes #2613, complements #2777 (which covers pending-segment flushes at tool/interim boundaries). Adds `tests/test_streaming_markdown.py::test_done_handler_prefers_message_tool_metadata_for_settled_render` to lock the precedence. - **PR #2808** by @chouzz — Recover deterministically from boot-time `/session/{id}` 404s (Option A for #2798). When `loadSession()` hits a 404 during boot-time restore (`!currentSid`), `static/sessions.js` now always clears `localStorage['hermes-webui-session']`, strips the stale URL with `history.replaceState(null, '', '/')`, and rethrows so boot falls through to empty-state recovery. The previous condition required the stale id to match `localStorage`, so a stale `/session/{id}` URL with empty `localStorage` (post state-reset) could leave the UI stuck on "Session not available in web UI." Fixes #2798. ### Added - **PR #2799** by @gavinssr — Add Hepburn skin (magenta-rose palette derived from the Hepburn TUI theme). Full light + dark palette under `:root[data-skin="hepburn"]` / `:root.dark[data-skin="hepburn"]`, registered in `static/boot.js` `_SKINS` and whitelisted in `static/index.html`'s inline skin gate. As part of this PR `loadSettingsPanel()` in `static/panels.js` now prefers `localStorage.getItem('hermes-skin')` over `settings.skin` when populating the skin picker (DOM truth → settings fallback), so the picker matches what the user actually sees after the inline gate has already resolved legacy aliases. ## [v0.51.118] — 2026-05-22 — Release CP (stage-pr2773 — 1-PR hotfix — v0.51.117 brick fix: chat input restored) ### Fixed - **PR #2773** by @nesquena-hermes — fix(chat): rename `_inflightStateLimits()` in `static/ui.js` to `_getInflightStateLimits()` so it no longer collides with the `window._inflightStateLimits` config object set in `static/boot.js`. Closes #2771. The v0.51.117 in-flight-recovery quota fix (#2766) declared a top-level helper with the same name as a window-attached config object; because top-level `function foo(){…}` declarations in classic (non-module) scripts attach to `window`, boot.js's `window._inflightStateLimits = {…}` assignment overwrote the function reference before any session could send. Every new chat broke on first `send()` with `TypeError: _inflightStateLimits is not a function`, leaving v0.51.117 effectively unusable. Renamed the function only (the public-ish window key is unchanged) and updated all 4 call sites. **New regression test `tests/test_window_function_collision.py` scans every static JS file for top-level `function NAME()` declarations whose name is also the target of `window.NAME = {…}` / `= `, the exact shape that broke #2715 (`_pinnedSessionsLimit` in v0.51.106) and #2771 (`_inflightStateLimits` in v0.51.117). The test fails loudly with a precise file:name diagnostic if the bug class returns. Verified end-to-end against the live browser before merge: `_getInflightStateLimits()` returns the limits object and `saveInflightState()` persists to localStorage without throwing. ## [v0.51.117] — 2026-05-22 — Release CO (stage-pr2766 — 1-PR — in-flight recovery storage quota-safe) ### Fixed - **PR #2766** by @george-andraws — Make in-flight recovery storage quota-safe so a full browser `localStorage` budget no longer blocks chat submission with `QuotaExceededError: Failed to execute 'setItem' on 'Storage': Setting the value of 'hermes-webui-inflight' exceeded the quota.` The fix has four parts: (1) compact recovery snapshots before writing to keep only recent messages, tool calls, uploaded-file metadata, stream id, and updated timestamp; (2) truncate individual large strings per field (`60000` chars default) and prune the serialized payload (`1500000` chars default); (3) catch quota errors specifically on `markInflight()`, drop the larger `hermes-webui-inflight-state` key, then retry the tiny marker write; (4) expose 5 new configurable settings (`inflight_state_max_sessions`, `_max_messages`, `_max_tool_calls`, `_max_string_chars`, `_max_json_chars`) with int-range validation so operators can tune the budget. Self-healing on the very first chat submit after upgrade for users with already-quota-exhausted storage — no manual reload required. Graceful degradation if storage is still too full even after compaction (clears recovery snapshots but never blocks chat submit). ## [v0.51.116] — 2026-05-22 — Release CN (stage-pr2676 — 1-PR — per-skill enable/disable toggle in Skills panel, CLI-parity with `hermes skills config`) ### Added - **PR #2676** by @lucasrc — Each skill in the Skills panel now has a toggle pill (enabled/disabled) so users can turn individual skills on or off directly from the WebUI without editing `config.yaml`. Achieves parity with the existing `hermes skills config` CLI subcommand (interactive TUI that toggles `skills.disabled` in config). The disabled state is mirrored through to `skills.platform_disabled.webui` when that key is present. Disabled skills remain visible in the panel (muted via `opacity: .45`) instead of being filtered out, so users can re-enable them later. New endpoint: `POST /api/skills/toggle` validates the skill exists in the filesystem before mutating config, wraps the YAML read-modify-write under the existing `_cfg_lock` for thread safety, and calls `reload_config()` so the change takes effect immediately. Toggle pill uses theme variables (`--accent-bg-strong`, `--accent`, `--border`, `--muted`, `--accent-text`) so it adapts automatically to each skin: gold for default, red for ares, blue for poseidon, purple for sisyphus, grey for mono — verified empirically across light + dark variants. i18n keys (`skill_enabled`, `skill_disabled`, `skill_toggle_failed`) translated across all 10 locales. Default-state safety: fresh installs (no `skills.disabled` key in config) return `disabled: False` for every skill — no regression risk for new users. ## [v0.51.115] — 2026-05-22 — Release CM (stage-pr2731 — 1-PR — clarify prompt collapse/expand with chevron-icon polish) ### Added - **PR #2731** by @Michaelyklam — Clarification prompts now include a compact Collapse/Expand control so users can temporarily shrink a blocking decision card and reread the chat context behind it before responding. The toggle uses Lucide chevron icons (chevron-down expanded → click to collapse, chevron-up collapsed → click to expand) and a small circular pill matching the existing composer-button design language. The collapsed card sits cleanly above the composer at every tested viewport (desktop 1920×1080, mobile iPhone 14 390×844) without edge clipping. New clarification prompts still open expanded so users notice them. ## [v0.51.114] — 2026-05-22 — Release CL (stage-407 — 1-PR — update-check recovery from remote re-tags) ### Fixed - **PR #2758** by @nesquena-hermes — fix(updates): pass `--force` to `git fetch --tags` in `api/updates.py` so the WebUI's release-tracking update check can recover from a remote re-tag (e.g. a release tag that was force-pushed to a new commit after a squash-merge). Without `--force`, plain `git fetch origin --tags` returns `! [rejected] vX.Y.Z (would clobber existing tag)` and the entire update path (check, force-apply, normal-apply) jams indefinitely — neither the periodic check nor manual "Check now" nor the Update button can recover. Three fetch call sites were patched (`_check_repo`, `apply_force_update`, `apply_update`) to use `--tags --force`; the WebUI never pushes tags, so deferring to the remote's view is the right contract. Closes #2756. ## [v0.51.113] — 2026-05-22 — Release CK (stage-406 — 1-PR — composer model picker lag fix + hard-refresh recovery) ### Fixed - **PR #2743** by @franksong2702 — Composer model picker now opens immediately from the existing static option list while the dynamic `/api/models` catalog hydrates in the background, instead of blocking the click on the catalog request. A just-selected session model also survives a hard refresh that interrupts the async `/api/session/update` POST: the selection is staged into `sessionStorage` (keyed by session_id, 10-minute TTL) before the async update flies, and `loadSession()` re-applies the pending pick on next session restore and retries the persistence call. Tests pin the new ordering: visible picker render before `await`, pending-state save before `await api('/api/session/update')`, and pending-state replay before the first `syncTopbar()` projects server metadata. ## [v0.51.112] — 2026-05-22 — Release CJ (stage-405 — 1-PR — session model authoritative across restore) ### Fixed - **PR #2737** by @ai-ag2026 — Keep the session model authoritative when a restored session is reactivated. Previously, stale browser-cached picker state could override an active conversation's model in four scenarios: (1) on initial boot when `localStorage` had a different model preference than the active session, (2) on hard refresh when `S._bootReady` revealed the composer chip before the live catalog hydrated, (3) when the session's model wasn't in the current provider catalog (the static/default fallback silently rewrote `S.session.model`), (4) when starting a new session whose model wasn't in the static HTML dropdown. The fix: `loadSession()` now requests `resolve_model=1` so backend normalization happens synchronously with metadata; boot model hydration prefers the active session over `localStorage`; hard refresh re-runs the model dropdown hydration before `_bootReady`; a new `_ensureModelOptionInDropdown()` helper injects a `data-custom='1'` option for models not in the catalog instead of silently rewriting `S.session.model` to the default. 100 LOC of new pytest regression coverage pinning each behavior. ## [v0.51.111] — 2026-05-22 — Release CI (stage-404 — 1-PR — keep state.db replays out of sidecar tail) ### Fixed - **PR #2746** by @ai-ag2026 — Prevent replayed state.db rows from being appended after an already-correct sidecar transcript tail. `merge_session_messages_append_only()` previously tried to skip state.db rows replaying the sidecar, but two edge cases leaked through: (1) the final row of a replayed sidecar prefix was not skipped because the replay index had reached the sidecar sequence length, and (2) a replayed middle segment was not considered prefix replay, so old state.db rows could be appended after the saved assistant tail. That made `/api/session` appear to end on an old user prompt even when the saved sidecar already ended on the real assistant answer. The fix tracks per-(role, content) visible-occurrence counts in the sidecar and uses that as a replay budget when comparing state.db rows; legitimate repeated messages from state.db are still preserved. `_has_visible_duplicate()` is kept as a thin wrapper around the new `_matching_visible_duplicate()` for backwards compatibility. Regression test covers both full-replay and middle-segment replay shapes. ## [v0.51.110] — 2026-05-22 — Release CH (stage-403 — 2-PR batch — default personality from config + sort configured providers to top) ### Added - **PR #2747** by @s010mn — `new_session()` now reads `display.personality` from `config.yaml` as the default for new conversations. Previously every new session started with `personality=None` and required an explicit `/personality ` slash command. Values `'none'`, `'default'`, `'neutral'`, and empty string are treated as no-personality. Case-insensitive — `personality: Taleb` normalizes to `taleb`. Config-read is wrapped in try/except so malformed config falls back to the prior behavior rather than crashing session creation. The `/personality` slash command still works for per-session overrides. - **PR #2683** by @jasonjcwu — Sort providers so configured/custom entries appear first in both the model picker dropdown (`api/config.py::get_available_models`) and the Settings providers panel (`api/providers.py::get_providers`). Priority order: (1) the active provider, (2) `custom:*` providers from `custom_providers` config, (3) providers with configured API keys (credential pool or `config.yaml`), (4) all others alphabetical. Eliminates scrolling past 25+ unconfigured providers to find the one in active use. ## [v0.51.109] — 2026-05-22 — Release CG (stage-402 — 2-PR batch — sidebar action menu click stability + chat panel sidebar resync after navigation) ### Fixed - **PR #2741** by @ai-ag2026 — Keep the sidebar conversation actions menu open while session-list refreshes, stream updates, or panel-resync repairs arrive. Previously the three-dot menu beside chat titles could be torn down before the user finished clicking it because `renderSessionListFromCache()` rebuilt the row DOM (and the fixed-position menu's anchor) without checking whether the menu was open. The new early-return at the top of the refresh keeps the menu stable; destructive menu actions explicitly close the menu before they fire, so dismissal still works as expected. - **PR #2736** by @ai-ag2026 — Resync the chat sidebar after returning from Settings/Logs/other panels. The session list is virtualized, and the browser can clamp the preserved scrollTop during a panel transition; without a render after the chat view is visible again, stale virtual spacer/header DOM remained until the next manual scroll. The new `_resyncChatSidebarAfterPanelSwitch()` helper runs one guarded `requestAnimationFrame` after the panel becomes visible, bails if a rename input or action menu is open, and uses no polling. ## [v0.51.108] — 2026-05-22 — Release CF (stage-401 — 4-PR batch — session-index dedup + update-check diagnostic redaction + handoff-summary sqlite connection leak fix + Slice 4d runner route gate docs) ### Added - **PR #2744** by @Michaelyklam — #1925 RuntimeAdapter RFC follow-up: marks the `runner-local` selection seam shipped in v0.51.105 and defines the next Slice 4d supervised runner route gate before live chat can move onto a runner backend. The gate keeps runner routing default-off, preserves legacy fallback and public response shapes, and carries the Runtime API gap matrix forward so active-run discovery, session→run lookup, command metadata, artifacts, and provider/tool routing do not become private WebUI runtime replicas. ### Fixed - **PR #2738** by @weidzhou — `_write_session_index()` full-rebuild path now deduplicates entries by `session_id`. When old-format `session_*.json` files coexist with WebUI-format `xxx.json` files sharing the same `session_id`, the index produced duplicate Vue `:key` entries and crashed the frontend with a blank page. The lazy rebuild now uses `dict[session_id → compact_entry]` keyed on session_id, with the higher `message_count` entry winning on conflict. - **PR #2730** by @ashbuildslife — Sanitize git fetch diagnostics before returning update-check errors to the browser. New `_sanitize_git_diagnostic()` in `api/updates.py` strips credentialed URL userinfo (`user:token@host`), GitHub token shapes (`ghp_*`, `gho_*`, `github_pat_*`), and secret-looking query parameters (`?access_token=`, `?token=`, `?password=`, `?auth=`, `?key=`), then caps the message at 300 characters. Empirically verified that plain `https://github.com/owner/repo.git` URLs and SSH-style `git@host:owner/repo` remotes pass through untouched — only credentialed shapes are redacted. Update-check failure context (e.g. `Authentication failed`, network errors) is preserved. - **PR #2742** by @Isla-Liu — Per-turn SQLite connection leak in handoff-summary path (#2233). Two functions on the `/api/session/handoff-summary` hot path were opening `sqlite3.connect(...)` inside a bare `with` statement, which commits the transaction at scope exit but does NOT close the connection. Per-turn invocations accumulated `state.db`/`state.db-wal` file descriptors and CPython heap pages on long-lived worker threads, surfacing as multi-GB VmRSS / 6× duplicated state.db fds on long-running installs. Wrapped both call sites with `contextlib.closing(...)` (already imported and used at 7 other sites in the same files) so the connection is closed deterministically: `api/models.py::count_conversation_rounds` and `api/routes.py::_persist_handoff_summary_to_state_db`. Regression test loops both functions 20× against a tmp `state.db` and asserts `/proc//fd` count does not grow more than 2. Live soak: fd growth = 0, VmRSS growth = 0 KB across 20 POSTs. ## [v0.51.107] — 2026-05-21 — Release CE (stage-400 — 8-PR batch — pinned-sessions-limit getter rename + uploaded-file user-turn dedupe + active-run repair guard + incremental KaTeX streaming + profile default model on fresh boot + French locale completion + update-check error surfacing + release-update apply path) ### Fixed - **PR #2718** by @eslicarrillo — Follow-up to v0.51.105's #2700: rename `_pinnedSessionsLimit()` to `_getPinnedSessionsLimit()` so the helper matches the rest of `sessions.js`'s `_get*()` naming convention for accessors. No behavior change. - **PR #2723** by @ai-ag2026 — Deduplicate uploaded-file user turns when the optimistic browser bubble uses plain text but the server-persisted pending turn includes the `[Attached files: ...]` suffix. Previously a turn with attachments could render twice (optimistic + persisted) in the visible transcript before reconciling. - **PR #2721** by @ai-ag2026 — During session repair on restart, treat sessions with a live in-flight run as active. The prior code path could prune restart-stale state for sessions that were actually mid-stream when the server bounced, dropping the resumable run. Now the active-stream check gates the prune so live runs survive a restart cleanly. - **PR #2710** by @Michaelyklam — Render streamed math (KaTeX) incrementally during a stream so completed expressions render in place as their closing delimiter arrives, instead of all-at-once at the end of the turn. Eliminates the visual flash where prose-with-math sat as raw `\\[...\\]` markup until the final render pass. - **PR #2709** by @starship-s — On a fresh boot with no persisted model in localStorage, prefer the active profile's configured default model over the static HTML option. Previously a clean install / first-load could surface the placeholder model until the user manually picked a different one, even when the profile had a default configured. Behavior note: the boot path now calls `_clearPersistedModelState()` on each load when no profile default is found, so a previously-persisted user pick is wiped on refresh — this is intentional (matches the PR's "profile default wins on fresh boot" intent and is ratified by `tests/test_model_default_boot_precedence.py`). - **PR #2722** by @victorwhale — Complete French (`fr`) locale coverage: +93 missing translation keys covering Settings, profile-ops, gateway tile, skills modal, session controls, and i18n test surfaces. Coverage 88.8% → 96.7%. - **PR #2717** by @ai-ag2026 — Surface update-check fetch errors in the UI instead of failing silently. The background `api/updates/check` request previously swallowed network failures, so an offline / blocked-CDN scenario showed no indication that the version banner couldn't render. Now the failure is logged and exposed to the System panel's update-status card. - **PR #2719** by @ai-ag2026 — Apply release-update target correctly when the user clicks "Check for updates" after a prior dismissal: clears the `sessionStorage` check-once stamp and forces banner re-evaluation. The prior path silently no-op'd because the once-per-tab guard fired before the explicit user click could re-trigger the fetch. ## [v0.51.106] — 2026-05-21 — Release CD (stage-399 — 3-PR batch — restamped state.db replay dedupe + context_messages dedupe so agent doesn't see duplicates + empty _partial bloat fix) ### Fixed - **PR #2686** by @ai-ag2026 — Prevent `/api/session` display merges from appending restamped `state.db` replay rows after the sidecar tail when those rows are already visible in the sidecar. Compressed sessions previously could appear to end on an old user prompt even though the assistant answer was persisted earlier in the transcript. The fix deduplicates by visible role+content even when timestamps drift (coarse sidecar seconds vs newer state.db floats), preserves the sidecar assistant tail across compaction-card variants and tool-metadata drift, and handles workspace-prefix user prompt variants. Regression test covers the full surface. - **PR #2705** by @AlexeyDsov — Deduplicate replayed context messages before they reach the agent so the model no longer sees the same conversation row twice. The UI-side fix shipped in v0.51.96's #2620 corrected the display transcript but the agent still received duplicates in its context (no on-disk duplication — only at runtime in the model-facing context). Starting from the 2nd turn in any session, duplicates would cause the agent to repeat itself or list items twice. The new dedup pass runs at the WebUI/agent boundary so the runtime context is canonical regardless of upstream replay shape. - **PR #2704** by @wirtsi — Prevent unbounded `_partial` message accumulation in session files. Two interacting bugs in `cancel_stream()` and `_message_identity()` produced multi-GB session JSON growth: (1) the `_partial_already_present` dedup check was gated on `if _stripped:`, but reasoning-only cancellations have empty stripped text so every cancel inserted a new identical empty `_partial` entry; (2) `_message_identity()` returned `None` for empty `_partial` messages so the merge layer had no way to spot the duplicate. The fix tightens both paths and adds a regression test that replays the cancel-cycle to assert bounded growth. Closes the OOM crash class reported against long-running reasoning-heavy sessions. ## [v0.51.105] — 2026-05-21 — Release CC (stage-398 — 4-PR batch — hide suggestions preference + Docker agent version from copied source + runner-local adapter selection + configurable pinned session limit) ### Added - **PR #2687** by @Michaelyklam (closes #2679) — Settings → Preferences gains a `Hide chat suggestions` toggle (config key `hide_empty_state_suggestions`). The empty new-chat screen normally shows three suggestion buttons as first-class tap targets, which causes accidental taps on mobile. Users who don't want the suggestions can hide them via the preference; the toggle persists across sessions and reloads. Default is OFF (suggestions remain visible) so existing users see no change. - **PR #2700** by @ai-ag2026 — Settings → Preferences gains a `Pinned conversations limit` numeric input. Builds on v0.51.96's #2614 3-cap by making the cap configurable (range 1–99, default 3, validated server-side via `_SETTINGS_INT_RANGES`). Backend validates the new cap on read, surfaces an error if a pin attempt would exceed it, and the right-click menu disables the pin item with an explanatory tooltip when the cap is reached. Default-3 keeps existing users on identical behavior. - **PR #2696** by @Michaelyklam — RuntimeAdapter slice 4c — feature-flagged runner backend selection. The existing `HERMES_WEBUI_RUNTIME_ADAPTER` env var gains a new `runner-local` mode that wires up a `RunnerRuntimeAdapter` factory and adds a restart/reattach harness gate before the runner backend is used at the dispatcher. No user-visible change in this slice — unset / `legacy-direct` keeps existing behavior intact, and no production caller wires the new adapter yet. The slice exists so future work can land a sidecar runner without changing the runtime contract for existing users. ### Fixed - **PR #2703** by @Michaelyklam (closes #2691) — System panel now detects the Hermes Agent version in Docker two-container deployments where the WebUI sees a copied Agent source volume instead of a live git checkout. The new detection cascade reads `VERSION` if present, falls back to the package metadata (`hermes_cli`), and finally to a `.git` describe if either is available, so the System panel reports the right version even when both `VERSION` and `.git` are absent in the copied source. ## [v0.51.104] — 2026-05-21 — Release CB (stage-397 — 9-PR batch — i18n zh-CN/zh-TW cron status + geist-contrast skin polish + tablet hardware Enter + stale Codex slash model state + SSE reconnect jitter + cron run inline expansion + inflight send race + new-chat model provider sync + virtualized sidebar scroll-clamp resync + transcript cache invalidation on same-count content) ### Fixed - **PR #2690** by @laiaman — Correct the zh-CN and zh-Hant translations for the `cron_status_active` label so it reads "enabled / scheduled" (`已启用` / `已啟用`) instead of "running" (`运行中` / `活躍中`). The English source is "active" (enabled, scheduled), and the prior Chinese strings conflated it with the separate `cron_status_running` "currently executing" state, making both labels look identical when a job was both scheduled and not currently firing. - **PR #2701** by @jasonjcwu — Geist-contrast skin composer polish: force `--user-bubble-text` to `#111` in light mode so typed text is black on the light input background; hide the textarea scrollbar to match the rest of the skin; recolor the send button so it reads correctly against the contrast palette. - **PR #2706** by @dobby-d-elf — Tablet (iPad-class) devices with an attached hardware keyboard now send on Enter and newline on Shift+Enter, matching desktop behavior. The prior touch-primary check forced Enter→newline on every touch device, but tablets with hardware keyboards have a physical Shift key and should follow the desktop contract. Detection uses `matchMedia('(pointer:coarse)')` + a `window.visualViewport` height-delta probe (>120px shrink = software keyboard open) so an iPad with hardware keyboard (viewport not shrunk) treats Enter as send, while a phone tapping into the composer (soft keyboard shrinks the viewport) keeps Enter as newline. Falls back to the legacy touch behavior when `visualViewport` is unavailable. - **PR #2684** by @ai-ag2026 — Repair stale `openai/...` slash-qualified model IDs when the active/session provider is `openai-codex`. A stale browser/localStorage selection of `openai/gpt-5` against an `openai-codex` provider previously routed the chat to OpenAI directly instead of through Codex. The cross-provider model-switch resolver now detects the mismatch and re-resolves the model to the matching `codex/...` ID before the request goes out. Explicit OpenRouter slash-qualified selections continue to fast-path through unchanged. - **PR #2671** by @AJV20 (closes #2629 + #2661) — Session-list SSE reconnects now use bounded jitter/backoff (each retry delay is `base*0.75 + random*(base*0.35)` where `base = min(30000, 5000 * 2^attempt)`, capped at 30s) instead of a fixed 5-second retry, so tabs that all dropped at the same time (server restart, network drop) don't all retry in lockstep. Expanded cron run rows now render the full output inline immediately on click; the truncated preview remains only for collapsed rows, and the full-output fallback no longer drops content when Markdown rendering is unavailable. - **PR #2689** by @ai-ag2026 — Preserve the optimistic in-flight message array across the `/api/chat/start` await window so a fast back-to-back send doesn't clear the user's message before the stream ID arrives. The fix snapshots the inflight entry before the await, recreates it if a sidebar/session refresh pruned it during that window, and skips stale-inflight cleanup for the submitting session until a stream ID is bound. Regression test covers the race. - **PR #2674** by @AJV20 — Resync the new-chat model picker when the server-created session has the same model ID as the current dropdown but a different provider. New conversations now resync to the configured default model provider instead of inheriting a stale persisted picker selection (e.g. `openai/gpt-5` from a previous session). Without this, the dropdown text matched the new session's model, but the provider attribute still pointed at the stale choice. - **PR #2688** by @ai-ag2026 — Resync the virtualized session sidebar after restoring a saved scroll position if the browser clamps or rejects that scroll position. Without this, date-group headers could render without their session rows beneath them until the user manually scrolled or a later refresh recomputed the virtual window. Regression test pins the recompute path. - **PR #2692** by @ai-ag2026 (refs #2613) — Invalidate the transcript render cache on same-count content changes, not just on count changes. The prior cache key was `(message_count, render_window_size)`, which silently reused a cached transcript whenever a same-count edit produced visibly different content (e.g. a tool retry that replaces a single assistant message with corrected text). The new cache signature folds a content hash into the key so any visible change forces a fresh render. Regression test asserts cache-bust on same-count content swap. ## [v0.51.103] — 2026-05-21 — Release CA (stage-396 — 1-PR follow-on — Settings → Plugins distinguishes exclusive/provider activation) ### Fixed - **PR #2663** by @Fail-Safe (closes #2659) — Settings → Plugins panel now distinguishes exclusive plugins (memory providers, web backends, browser providers activated via `.provider` config) from disabled-or-broken plugins. The `/api/plugins` payload gains `kind` + `activation` fields; cards render a new "Active (provider)" badge variant for exclusive activation instead of mislabeling these plugins as "Disabled / No registered lifecycle hooks". Purely additive — for users without an exclusive provider configured, the panel renders "Enabled" and "Disabled" badges exactly as before. The legacy `enabled` boolean is preserved on the payload for back-compat with older WebUI clients; new clients read `activation` first with a fallback. 3 new behavioral tests cover the exclusive, model-provider, and standalone code paths. ## [v0.51.102] — 2026-05-21 — Release BZ (stage-395 — 1-PR follow-on — capped CLI sidebar candidate window now keyed on last-activity not start time) ### Fixed - **PR #2662** by @Michaelyklam (closes #2656) — Capped CLI/agent sidebar scans now order the candidate CTE by `COALESCE(MAX(messages.timestamp), s.started_at)` instead of `s.started_at` alone. Long-lived CLI sessions that were resumed days later (old `started_at`, recent message activity) stay visible in the candidate window instead of falling outside the 8×limit oversample. Closes the regression I filed against v0.51.99's #2647 sidebar candidate-window narrowing. New regression test creates an old session with a recent message timestamp and asserts it surfaces at the top. ## [v0.51.101] — 2026-05-20 — Release BY (stage-394 — 2-PR deep-review batch — workspace Git backend + sidebar tab visibility toggle) ### Added - **PR #2625** by @stocky789 — Add backend Git operations for the workspace panel. New `api/workspace_git.py` module exposes read-only ops (`/api/git/status`, `/api/git/branches`, `/api/git/diff`, `/api/git/commit-message[-selected]`) unconditionally and mutating ops (`stage`, `unstage`, `discard`, `commit`, `commit-selected`, `checkout`, `stash-checkout`, `pull`, `push`) only when `HERMES_WEBUI_WORKSPACE_GIT_DESTRUCTIVE=1` is set in the environment — default OFF so existing deployments are unaffected. All subprocess calls use `["git", *args]` with `shell=False`, all branch/ref names go through `git check-ref-format --branch` validation before flowing to `git switch -c`, and `subprocess.env` is scrubbed of `GIT_DIR`/`GIT_WORK_TREE`/`GIT_CONFIG_GLOBAL`/`GIT_CONFIG_SYSTEM`/`GIT_CONFIG_COUNT`/`GIT_CONFIG_PARAMETERS` plus the full `GIT_CONFIG_KEY_*`/`GIT_CONFIG_VALUE_*` namespace before every invocation. `GIT_INDEX_FILE` is intentionally preserved to drive selected-file commits through a private temporary index. Paths are bound to the workspace root via `safe_resolve_ws()` + `Path.relative_to()` enforcement (rejects `..` traversal and symlinked escapes); active-stream gate prevents mutations during a running agent turn. Documented in `docs/workspace-git.md` with the full trust model (hooks-as-RCE warning, default-allowed vs gated lists, env-scrub enumeration). Frontend UI ships in a follow-up PR. - **PR #2636** by @FrancescoFarinola — Per-tab sidebar visibility toggle in Settings → Appearance. Power users can hide unused rail tabs (Tasks, Kanban, Skills, Memory, Spaces, Profiles, Todos, Insights, Logs) while keeping Chat and Settings always reachable. Settings is per-profile so each profile can have its own hidden-tabs preference; an inline `