diff --git a/.github/workflows/docker-smoke.yml b/.github/workflows/docker-smoke.yml new file mode 100644 index 00000000..66de8488 --- /dev/null +++ b/.github/workflows/docker-smoke.yml @@ -0,0 +1,208 @@ +name: Docker smoke + +# Runtime smoke gate for Docker init logic. +# +# Background: v0.51.84 (PR #2470) shipped a startup-killing :ro mount + chown +# interaction (EROFS under `set -e`) that 9 source-level pytest invariants + +# 5800+ existing tests all passed. The independent reviewer caught it by eye. +# This workflow closes that class of gap by actually `docker compose up`-ing +# each variant against a real Docker daemon on the GHA runner. +# +# Scope (intentionally small for v1): +# - 3 compose variants (single, two-container, three-container) +# - For multi-container variants, rebuild the local Dockerfile and re-tag +# it as ghcr.io/nesquena/hermes-webui:latest BEFORE `up` so the PR's +# changes to docker_init.bash / Dockerfile actually execute. Without this +# the multi-container variants would pull the previous release from GHCR +# and silently miss every PR-level regression. +# - Pre-flight `docker compose config` job to catch schema/interpolation drift. +# - Reaper before each smoke run + trap on EXIT for orphan defence. +# +# Out of scope for v1 (per design review): +# - HERMES_WEBUI_SMOKE_TEST env flag in docker_init.bash (production-code footgun) +# - --user 60000:60000 (skips the chown branch we're protecting against) +# - Hadolint / yamllint (separate lint workflow, follow-up PR) +# - Local-runnable scripts/docker-smoke-test.sh (ship CI first, then iterate) +# - Podman runtime smoke (defer until a podman-specific bug ships) + +on: + pull_request: + branches: [master] + paths: + - 'Dockerfile' + - 'docker_init.bash' + - 'docker-compose*.yml' + - '.dockerignore' + - '.env.docker.example' + - '.github/workflows/docker-smoke.yml' + push: + branches: [master] + paths: + - 'Dockerfile' + - 'docker_init.bash' + - 'docker-compose*.yml' + - '.dockerignore' + - '.env.docker.example' + - '.github/workflows/docker-smoke.yml' + workflow_dispatch: + +# Fork PRs run with no secrets — that's the right model. Pin to least privilege. +permissions: + contents: read + +jobs: + compose-config: + name: Compose config validation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Validate every compose file parses + run: | + set -euo pipefail + for f in docker-compose.yml docker-compose.two-container.yml docker-compose.three-container.yml; do + echo "::group::compose config: $f" + docker compose -f "$f" config > /dev/null + echo "::endgroup::" + done + + smoke: + name: Smoke ${{ matrix.variant }} + runs-on: ubuntu-latest + needs: compose-config + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + variant: + - single + - two-container + - three-container + steps: + - uses: actions/checkout@v4 + + - name: Resolve compose file + project name + id: vars + run: | + set -euo pipefail + case "${{ matrix.variant }}" in + single) + echo "compose_file=docker-compose.yml" >> "$GITHUB_OUTPUT" + ;; + two-container) + echo "compose_file=docker-compose.two-container.yml" >> "$GITHUB_OUTPUT" + ;; + three-container) + echo "compose_file=docker-compose.three-container.yml" >> "$GITHUB_OUTPUT" + ;; + esac + # Per-run project name so concurrent jobs / reruns can't clobber each other. + echo "project=hermes-smoke-${{ matrix.variant }}-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_OUTPUT" + + - name: Reap any prior hermes-smoke resources on this runner + run: | + set -euo pipefail + # Hosted GHA runners are fresh, so this is mostly defence-in-depth for + # self-hosted runner re-use. We rely primarily on the unique per-run + # project name + `compose down -v --remove-orphans` in the EXIT trap + # to clean up the resources THIS run creates; this step only sweeps + # leftovers from prior runs that crashed before their trap fired. + # Match by project-name prefix instead of labels (the compose files + # don't carry hermes-smoke labels on their resources). + for c in $(docker ps -aq --filter "name=hermes-smoke-"); do + docker rm -f "$c" || true + done + for v in $(docker volume ls -q | grep "^hermes-smoke-" || true); do + docker volume rm -f "$v" || true + done + for n in $(docker network ls --format '{{.Name}}' | grep "^hermes-smoke-" || true); do + docker network rm "$n" || true + done + + - name: Build local Dockerfile + # We always build the local Dockerfile so the PR's changes are tested, + # even on the multi-container variants whose compose files reference + # ghcr.io/nesquena/hermes-webui:latest. Without this retag, multi-container + # smoke runs would test the previous release, not the PR. + run: | + set -euo pipefail + docker build -t ghcr.io/nesquena/hermes-webui:latest . + + - name: Prepare ephemeral host paths + id: paths + run: | + set -euo pipefail + STATE_DIR="$(mktemp -d -t hermes-smoke-state-XXXXXX)" + WORK_DIR="$(mktemp -d -t hermes-smoke-work-XXXXXX)" + echo "state_dir=$STATE_DIR" >> "$GITHUB_OUTPUT" + echo "work_dir=$WORK_DIR" >> "$GITHUB_OUTPUT" + echo "Allocated:" + echo " HERMES_HOME = $STATE_DIR" + echo " HERMES_WORKSPACE = $WORK_DIR" + + - name: Smoke (up + health + log scan + down) + env: + COMPOSE_FILE: ${{ steps.vars.outputs.compose_file }} + PROJECT: ${{ steps.vars.outputs.project }} + HERMES_HOME: ${{ steps.paths.outputs.state_dir }} + HERMES_WORKSPACE: ${{ steps.paths.outputs.work_dir }} + run: | + set -euo pipefail + + # ----- Trap-guaranteed cleanup, regardless of exit reason ----- + cleanup() { + local rc=$? + echo "::group::Cleanup (rc=$rc)" + docker compose -p "$PROJECT" -f "$COMPOSE_FILE" logs --no-color --tail=200 || true + docker compose -p "$PROJECT" -f "$COMPOSE_FILE" down -v --remove-orphans || true + rm -rf "$HERMES_HOME" "$HERMES_WORKSPACE" || true + echo "::endgroup::" + return $rc + } + trap cleanup EXIT + + echo "::group::docker compose up" + # --wait blocks until all services report healthy OR --wait-timeout fires. + # Compose v2 returns nonzero on either failure mode. + docker compose -p "$PROJECT" -f "$COMPOSE_FILE" up -d --wait --wait-timeout 120 + echo "::endgroup::" + + echo "::group::container roster" + docker compose -p "$PROJECT" -f "$COMPOSE_FILE" ps + echo "::endgroup::" + + # ----- WebUI /health probe ----- + # Single-container: WebUI is on the host on 127.0.0.1:8787. + # Two/three-container: same — both compose files publish 127.0.0.1:8787. + echo "::group::Probe /health" + attempts=0 + max_attempts=30 + until curl --fail --silent --max-time 5 http://127.0.0.1:8787/health > /dev/null; do + attempts=$((attempts + 1)) + if [ "$attempts" -ge "$max_attempts" ]; then + echo "❌ WebUI /health never returned 200 after $max_attempts attempts (~60s)" + exit 1 + fi + sleep 2 + done + echo "✅ /health = 200 after $attempts attempts" + echo "::endgroup::" + + # ----- Startup log scan: must not contain any known-bad signatures ----- + # These are the exact patterns that would have flagged #2470 in real time. + # The grep -i is anchored to actual error tokens; benign log lines that + # contain the substring 'error' in a stack-friendly context (e.g. + # "errorless", URL paths) are improbable for these specific tokens. + echo "::group::Startup log scan" + LOGS="$(docker compose -p "$PROJECT" -f "$COMPOSE_FILE" logs --no-color)" + # `!! ERROR` + `!! Exiting script` are the actual strings emitted by + # docker_init.bash's error_exit() helper — the function name itself + # never appears in output. The literal token `error_exit` is kept as + # a belt-and-suspenders catch for any stray debug/echo of the name. + BAD_PATTERNS='EROFS|Read-only file system|Traceback|PermissionError|!! ERROR|!! Exiting script|error_exit|groupmod: cannot|usermod: cannot|Failed to set (UID|GID|owner|permissions|ownership)' + if echo "$LOGS" | grep -E -i "$BAD_PATTERNS"; then + echo "❌ Startup logs contain known-bad pattern (see above)" + exit 1 + fi + echo "✅ No known-bad patterns in startup logs" + echo "::endgroup::" diff --git a/AGENTS.md b/AGENTS.md index a916be04..6b902daf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,8 @@ Before making changes, read: 1. `README.md` 2. `CONTRIBUTING.md` -3. `CHANGELOG.md` +3. `docs/CONTRACTS.md` +4. `CHANGELOG.md` For architecture, testing, or setup work, also read the matching reference: @@ -19,6 +20,10 @@ For architecture, testing, or setup work, also read the matching reference: - `TESTING.md` for local verification commands and manual test guidance - `docs/onboarding.md` for first-run onboarding behavior - `docs/troubleshooting.md` for diagnostic flows +- `docs/rfcs/README.md` for larger RFCs and state/durability contracts + +For UI or UX work, read `docs/UIUX-GUIDE.md` and `DESIGN.md` before +changing layout, interaction flow, themes, chat rendering, or composer chrome. ## Onboarding and reinstall support @@ -39,15 +44,22 @@ Follow that checklist's safety rules: ## Contribution style -- Keep changes focused on one logical problem. -- Prefer the existing Python + vanilla JavaScript structure over new - dependencies or build steps. +- Keep one logical change per PR; split unrelated refactors or cleanup. +- Read `docs/CONTRACTS.md` and the linked contract/RFC for the touched + subsystem before editing. +- Prefer the existing Python + vanilla JavaScript structure. Do not add + dependencies, build tools, frameworks, or long-lived processes without clear + justification and a rollback story. - Update docs when changing setup, onboarding, runtime behavior, architecture, - or testing guidance. + testing guidance, or user-facing workflows. - Update `CHANGELOG.md` for user-visible behavior, setup, workflow, or documentation changes that should be release-note ready. -- For UI or UX changes, follow `CONTRIBUTING.md`: include before/after evidence - and test relevant responsive states. +- For UI or UX changes, include before/after evidence and test relevant + desktop, narrow, and mobile states. +- For behavior changes, add or update automated tests where practical and list + the manual verification performed. +- For runtime, streaming, recovery, replay, compression, or sidebar metadata + changes, name the state layer being mutated and prove the relevant invariant. ## Local state and secrets diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index bad8dea6..6b865018 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1318,7 +1318,7 @@ Complete list of all HTTP endpoints as of Sprint 1 (v0.3). /api/crons/output ?job_id=X&limit=N -> {outputs: [{filename, content}]} /api/skills All skills. Returns {skills: [{name, description, category}]} /api/skills/content ?name=X -> full skill data including SKILL.md content - /api/memory MEMORY.md + USER.md. Returns {memory, user, *_path, *_mtime} + /api/memory MEMORY.md + USER.md + SOUL.md. Returns {memory, user, soul, *_path, *_mtime} ### POST Endpoints Added in Sprint 3 @@ -1453,7 +1453,7 @@ fetches GET /api/skills/content and renders in the right panel using `showPrevie #### Memory Panel `loadMemory()` fetches GET /api/memory (reads MEMORY.md + USER.md from -~/.hermes/memories/), renders both as markdown via renderMd() with timestamps. +~/.hermes/memories/, and SOUL.md from ~/.hermes/), renders both as markdown via renderMd() with timestamps. #### New API Endpoints (Section 18 update) @@ -1464,7 +1464,7 @@ fetches GET /api/skills/content and renders in the right panel using `showPrevie POST /api/crons/resume {job_id} -> resume_job(job_id) GET /api/skills All skills via tools.skills_tool.skills_list() GET /api/skills/content ?name=X -> full skill data via skill_view(name) - GET /api/memory MEMORY.md + USER.md content and mtimes + GET /api/memory MEMORY.md + USER.md + SOUL.md content and mtimes #### Phase D Input Validation Applied diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fa25bab..941c61ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,302 @@ ## [Unreleased] + +## [v0.51.92] — 2026-05-19 — Release BP (stage-385 — 7-PR full sweep batch — RFC Slice 3c clarification + workspace tree icon alignment + project move cache refresh + auto-compression handoff metadata + Grok OAuth provider catalog + anonymous custom endpoint picker fallback + PWA standalone reload + pull-to-refresh) + +### Fixed + +- **PR #2563** by @Michaelyklam (closes #2554) — Align workspace-tree file rows with sibling directory rows by reserving the same expand/collapse toggle slot for files via a new `.file-tree-toggle-placeholder` element. Expanded directories now show child files stepped in at the same icon column as child folders. Directory toggles and file interactions are unchanged; source-level regression coverage and before/after PNGs included. +- **PR #2561** by @nanookclaw (closes #2551) — Refresh the authoritative `_allSessions` cache when the project picker moves a session to/from a project. Previous code mutated only the shallow sidebar row copy, so `renderSessionListFromCache()` re-read the unchanged cache and repainted a stale project dot until the next `/api/sessions` poll healed the UI. Both the "Removed from project" and "Moved to " branches now write the new `project_id` into `_allSessions[idx]` before re-rendering. +- **PR #2567** by @dso2ng (refs #2477) — Surface automatic-compression handoff metadata through the `compressed` SSE event so the active browser stream keeps its completion card even after the backend rotates the session id from the origin to a compressed continuation. The event now carries both `old_session_id` and `new_session_id`/`continuation_session_id`; the frontend `compressed` listener accepts either, and the automatic-compression detail line names the compressed continuation session so the done state isn't silently dropped. +- **PR #2568** by @Michaelyklam (closes #2545) — Add the Hermes Agent `xai-oauth` provider to the WebUI's OAuth provider catalog so Grok OAuth accounts authenticated via the Hermes CLI appear in Settings → Providers and the `/api/models` picker. The provider is treated as CLI-managed OAuth (no WebUI API-key form) and uses the live Hermes CLI model catalog when available with a Grok 4.20 static fallback. +- **PR #2550** by @espokaos-ops (refs #2542) — Keep anonymous custom OpenAI-compatible endpoints in the model picker even when the configured `/v1/models` probe fails. Lightweight relays and llama-server-style deployments that authenticate `/v1/chat/completions` but not `/v1/models` no longer have their provider group silently dropped from the picker. Users can type a model id manually in the free-form input when no live catalog is available. + +### Added + +- **PR #2548** by @espokaos-ops — Add a PWA-standalone reload affordance. A small refresh button appears in the app titlebar (visible only under `@media (display-mode: standalone), (display-mode: fullscreen)`) so users running the WebUI as an installed home-screen PWA can reload without re-launching the app. Adds a complementary pull-to-refresh gesture on the messages container with an 80px threshold and a smooth-scroll-to-top guard so accidental triggers while reading history feel intentional. 4-viewport screenshots (390/1280/1440/1920, light/dark, hover/idle) included under `docs/pr-media/2548/`. + +### Documentation + +- **PR #2560** by @Michaelyklam (refs #1925) — Clarify the RuntimeAdapter Slice 3c state after #2544 shipped. The RFC now distinguishes shipped `/api/goal` routing through `RuntimeAdapter.update_goal(...)` from the still-staged `queue_message(...)` protocol method, and explicitly warns not to add a new server-side queue endpoint or queue scheduler merely for adapter symmetry while `/queue` remains browser-side queue/drain behavior. + +## [v0.51.91] — 2026-05-18 — Release BO (stage-384 — 5-PR full sweep batch — reasoning-replay history fix + archive-extract per-session inbox + fallback streaming warnings + sanitized custom-provider env hints + Slice 3c queue/goal adapter routing) + +### Fixed + +- **PR #2536** by @Michaelyklam (closes #2514, refs #2535) — Stop reasoning-only Thinking entries from being replayed into provider-facing history as blank assistant turns. Long WebUI sessions were accumulating duplicated stale Thinking blocks and inflated Activity/tool metadata on later turns when reasoning-only display entries (from interrupted/canceled turns) got reinserted into the restored conversation history. The fix keeps visible Thinking cards in the transcript while filtering them out of provider-facing replay. Settled compact Activity rerenders now also clear previously inserted Thinking rows before rebuilding the visible transcript. +- **PR #2520** by @OneFat3 (refs #2247) — Route archive extraction (`/api/upload/extract`) through the per-session attachment inbox (`_session_attachment_dir`) instead of hardcoded `Path(s.workspace)`, matching the single-file upload path. Extracted archives now land at `///` so session deletion cleanup covers them and per-session isolation is preserved when `HERMES_WEBUI_ATTACHMENT_DIR` is configured. +- **PR #2505** by @cyberdyne187 — Surface provider fallback and rate-limit lifecycle notices as auto-clearing fallback warnings in the streaming composer status. The new bridge in `_agent_status_callback` matches agent lifecycle messages containing `rate limited` / `switching to fallback` / `falling back` / `fallback activated` / `trying fallback` and emits them as `warning` events with `type=fallback`, so the existing `static/messages.js` warning channel surfaces them with the correct auto-clear contract instead of letting them drop silently. +- **PR #2556** by @Michaelyklam (closes #2541) — Sanitize auto-generated custom-provider API-key environment variable names so endpoint-derived provider ids such as `custom:gpu.local-8000` use POSIX-safe names like `CUSTOM_GPU_LOCAL_8000_API_KEY`. Runtime custom-provider key resolution now checks the sanitized env var first and falls back to the legacy punctuation-preserving name with a one-shot deprecation warning. Configured literal `api_key` values and explicit `key_env` config are unchanged. + +### Documentation + +- **PR #2544** by @Michaelyklam (refs #1925) — Implement the first Slice 3c RuntimeAdapter control routing. `RuntimeAdapter` / `LegacyJournalRuntimeAdapter` now expose `queue_message(...)` and `update_goal(...)` as protocol-translator delegates, and the `/api/goal` route uses `update_goal(...)` only when `HERMES_WEBUI_RUNTIME_ADAPTER=legacy-journal` is enabled while preserving the legacy-direct response shape. The change keeps `/queue`'s existing browser-side drain semantics and goal post-turn evaluation in the current agent loop; no runner/sidecar, WebUI-owned queue, goal scheduler, cached-agent table, or execution-survives-restart claim is introduced. + +## [v0.51.90] — 2026-05-18 — Release BN (stage-383 — 10-PR full sweep batch — empty-gateway messaging history fix + previous-messaging-sessions setting + Kanban board switcher layout + UI/UX demo theme controls + Slice 3c queue/goal RFC gate + keyless custom endpoints + custom-provider remote model catalog parity + auto-compression elapsed timer + new-conversation cold-start guard + Kanban drag-drop detail open fix) + +### Fixed + +- **PR #2286** by @junjunjunbong (refs #2275) — Narrow messaging stale-session filtering to active gateway sessions that are visible in the current sidebar candidate set. Older Discord/messaging history is now preserved when the gateway advertises a fresh zero-message session that hasn't yet entered the visible projection, instead of being hidden as stale. Adds a regression test for an empty active Discord gateway row preserving prior history. +- **PR #2459** by @franksong2702 (closes #2458) — Fix the Kanban board switcher menu when a board's icon slot carries a long text label (e.g. `layout-kanban`). The icon column changed from a fixed `18px` slot to a bounded flex cell with `min-width:18px;max-width:7.5rem`, with overflow ellipsis on the icon itself so long labels render fully when space allows and truncate cleanly when not. Title and count columns keep stable spacing. Adds before/after screenshots and a CSS contract regression in `tests/test_kanban_ui_static.py`. +- **PR #2522** by @Michaelyklam (refs #2271) — Treat named custom OpenAI-compatible endpoints with a configured `base_url` as key-optional at WebUI agent startup. Local keyless servers (llama-server / vLLM-style LAN deployments) no longer fail early with a synthetic `CUSTOM:_API_KEY` env-var prompt before the request reaches the endpoint; instead the OpenAI-compatible client initialises with a harmless placeholder key and real configured keys are still preferred when present. Refactors the three near-identical custom-provider rebuild blocks (initial agent setup + two retry/healing paths) through the existing `resolve_custom_provider_connection` helper. +- **PR #2515** by @Michaelyklam (closes #2513) — Keep named custom-provider model pickers populated from each configured endpoint's live `/models` catalog even when `custom_providers[].model` is present. The singular `model` field now acts as a sticky/fallback entry appended *after* the remote catalog rather than collapsing the picker to just the configured model and hiding sibling named custom providers. Extracts reusable OpenAI-compatible `/models` parsing/fetching helpers and threads them through both the active-base-url and per-named-provider paths. +- **PR #2512** by @dso2ng (refs #2477, Slice A) — Show an elapsed timer on the running automatic-compression card so long WebUI context-compression pauses no longer look frozen while the browser waits for the `compressed` event. Stamps `startedAt` on the `compressing` SSE event, ticks once per second, and switches to a `5+ min` cap label past the Slice A bound so the UI never frame-freezes at `05:00`. Browser-transient state only — no SSE contract change and no server-side resume reconstruction. +- **PR #2528** by @Michaelyklam (closes #2518) — Guard New Conversation creation while a previous `/api/session/new` request is still in flight, so cold model/provider catalog resolution gives immediate pending feedback and rapid repeated clicks reuse the same create request instead of enqueueing duplicate blank sessions. Coalesces concurrent `newSession()` calls behind a single in-flight promise, disables the sidebar button with `aria-busy="true"`, and shows a localized `Creating new conversation…` composer status. +- **PR #2530** by @franksong2702 (refs #2529) — Keep Kanban drag/drop status updates from also opening the task detail pane. Two failure paths were both producing detail-pane opens after drag/drop: the browser's trailing synthetic click after `drop`, and the generic task-update helper opening detail on every PATCH. The fix adds a time-windowed `_kanbanSuppressCardClickUntil` set on `ondragstart`/`ondragend`/`ondrop` and routes drag/drop status changes through a board-only update path. Explicit card click and keyboard activation remain unchanged. + +### Added + +- **PR #2294** by @junjunjunbong — Add a `show_previous_messaging_sessions` setting so users can opt back into seeing previous messaging sessions that were replaced by `session_reset` or auto-compression. The preference is wired through boot, settings persistence, and the sidebar projection. Also adds a separate "Hide from list" action for imported messaging/CLI sessions that hides individual rows from the sidebar without deleting source history. + +### Documentation + +- **PR #2511** by @franksong2702 (refs #2502 / #2503) — Update the `docs/ui-ux/` demo appearance controls to initialize as `class="dark" data-skin="slate"` instead of the deprecated `data-theme`-only buttons and legacy theme names. Brings the demo pages in line with the live Theme + Skin contract referenced from the new `docs/CONTRACTS.md` so contributors following the contract-index path don't land on stale demos. +- **PR #2509** by @Michaelyklam (refs #1925) — Advance the runtime-adapter RFC after the Slice 3b approval/clarify seam shipped in v0.51.89. The RFC now marks Slice 3b as shipped and defines the next Slice 3c queue/continue + goal control gate: route those controls through `RuntimeAdapter.queue_message(...)` / `update_goal(...)` only after pinning stable response contracts, bounded unavailable-control behavior, replayable lifecycle/status evidence, ordering/idempotency expectations, and explicit non-goals for runner/sidecar ownership or a WebUI-owned queue/goal scheduler. Docs + adapter-seam regression test only — no runtime/control routing changes in this PR. +## [v0.51.89] — 2026-05-18 — Release BM (stage-382 — 6-PR full sweep batch — runtime adapter approval/clarify seam + SOUL.md memory panel + #1855 resolve_model_provider fast-path + PWA sidebar spinner fix + /model active-provider preference + contributor contract docs index) + +### Changed + +- **PR #2496** by @Michaelyklam (refs #1925) — Route approval and clarify responses through the default-off `RuntimeAdapter.respond_approval(...)` / `respond_clarify(...)` seam when `HERMES_WEBUI_RUNTIME_ADAPTER=legacy-journal` is enabled. The default `legacy-direct` path still uses the existing callback helpers directly, legacy no-id responses keep their historical `ok: true` shape, and stale explicit approval ids are now bounded as not-active instead of falling back to the oldest queued command. No approval queue, clarify queue, callback registry, runner, sidecar, queue/goal migration, or cached-agent state is introduced. + +### Added + +- **PR #2500** by @mccxj — Surface `SOUL.md` (the agent's third-person voice/persona profile, stored at `HERMES_HOME/SOUL.md` alongside `config.yaml` / `.env`) as a third section in the Memory panel next to MEMORY.md (notes) and USER.md (profile). `GET /api/memory` now returns `soul`, `soul_path`, and `soul_mtime`; `POST /api/memory/write` accepts `section="soul"` writing to `HERMES_HOME/SOUL.md` (not inside `memories/`). Redaction still applies, i18n labels (`agent_soul` / `no_soul_yet`) added across all 11 locales, new `sparkles` Lucide icon for the section header. + +### Fixed + +- **PR #2499** by @franksong2702 — Keep server-idle session rows from inheriting stale local streaming fields during sidebar optimistic merging, so PWA/browser caches cannot keep a completed session's spinner alive after `/api/sessions` reports no active stream or pending user message. +- **PR #2501** (closes #1855) — Short-circuit the `resolve_model_provider` stage in `POST /api/chat/start` (and sibling chat-handler call sites) when the request already carries an explicit `(model, model_provider)` pair and the model isn't `@provider:model`-qualified. The new fast path in `_resolve_compatible_session_model_state()` returns the inputs verbatim without calling `get_available_models()` — that catalog rebuild can do network I/O (custom OpenAI-compat `/models`, OpenRouter `/models`, LM Studio probes, credential-pool refresh) under an RLock thundering-herd guard and was observed wedging a single request for 115 seconds in a production-grade local deployment. The recurrence captured via the PR #1911 stage diagnostics confirmed the wedge sat entirely in `resolve_model_provider` while every other stage completed in <5 ms. Users behind default-60s reverse proxies (nginx / Apache / Caddy / Cloudflare) were seeing a `502 Proxy Error` while the WebUI eventually completed the run anyway, creating a duplicate-send risk if the user retried in the browser. The slow path is preserved for the inputs that genuinely need it: bare/un-qualified models without a stored `model_provider` (cross-provider repair), `@provider:model`-qualified strings (active-provider validation per #1253), and empty models (default-model lookup). 14 new regression tests in `tests/test_issue1855_resolve_model_provider_fast_path.py` cover both directions — fast-path skips, slow-path still fires — including a static check that the short-circuit precedes the catalog call in source order. + +### Documentation + +- **PR #2503** by @franksong2702 (refs #2502) — Add `docs/CONTRACTS.md` as a public contributor-facing routing index that points UI/UX, runtime/state, and onboarding/setup changes to the relevant public docs (DESIGN.md, AGENTS.md, RFCs, troubleshooting) before contributors edit code or open PRs. Also adds `docs/UIUX-GUIDE.md` synthesizing the calm-developer-console UI/UX principles from DESIGN.md / README.md / THEMES.md / `docs/ui-ux/` into one contributor guide, refreshes the README and THEMES.md skin lists to cover all ten built-in skins (`catppuccin` + `nous`), and tightens the AGENTS.md / CONTRIBUTING.md contribution-style notes for state-layer and evidence requirements. Docs-only — no runtime or maintainer-policy changes. + +## [v0.51.88] — 2026-05-18 — Release BL (stage-381 — 3-PR security + UX + lineage batch — session-bound CSRF tokens for unsafe browser requests + quoted-reply selected-text composer append + compression-continuation sidebar collapse) + +### Security + +- **PR #2484** by @franksong2702 (refs #1909) — Add session-bound CSRF token protection for authenticated unsafe browser requests, layered on top of the existing Origin/Referer same-origin checks. New helpers in `api/auth.py` derive a per-session HMAC token from the HttpOnly session cookie's server-side token (rotates on login, invalidates on logout/expiry). `api/routes.py` `_check_csrf()` now requires a valid `X-Hermes-CSRF-Token` (legacy `X-CSRF-Token` accepted) on authenticated POST/PATCH/DELETE/PUT requests with a browser Origin/Referer. `/api/auth/login` and `/api/csp-report` remain exempt so the bootstrap and CSP-report paths stay unauthenticated; non-browser callers (curl, MCP, agent) without Origin/Referer continue to bypass the token check. The shell template now injects the token via `__CSRF_TOKEN_JSON__` and the frontend attaches it to same-origin unsafe fetches. + +### Added + +- **PR #2485** by @franksong2702 (refs #2481) — Add a ChatGPT-style "Reply with selection" floating button that appears when the user selects visible chat transcript text. Clicking the button appends the selection into the composer as a Markdown blockquote, with repeat selections appending additional quoted blocks for multi-selection workflows. Frontend-only slice: no new backend routing, message schema, or persistence contract — the existing composer/send path is reused. New i18n labels/title/aria text across locales including `zh-Hant`. + +### Fixed + +- **PR #2493** by @dso2ng (closes #2489) — Collapse WebUI auto-compression continuations into the parent session's sidebar row so long-running conversations stay visually continuous. `static/sessions.js` `_sessionLineageKey()` now accepts a `sessionsById` map and recognises the `pre_compression_snapshot` parent-chain shape as compression lineage even when the continuation lacks explicit `_lineage_root_id` / `lineage_root_id` metadata. The existing child/fork guard is preserved; only the snapshot-parent case is folded back into a single sidebar row. Regression test covers the #2489 case where parent snapshot + continuation were both visible but the continuation carried only `parent_session_id`. + +### Documentation + +- **PR #2483** by @franksong2702 (refs #2364) — Add a narrow README note for the community ARM64 Android AVF field report: Hermes Agent + WebUI running inside a Debian 12 VM on a mid-range Android phone with cloud-hosted inference. The note frames the report as a compatibility signal rather than an official support baseline or provider/model benchmark, and records practical mobile caveats around first-install compile time, Android tab reloads, and battery optimization. +- **PR #2487** by @Michaelyklam (refs #1925) — Advance the runtime-adapter RFC after the Slice 3a cancel-control implementation shipped in v0.51.86. The RFC now marks Slice 3a as shipped and defines the next Slice 3b gate for approval/clarify controls: route them through `RuntimeAdapter.respond_approval(...)` / `respond_clarify(...)` only after pinning response-shape stability, bounded missing-prompt behavior, replayable request/resolution events, duplicate-response safety, and explicit non-goals for queue/goal and runner/sidecar work. + +## [v0.51.87] — 2026-05-18 — Release BK (stage-380 — 2-PR Docker hygiene + CI gate — read-only mount tmpfs staging + Docker runtime smoke workflow + agent-source boundary inventory + writable-mount startup warning) + +### Added + +- **PR #2482** by @Michaelyklam (refs #2453) — Add a durable source/API boundary inventory for the WebUI's remaining Hermes Agent source dependencies: chat execution, runtime events, profiles, goals, slash/plugin commands, provider/auth/model catalogs, redaction parity, and imported Agent/Gateway sessions. The new RFC tracks replacement API contracts before the source mount can be removed. + +### Changed + +- **PR #2482** by @Michaelyklam (refs #2453) — Make the multi-container source boundary more explicit: Docker docs and README now link the boundary inventory, and `docker_init.bash` emits a startup warning when the WebUI sees a writable agent-source mount instead of the default read-only `hermes-agent-src` mount. + +### Fixed + +- **PR #2490** by @nesquena-hermes — Multi-container Docker startup is no longer broken by the v0.51.84 `:ro` mount on `hermes-agent-src`. `docker_init.bash` was calling `uv pip install "$_agent_src[all]"` against the mounted source tree directly. setuptools' `egg_info` build step touches `hermes_agent.egg-info/` inside the source tree even under PEP 517 build isolation, which `EROFS`-failed on the now-read-only mount and (under `set -e`) killed startup of every multi-container deploy. The init script now stages the agent source into `/tmp/hermes-agent-build` via `rsync` (with a `cp -a` fallback for images without rsync, both excluding any pre-baked `*.egg-info`, `build`, `dist`, and `__pycache__` artifacts) and runs the install against that writable copy, leaving the underlying `:ro` mount untouched. Stage dir is removed after the install completes. This regression was caught by the new Docker runtime smoke gate (below) on its very first CI run against its own PR — 5800+ source-level pytests + the independent reviewer's eyeball had all missed it on PR #2470. + +### Infrastructure + +- **PR #2490** by @nesquena-hermes — Add a Docker runtime smoke gate (`.github/workflows/docker-smoke.yml`) triggered on PRs and pushes to `master` that modify `Dockerfile`, `docker_init.bash`, `docker-compose*.yml`, `.dockerignore`, or `.env.docker.example`. Validates every compose file parses (`docker compose config`), then matrix-runs the single, two-container, and three-container variants end-to-end: rebuilds the local `Dockerfile` and re-tags it as `ghcr.io/nesquena/hermes-webui:latest` so the multi-container variants exercise PR-level changes rather than the previously-released registry image, `docker compose up -d --wait`s with a 120s health window, probes `/health`, and greps startup logs for known-bad signatures (`EROFS`, `Traceback`, `PermissionError`, `error_exit`, `!! ERROR`, `!! Exiting script`, `groupmod: cannot`, `usermod: cannot`, `Failed to set`). Closes the source-only-test gap that let v0.51.84's `:ro`-mount × `chown -h ... {} +` startup regression reach review with 5800+ green pytests. Workflow runs with `permissions: contents: read`, uses per-run project names and a pre-flight orphan reaper for safe concurrency, and unconditionally tears down all volumes/networks in an `EXIT` trap. Two new source-level invariants in `tests/test_docker_docs_and_readonly.py` pin the staging path so the underlying `:ro`-incompatible call doesn't regress. + + +## [v0.51.86] — 2026-05-17 — Release BJ (stage-379 — 4-PR review-bypass batch — memory-provider session lifecycle + cross-provider /model alias + RuntimeAdapter cancel seam + Fork-from-here messaging coord) + +### Fixed + +- **PR #2461** by @starship-s — Add a WebUI-side memory-provider session lifecycle for batch-extraction providers (OpenViking, etc.). The new `api/session_lifecycle.py` module tracks per-session generation, segment ownership, and an `in_flight` flag with a `threading.Condition`, so a late-finishing commit can only advance `committed_generation` against its captured generation without erasing newer turns marked during the commit. `mark_turn_completed` runs post-turn after save/cancel/completed-journal guards; `commit_session_memory` runs at session boundaries (new session, eviction, shutdown) outside cache locks and per-session mutation locks. `register_agent`/`unregister_agent` preserves dirty segment owners so failed work remains retryable even if the cache drops the current agent reference. `drain_all_on_shutdown` flushes every registered session with uncommitted work at process exit. +- **PR #2473** by @ts2111 — `/model ` now correctly routes cross-provider custom-model aliases to their `custom_providers[].name` rather than incorrectly falling through to the active provider's `config_base_url` branch. Adds a custom-providers prefix check in `resolve_model_provider()` between the explicit early-return carve-outs and the `config_base_url` catch-all, and exposes a top-level `aliases` key in `/api/models` so the frontend can resolve user-defined `/model ` shortcuts. `cmdModel()` now fetches `/api/models`, resolves the alias, fuzzy-matches the dropdown, and falls back to a direct `POST /api/session/update` when no dropdown match exists. +- **PR #2480** by @Michaelyklam (closes #2472) — Make "Fork from here" use the same merged messaging-session transcript coordinate space that `/api/session` exposes, so forking an older message no longer silently copies the full sidecar when CLI/Gateway history inflated the visible message offset. Extracts the merge logic into `_merged_session_messages_for_display(session, cli_messages)` and routes both `GET /api/session` and `POST /api/session/branch` through it. The frontend snapshots the source session id across the async full-history load (so a fast sidebar switch can't fork the wrong session), reloads the forked transcript fully after creation, and the branch handler best-effort saves the source session before slicing to keep undo/retry state coherent. + +### Changed + +- **PR #2479** by @Michaelyklam (refs #1925) — Route Stop Generation through the default-off `RuntimeAdapter.cancel_run(...)` seam when `HERMES_WEBUI_RUNTIME_ADAPTER=legacy-journal` is enabled. Implements the first code slice of the Slice 3a cancel-control gate accepted in #2469 / v0.51.85. The default `legacy-direct` path still calls `cancel_stream(...)` directly; the adapter branch preserves the existing `{ok, cancelled, stream_id}` JSON response contract. No new cancellation registry, runner, sidecar, approval/clarify, queue/goal, or cached-agent state is introduced — adapter remains a pure protocol translator. + + +## [v0.51.85] — 2026-05-17 — Release BI (stage-378 — 3-PR batch — workspace-prefix display leakage fix + release-tag update banner + Slice 3a cancel-control gate RFC) + +### Fixed + +- **PR #2145** by @swftwolfzyq — Prevent internal `[Workspace::v1: …]` metadata from leaking into the visible user transcript when a failed provider/retry path echoes an optimistic draft followed by the workspace prefix and the real prompt. Adds `_looks_like_current_user_turn(msg, msg_text)` to match the current human turn even when the internal tag appears mid-text — only when the text after the sentinel exactly matches the submitted prompt — and routes the merge/dedupe/display-normalization paths in `_merge_display_messages_after_agent_result` and `_find_current_user_turn` through it. Replaces `_has_new_assistant_reply` length-delta gating in `_periodic_checkpoint` with the new `_assistant_reply_added_after_current_turn` helper, which slices result messages from the current-turn position before counting assistant deltas — silent/no-response failures are now detected from the current turn alone instead of being masked by prior assistant content. +- **PR #2146** by @swftwolfzyq — Track WebUI update checks against the latest published release tag instead of every commit on the upstream branch, so operators who only want released versions stop seeing noisy update banners for post-release development commits. Falls back to branch-based detection when no release tags are available. Splits `git describe --dirty` into a fast base describe plus a bounded dirty probe so WSL-mounted workspaces never block version detection on a slow `--dirty` walk, keeping the base version visible even if the dirty probe times out. + +### Documentation + +- **PR #2469** by @Michaelyklam (refs #1925) — Advance the runtime-adapter RFC after the Slice 2 seam shipped by marking Slice 2 complete and defining the first Slice 3a cancel-control gate. The new gate scopes Stop Generation through `RuntimeAdapter.cancel_run(...)` only, pins behavior-preserving cancellation, journal/status coherence, idempotent duplicate cancel, and explicit non-goals for approval/clarify, queue/goal, runner/sidecar, and public chat-start response changes. + +## [v0.51.84] — 2026-05-17 — Release BH (stage-377 — 1-PR Docker hygiene — agent-image upgrade docs + read-only WebUI source mount + chown prune widening) + +### Changed + +- **PR #2470** — Multi-container Docker hygiene pass. The `hermes-agent-src` named volume in `docker-compose.two-container.yml` and `docker-compose.three-container.yml` is now mounted **read-only** on the WebUI service (the WebUI only reads it to install the agent's Python dependencies at startup), bringing the actual mount mode in line with the existing `docs/docker.md` architecture diagram. To keep `docker_init.bash` startup compatible with the new read-only mount, `chown_home_hermeswebui` now prunes the entire `/home/hermeswebui/.hermes/hermes-agent` subtree from the ownership walk instead of only `.git/objects` — the WebUI never writes to the agent source, so the previous narrower carve-out was always a nicety, and on a `:ro` mount it would have returned `EROFS` and killed startup under `set -e`. The widened prune also subsumes the original #2237 macOS bind-mount case (the `.git/objects` packs are inside the now-pruned subtree). The default `${HERMES_WORKSPACE:-~/workspace}` workspace bind is changed to `${HERMES_WORKSPACE:-${HOME}/workspace}` so the path resolves consistently across Linux, macOS, WSL2, and Docker Desktop on Windows (matching the single-container `docker-compose.yml` convention). No behaviour change for users who set `HERMES_WORKSPACE` explicitly. + +### Documentation + +- **PR #2470** — `docs/docker.md` gains an **"Upgrading the agent container"** section documenting the root cause of [#1416](https://github.com/nesquena/hermes-webui/issues/1416): the `hermes-agent-src` named volume caches the agent's `/opt/hermes` source tree on first run, and Docker reuses the cached volume on every subsequent `compose up` — even after `docker pull` of a newer agent image. The new section gives the canonical `down → docker volume rm → pull → up -d` recipe and the same upgrade pointer is mirrored as a comment block in both multi-container compose files. A new **"What the multi-container setup isolates (and what it doesn't)"** section explicitly frames the two/three-container setups as **process, network, and resource isolation, not filesystem isolation** — calibrating expectations for users who reach for multi-container expecting a trust boundary between the chat UI and the agent. + +## [v0.51.83] — 2026-05-17 — Release BG (stage-376 — 12-PR contributor batch — chat-start adapter parity + populated-core journal recovery + thinking card dedup + context metadata refresh + model cache fingerprint + stream fade cap + manual cron delivery + active-session spinner + email gateway label + thinking copy button + /theme i18n + compact activity semantics) + +### Added + +- **PR #2460** by @Michaelyklam (closes #2449) — Add a copy button to Thinking card headers so users can copy the card's reasoning text without selecting the `
` manually. The button stops header-toggle propagation and shows the same short checkmark feedback pattern used by existing copy actions.
+
+### Fixed
+
+- **PR #2438** by @franksong2702 (fixes #2435) — Keep the default-off `HERMES_WEBUI_RUNTIME_ADAPTER=legacy-journal` chat-start path response-compatible with the legacy-direct path by not adding adapter-internal `run_id`, `status`, or `active_controls` fields to `/api/chat/start` responses. The adapter facade for the future #1925 runtime split stays an internal protocol-translator seam instead of expanding the public chat-start contract.
+- **PR #2439** by @franksong2702 (fixes #2434) — Recover already-journaled visible assistant text and tool cards even when restart repair first syncs a populated Hermes core transcript into an otherwise empty WebUI sidecar. The core-sync branch now merges non-duplicate run-journal output before clearing stale stream state, closing the carve-out from PR #2427 where recoverable partial output could be silently skipped. Adds `_append_journaled_partial_output(..., dedupe_existing=True)` plus helpers `_run_journal_has_visible_output`, `_find_existing_assistant_for_journal_content`, and `_journal_tool_already_present`.
+- **PR #2441** by @Michaelyklam (fixes #2440) — Compact live Thinking cards now reuse the same timeline card across sequential tool calls within a single assistant turn. `finalizeThinkingCard` clears the `data-thinking-active` marker by searching the entire assistant turn instead of only the tool group, and `appendThinking` reuses the most recent Thinking card when no active marker is set, preventing repeated Thinking cards from stacking as reasoning resumes between tool calls.
+- **PR #2444** by @franksong2702 (fixes #2442) — Refresh session context-window metadata when a session's resolved model changes during deferred hydration or when the user switches models, so high-context models do not stay stuck on a stale prior window and trigger premature compression. Adds a shared `_resolve_context_length_for_session_model` helper, updates `GET /api/session?resolve_model=1` to refresh non-zero persisted windows from current model metadata, resets context metadata on `/api/session/update` model/provider changes, and applies returned `context_length`/`threshold_tokens`/`last_prompt_tokens` in the deferred client-side resolution path with an immediate context-indicator resync.
+- **PR #2445** by @Michaelyklam (fixes #2443) — `/api/models` now fingerprints the in-module provider catalog plus the local Codex `models_cache.json` as part of its persisted cache metadata, so server-side catalog additions and Codex local catalog refreshes invalidate `models_cache.json` immediately on the next restart instead of waiting for the 24-hour TTL or manual cache deletion.
+- **PR #2450** by @Michaelyklam (fixes #2447) — Cap the optional streaming word-fade drain after the final `done` SSE event so very large or bursty completed responses render from the canonical session promptly instead of keeping the chat in a live/working state until Stop is pressed. The existing caught-up path and per-token animation wait are preserved for normal responses.
+- **PR #2452** by @Michaelyklam (fixes #2451) — Manual WebUI cron triggers now deliver the same final response or failure notice as scheduled cron runs. The manual-run wrapper reuses the scheduler delivery contract (`[SILENT]` skipping, separate `last_delivery_error` metadata, error-notice fallback) with a `TypeError` shim for legacy `mark_job_run` signatures used by older WebUI test doubles.
+- **PR #2455** by @franksong2702 (fixes #2454) — Keep the sidebar spinner in sync with server session metadata when the currently open session has finished but the browser still has stale local busy state. A new `_reconcileActiveSessionIdleStateFromList` helper clears `S.busy`, `S.activeStreamId`, the `INFLIGHT` cache, and active-session stream metadata before optimistic merging can re-mark the row as streaming.
+- **PR #2457** by @Michaelyklam (closes #2456) — Email gateway sessions imported from Hermes Agent `state.db` now normalize as messaging sessions and show an `Email` source label in the WebUI sidebar instead of falling through as unlabelled generic agent sessions. Keeps the Python source-normalization contract (`MESSAGING_SOURCES`, `SOURCE_LABELS`), gateway status platform labels, and frontend `static/sessions.js` whitelist in sync.
+- **PR #2463** by @Michaelyklam (closes #2462) — Align `/theme` command help strings in Russian, German, Simplified Chinese, Traditional Chinese, and French with the current Theme × Skin contract. The localized command descriptions now mention `system/dark/light` plus the full skin list through `nous`, and French invalid-usage text now uses the actual `/theme ` slash command prefix instead of `/thème`. Supersedes the parallel-discovery duplicate at #2464 (closed in favor of this PR).
+
+### Changed
+
+- **PR #2466** by @franksong2702 (closes #2465) — Clarify `Compact tool activity` semantics in Preferences: the setting now describes compact inline activity that preserves the agent timeline, matching the current long-running turn behavior where thinking cards, visible progress notes, and tool Activity bursts stay in chronological order instead of being described as one top-of-turn collapsed block. Renderer behavior is unchanged; this is a description-only correction plus the `simplified_tool_calling` default comment and regression-test wording.
+
+## [v0.51.82] — 2026-05-17 — Release BF (stage-375 — 2-PR batch — table renderer pipe protection + Catppuccin appearance skin)
+
+### Added
+
+- **PR #2432** by @Michaelyklam (closes #2426) — Add a Catppuccin skin to Appearance settings. The single opt-in skin maps light mode to Catppuccin Latte and dark mode to Catppuccin Mocha, using Mauve as the accent while preserving the existing theme/skin persistence and no-build-step architecture.
+
+### Fixed
+
+- **PR #2428** by @bengdan — Protect pipes inside parens / brackets / braces from naive `split('|')` in the Markdown table renderer. Cells like `` `(a|b)` ``, `` `Union[int|float]` ``, `` `(a|b|c)` ``, and `` `Union[int|float|str]` `` now stay in a single column instead of mis-splitting. The fix uses an iterative `_protectPipes` loop so all pipes inside one bracket pair are caught, not just the first. Also adds a `$...$` guard so a KaTeX inline-math span straddling ` | ` column separators is left alone instead of being stashed as math. Stage-fix on the contributor branch (a) swapped the literal `}` glyphs in the regex character classes for `\x7d` hex escapes (semantically identical, but the JS source no longer carries bare close-brace glyphs that confused the brace-counting `extractFunc` in `tests/test_renderer_js_behaviour.py`); (b) dropped a stray apostrophe stop that would have mis-split `('a'|'b')`-style string-literal unions; (c) dropped angle brackets `<` / `>` from the protected-bracket set, after Opus advisor flagged that `| x < 5 | y > 10 |` would otherwise collapse into a single cell (comparison-operator usage dominates content-grouping usage in real LLM table output); and (d) added `tests/test_issue2428_table_pipe_protection.py` with 12 regression cases covering single-pipe, multi-pipe-in-brackets, apostrophes-with-pipes, the KaTeX-in-table guard, and the angle-bracket comparison-operator case.
+
+## [v0.51.81] — 2026-05-17 — Release BE (stage-374 — 6-PR batch — cost-history POSIX lock + prompt-cache tokens + Plugins panel i18n + pending-placeholder chat + journal-replay partial recovery + default-off RuntimeAdapter Slice 2 seam)
+
+### Added
+
+- **PR #2424** by @Michaelyklam (refs #1925) — Add the default-off `RuntimeAdapter` Slice 2 seam. `HERMES_WEBUI_RUNTIME_ADAPTER=legacy-journal` now routes chat start through a `LegacyJournalRuntimeAdapter` facade over the existing legacy streaming path, while the default remains `legacy-direct`. The new adapter interface/payload classes expose start/observe/status/cancel/approval/clarify methods and delegate controls to existing handlers without introducing a runner, sidecar, new process-local queues, cached agents, cancellation registries, or callback registries.
+- **PR #2421** by @Michaelyklam (fixes #2419) — Surface provider prompt-cache read/write tokens in WebUI usage displays. Cache-miss cost issues are now visible in the context tooltip and per-turn usage footer; counters carry through session persistence, SSE usage payloads, and live snapshots so deltas remain accurate across the active turn.
+- **PR #2425** by @mccxj — Wire Settings → Plugins panel into the existing i18n system. Panel title, description, empty state, and per-plugin labels (hooks, enabled/disabled, load failures) now respect the user's language preference; 10 new keys ship in English with `TODO: translate` placeholders in 9 additional locales.
+
+### Fixed
+
+- **PR #2418** by @Michaelyklam (fixes #2402) — OpenRouter cost-history snapshot updates now take a provider-specific POSIX file lock around the read-modify-write cycle, preserving the existing process-local lock while preventing lost snapshot updates if WebUI is deployed with multiple worker processes sharing one Hermes home/state directory.
+- **PR #2431** by @Michaelyklam (fixes #2429) — Chat sends now render the assistant-side pending `Thinking…` placeholder immediately after the user turn is echoed, before `/api/chat/start` returns a stream id or the first SSE event arrives. The existing stale-stream guard remains in place for ordinary reasoning updates — only the explicit pre-stream placeholder path is allowed through.
+- **PR #2427** by @franksong2702 (fixes #2423) — Recover already-journaled visible assistant text and tool cards when a WebUI process restart interrupts an in-flight browser-originated turn. The stale-stream repair path now materializes run-journal output before the explicit interrupted marker instead of collapsing the turn to "no agent output was recovered."
+
+### Documentation
+
+- **PR #2416** by @Michaelyklam (refs #1925) — Expand the runtime-adapter RFC with the concrete Slice 2 adapter-seam contract: minimal `RuntimeAdapter` methods, payload fields, `legacy-direct` / `legacy-journal` feature-flag rollback path, legacy-backend mapping, explicit non-goals, and adapter-seam acceptance tests. Keeps the next step scoped to a reversible protocol-translator boundary over the journaled legacy path, not a runner/sidecar or execution-ownership move.
+
+## [v0.51.80] — 2026-05-17 — Release BD (stage-373 — 2-PR batch — provider config flag filter + stale compaction greeting heuristic)
+
+### Fixed
+
+- **PR #2415** by @Michaelyklam (fixes #2399) — `providers.only_configured` and other scalar flags under the top-level `providers:` config mapping no longer appear as fake provider groups in the model picker. Provider detection now only seeds picker groups from known provider ids/aliases or dict-shaped provider configs, so filtering flags cannot render as `Only-Configured`. The gating contract is documented inline in `api/config.py` (within the existing `_PROVIDER_MODELS`/`_PROVIDER_DISPLAY` membership block) so the test_issue604 source-scan stays satisfied.
+- **PR #2417** by @nesquena-hermes (co-authored by @franksong2702, supersedes #2309, closes #2308) — Compressed sessions with hidden "resume active task" context no longer treat a short fresh greeting (`hi`, `hello`, plus 6 CJK greetings) as implicit permission to continue an old agent task. Explicit continuation prompts (`continue`, `resume`, plus 4 CJK continuation phrases) still keep the compacted task context. The new helpers (`_normalize_fresh_chat_text`, `_is_casual_fresh_chat_message`, `_has_task_resume_compaction_marker`, `_context_messages_for_new_turn`) require BOTH the compaction phrase AND a task-resume keyword in the SAME message before treating it as a stale-task marker (precision-preserving guard). Length cap of 24 chars + workspace-prefix normalization + exact greeting-set match prevent false positives. CJK greetings/continuation terms are stored as Python `\u`-escape sequences so `api/streaming.py` passes the `test_title_sanitization::test_title_generation_source_has_no_cjk_literals` English-only-source invariant; runtime values are unchanged. Stage-372 Opus advisor pass caught two CJK codepoint typos (`嘖→嗨`, `哈喂→哈喽`) in the maintainer rebase and corrected them; new regression test `test_all_cjk_greetings_drop_stale_compaction_context` pins all 6 CJK greetings against future codepoint drift with `U+XXXX` failure messages.
+
+## [v0.51.79] — 2026-05-16 — Release BC (stage-372 — 5-PR batch — text-mode image history fix + Activity-group compression boundary + named custom provider routing + quota chip Settings toggle + RFC docs)
+
+### Added
+
+- **PR #2413** (self-built follow-up to v0.51.78's #2082, closes the quota-chip default-on regression) — New "Show provider quota chip in composer" checkbox in Settings → Preferences, default off. When disabled (the new default), the chip is hidden at all viewports and the `/api/provider/quota` fetch is skipped entirely. When enabled, the existing `@media (max-width:1399.98px)` gate from stage-371 still restricts the chip to wide desktops only. Per Nathan's directive 2026-05-16 immediately after stage-371 shipped — users get explicit agency over an ambient composer-chrome element. Wired through `api/config.py` `_SETTINGS_DEFAULTS`, `static/boot.js`, `static/panels.js` round-trip, `static/ui.js` short-circuit-when-disabled, `static/index.html` Settings field, and 11 locales in `static/i18n.js`.
+
+### Fixed
+
+- **PR #2406** by @Michaelyklam (fixes #2398) — The fallback synchronous `POST /api/chat` route now passes the active WebUI config into the conversation-history sanitizer, so text-mode providers do not receive historical native `image_url` content parts when direct API callers use the legacy chat endpoint. This brings the sync route in line with the streaming chat path fixed for #2297.
+- **PR #2408** by @Michaelyklam (fixes #2404) — Auto-compression cards now close the current live Activity burst before rendering, so post-compression tools start a fresh `Activity` row instead of joining the pre-compression tool group across a real timeline/context boundary. Adds a `closeCurrentLiveActivityGroup()` helper that clears the `data-live-activity-current` marker before `appendLiveCompressionCard()` inserts the compression card. Resolves the DEFER from stage-370 Opus advisor review of PR #2390.
+- **PR #2411** by @Michaelyklam (fixes #2405) — Named `custom:*` providers no longer lose vendor-prefixed model selections when the static model picker has not hydrated that model yet. The frontend now treats named custom providers as routable aggregators for both mismatch-warning suppression and missing-dropdown fallback, and live-fetched models keep explicit `@custom:name:` provider context so selections persist instead of snapping back to the configured default.
+
+### Documentation
+
+- **PR #2407** by @Michaelyklam — Document the #1925 runtime-adapter gate update: Slice 1 run-journal replay has now passed a 100-trial synthetic replay/restart validation pass on current `origin/master`, #2313's selected-session chat SSE cap is shipped, and Slice 2 is ready for a reversible adapter-seam planning PR without moving execution ownership yet.
+
+### Test infrastructure
+
+- New regression test `tests/test_quota_chip_settings_toggle.py` (6 cases) pins the quota-chip toggle invariants: Settings field present with i18n labels, `show_quota_chip` default-`False` in `_SETTINGS_DEFAULTS` + `_SETTINGS_BOOL_KEYS`, render/refresh both short-circuit when disabled (no wasted API calls), boot initializes `window._showQuotaChip` from settings + default-false on settings-fetch failure, full panels.js round-trip, 11 locale strings present.
+
+## [v0.51.78] — 2026-05-16 — Release BB (stage-371 — stuck-PR sweep salvage — RTL chat + ambient quota chip with composer-clutter gate)
+
+### Added
+
+- **PR #2409** (maintainer follow-up from 2026-05-16 stuck-PR sweep, co-authored by @malulian and @ai-ag2026, closes #1721 and #2082) — Two stalled contributor PRs absorbed into one self-built release after Telegram UX approval across mobile/laptop/desktop/wide viewports.
+  - **Right-to-left chat layout (salvaged from #1721 by @malulian)** — New Settings → Preferences toggle, default off, flips the chat-area direction for Arabic and Hebrew users. Honors @aronprins' design review on PR #1721 (May 13 2026): drops the contributor's composer footer toggle button to keep composer real estate clean. Implementation includes a flash-prevention bootstrap `
 
 
+
 
 
 
@@ -26,7 +26,46 @@
 
 
 
-
+
+
 
   
   
@@ -62,6 +101,18 @@
       });
     }
   
+  
 
 
 
 
+
- - - + + +
@@ -935,6 +996,13 @@ +
+ +
Flips alignment of chat messages and the composer input for languages like Arabic or Hebrew. Affects only the chat area — sidebar and other panels stay left-to-right.
+
Displays input/output token count below each assistant reply. Also toggled with /usage.
+
+ +
Displays an ambient remaining-quota indicator (e.g. OpenRouter credit balance) in the composer footer. Default off. Only visible on wide displays (≥1400px) when enabled, to keep the composer uncluttered on laptop and standard desktop widths.
+
-
Group thinking and tool calls into one collapsed activity section per assistant turn.
+
Show thinking and tool calls as compact inline activity while preserving the agent timeline.
Show conversations from CLI, Telegram, Discord, Slack, and other channels in the session list. Click to import and continue.
+
+ +
Show older Discord, Telegram, Slack, and Weixin sessions that were replaced by reset or compression.
+