fix: dedupe replayed context tail after compression - #2651
Merged
1 commit merged intoMay 20, 2026
Merged
1 commit merged into
1 commit merged into
Conversation
f3b8d57
dobby-d-elf
pushed a commit
to dobby-d-elf/hermes-webui
that referenced
this pull request
May 20, 2026
eleboucher
pushed a commit
to eleboucher/homelab
that referenced
this pull request
May 21, 2026
… 0.51.103) (#594) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.96` → `0.51.103` | --- ### Release Notes <details> <summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary> ### [`v0.51.103`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051103--2026-05-21--Release-CA-stage-396--1-PR-follow-on--Settings--Plugins-distinguishes-exclusiveprovider-activation) [Compare Source](nesquena/hermes-webui@v0.51.102...v0.51.103) ##### Fixed - **PR [#​2663](nesquena/hermes-webui#2663 by [@​Fail-Safe](https://github.com/Fail-Safe) (closes [#​2659](nesquena/hermes-webui#2659)) — Settings → Plugins panel now distinguishes exclusive plugins (memory providers, web backends, browser providers activated via `<category>.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`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051102--2026-05-21--Release-BZ-stage-395--1-PR-follow-on--capped-CLI-sidebar-candidate-window-now-keyed-on-last-activity-not-start-time) [Compare Source](nesquena/hermes-webui@v0.51.101...v0.51.102) ##### Fixed - **PR [#​2662](nesquena/hermes-webui#2662 by [@​Michaelyklam](https://github.com/Michaelyklam) (closes [#​2656](nesquena/hermes-webui#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](nesquena/hermes-webui#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`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051101--2026-05-20--Release-BY-stage-394--2-PR-deep-review-batch--workspace-Git-backend--sidebar-tab-visibility-toggle) [Compare Source](nesquena/hermes-webui@v0.51.100...v0.51.101) ##### Added - **PR [#​2625](nesquena/hermes-webui#2625 by [@​stocky789](https://github.com/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](nesquena/hermes-webui#2636 by [@​FrancescoFarinola](https://github.com/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 `<script>` in `<head>` applies `nav-tab-hidden` from `localStorage` before first paint so toggled-off tabs don't flash visible on reload. Default off — no tabs are hidden out of the box; existing deployments are unaffected. Chips use `role="switch"` + `aria-checked` for clear screen-reader narration, and the container has `role="group"` + `aria-labelledby` pointing at its label. Backend validator strips `chat` and `settings` from `hidden_tabs` at save time as a belt-and-suspenders against tampered POSTs. Profile switch reconciliation: `_refreshProfileSwitchBackground` re-fetches `/api/settings` and re-applies `hidden_tabs` after a profile change so the new profile's preference takes effect immediately. ##### Maintainer additions during stage - `_refreshProfileSwitchBackground` profile-switch reconciliation for [#​2636](nesquena/hermes-webui#2636) (Profile A's hidden-tabs no longer bleeds into Profile B until Settings is opened). - `role="switch"` + `aria-checked` chip a11y for [#​2636](nesquena/hermes-webui#2636) (was `aria-pressed` — confusing polarity for users where chip-off looks like the off state). - Server-side `hidden_tabs` validator strip of `chat`/`settings` for [#​2636](nesquena/hermes-webui#2636). - CSS contrast fix for [#​2636](nesquena/hermes-webui#2636) — `color: #​1a1a1a` + `font-weight: 600` on filled chips (was `color: var(--bg-page)` which resolved to white in dark theme and was barely readable on the gold accent). - 3 new regression tests for the [#​2636](nesquena/hermes-webui#2636) maintainer additions (profile-switch wiring, chat/settings server-side strip, a11y switch role). ##### UX approval PR [#​2636](nesquena/hermes-webui#2636) went through the full multi-viewport screenshot gate (390 mobile, 1280 laptop, 1440 desktop, 1920 wide; both light and dark themes; default-on and 3-off mixed states; rail-effect proof showing hidden tabs collapse cleanly). Approved via Telegram for merge. ### [`v0.51.100`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051100--2026-05-20--Release-BX-stage-393--3-PR-deep-review-batch--lazy-journal-recovery-retry--faster-profile-switch--cross-tab-session-list-SSE-sync) [Compare Source](nesquena/hermes-webui@v0.51.99...v0.51.100) ##### Fixed - **PR [#​2615](nesquena/hermes-webui#2615 by [@​Isla-Liu](https://github.com/Isla-Liu) — Self-heal stalled interrupted-turn markers by lazily retrying run-journal recovery on subsequent sidebar/full-load reads. When `_apply_core_sync_or_error_marker` lands in the "interrupted, no journal yet" branch, the session now carries `_pending_journal_recovery` metadata so the next `get_session()` call re-runs `_append_journaled_partial_output(..., dedupe_existing=True)` if a fresh journal has materialized in the meantime. Bounded by three independent ceilings: `_JOURNAL_RETRY_MAX_ATTEMPTS = 12` (attempts on sealed-but-unrecoverable journals), `_JOURNAL_RETRY_GIVEUP_SECONDS = 24 * 3600` (wall-clock cap that fires even when the journal never materializes), and an explicit "no stream id → demote immediately" branch. Per-session `threading.Lock` (non-blocking acquire) guards concurrent `get_session()` callers; second caller no-ops to `False`. Closes a real WSL2/network-FS class of bug where the recovery marker stuck on a transiently-missing journal and never self-healed without a server restart. ##### Changed - **PR [#​2633](nesquena/hermes-webui#2633 by [@​dobby-d-elf](https://github.com/dobby-d-elf) — Make agent profile switching and initial app boot dramatically faster (\~25× on the contributor's benchmark). Switch-time: applies the selected profile's defaults immediately (resolved via a per-profile `_profile_default_model_state(profile)` config read), refreshes the visible/current screen, schedules non-visible workspace/model metadata in the background after the current view settles. A `_profileSwitchGeneration` counter is checked at five `await` boundaries to abort stale switches when the user re-toggles mid-refresh. Boot-time: workspace and onboarding fetches run in parallel instead of serial; onboarding short-circuits with `Promise.resolve(false)` when `onboarding_completed` is true. Boot synthetic model options now carry `data-provider` metadata so a pre-catalog `/api/chat/start` doesn't fuzzy-fallback to the wrong provider. FLIP animation capture/play is index-pinned by a new positional test; `prefers-reduced-motion` is honored in both JS and CSS. Server-side `new_session()` resolves the requesting profile's `config.yaml` directly rather than the process-global, preventing cross-tagging when the URL omits `model`/`model_provider`. ##### Added - **PR [#​2637](nesquena/hermes-webui#2637 by [@​dobby-d-elf](https://github.com/dobby-d-elf) — Push lightweight session-list invalidation events to connected browsers via a new in-process pub/sub bus (`api/session_events.py`) and SSE endpoint (`/api/sessions/events`). Triggered on every visible-session mutation: new (only after worktree creation or first-message reveal), delete, archive, move, pin, rename, duplicate, import, and cron-job completion. Per-subscriber `Queue(maxsize=1)` with latest-wins drain — under burst, browsers see the freshest payload, not a backlog. Disconnect cleanup runs in the handler's `finally:` block, scoped to the shared `_CLIENT_DISCONNECT_ERRORS` tuple so socket-failure modes beyond `BrokenPipeError` also trigger unsubscribe. Cron-scheduler integration uses a `_cron_profile_context_depth()` gate to avoid double-publish across manual vs scheduled cron run paths. **Follow-on issue filed:** the bus has no profile identity in its payload — a cron tick in profile A wakes the SSE handler for every browser regardless of which profile its session cookie selects, costing wasted `/api/sessions` round-trips on other-profile tabs (no data leak; the GET is profile-scoped). Filing tracker for a follow-up that snapshots the active profile at publish time. ##### Test infrastructure - HTTP integration tests for `/api/sessions/events`: handshake (200 + `text/event-stream`), event delivery driven by a side-effect POST, rapid open/close burst that survives a subsequent `/api/sessions` GET, and a source-level guard that the handler's `except _CLIENT_DISCONNECT_ERRORS:` clause stays wired to the shared tuple. Added as stage-393 follow-up to Opus advisor's blocking note on PR [#​2637](nesquena/hermes-webui#2637) (the existing tests covered the in-process bus but never opened a real HTTP connection). ### [`v0.51.99`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05199--2026-05-20--Release-BW-stage-392--5-PR-batch--compact-tool-activity-grouping--CLI-sidebar-scan-cap--title-generation-API-key-forwarding--post-compression-replay-dedup--clarify-popup-stability) [Compare Source](nesquena/hermes-webui@v0.51.98...v0.51.99) ##### Fixed - **PR [#​2638](nesquena/hermes-webui#2638 by [@​dobby-d-elf](https://github.com/dobby-d-elf) — Keep compact tool activity grouped under a single Activity disclosure within an assistant turn. The compact renderer was splitting Thinking and interim tool activity into multiple visible fragments inside the same turn. Composes cleanly with v0.51.96's [#​2620](nesquena/hermes-webui#2620) (second-level dedup key) — the bundled `merge_session_messages_append_only()` change now only adds to `seen_message_keys` when the key is an authoritative `message_id`-prefixed key, preserving the v0.51.96 invariant that two distinct state-only same-second rows must remain visible. - **PR [#​2647](nesquena/hermes-webui#2647 by [@​Michaelyklam](https://github.com/Michaelyklam) (closes [#​2628](nesquena/hermes-webui#2628)) — Cap the CLI/agent session sidebar bridge to a recent-candidate window before joining message rows. Previously `show_cli_sessions` ran a `LEFT JOIN messages ... GROUP BY s.id` across the entire Hermes `state.db`, aggregating 100k+ message rows before applying the visible sidebar cap. Large installs paid that cost on every sidebar read. The capped path now selects an oversampled candidate set (8× the visible limit) ordered by `started_at DESC`, then runs the message aggregate inside that window. Uncapped callers (full scans, exports) are unchanged. - **PR [#​2650](nesquena/hermes-webui#2650 by [@​starship-s](https://github.com/starship-s) — Forward the configured `auxiliary.title_generation.api_key` for config-derived title routes. Completes [#​2612](nesquena/hermes-webui#2612) / v0.51.96 — the prior PR routed provider/model/base\_url through but the API key was left to fall back to the chat client's key, which doesn't match the Hermes Agent task-config shape and silently failed for setups where the title model lives behind a different account. The new `caller_supplied_route` guard prevents leakage of the title-generation key to caller-supplied (active-agent fallback) routes. - **PR [#​2651](nesquena/hermes-webui#2651 by [@​LumenYoung](https://github.com/LumenYoung) (refs [#​1217](nesquena/hermes-webui#1217)) — Dedupe replayed active-context tails before appending agent result deltas to the WebUI display transcript, and apply the same replay protection to persisted `context_messages`. Without this, post-compression continuation re-fed an already-present tail into the next model turn, inflating the model-facing context and bloating the visible transcript with duplicate assistant cards. - **PR [#​2643](nesquena/hermes-webui#2643 by [@​arshkumarsingh](https://github.com/arshkumarsingh) (closes [#​2639](nesquena/hermes-webui#2639)) — Require a stable `clarify_id` and wait for the backend ack before hiding the WebUI "Clarification needed" popup. Three bugs conspired to cause stale clarifications to silently fail: (a) `_ClarifyEntry` had no unique identifier so the frontend couldn't reference a specific pending prompt; (b) the backend used FIFO resolution which silently dropped late/stale responses; (c) the frontend hid the popup before the POST returned, so users saw a successful submit while the agent fell back to its best-judgement timeout path. Server-side `clarify_id` is now generated in `_ClarifyEntry.__init__` (UUID-based), propagated through SSE/poll payloads, sent back by the browser, and matched via `resolve_clarify_by_id()`. The popup stays visible until the POST returns; a 409/`stale:true` response keeps the draft and shows a toast. The legacy `not bool(clarify_id)` quirk that always returned `ok:true` is gone. ### [`v0.51.98`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05198--2026-05-20--Release-BV-stage-391--1-PR-follow-on--customproviders-allowlist-priority-over-live-v1models) [Compare Source](nesquena/hermes-webui@v0.51.97...v0.51.98) ##### Fixed - **PR [#​2640](nesquena/hermes-webui#2640 by [@​colin-chang](https://github.com/colin-chang) — When a `custom_providers` entry in `config.yaml` declares a curated `models:` allowlist (e.g. a ZenMux or other aggregator gateway), respect the curated list instead of also fetching the live `/v1/models` catalog. Without this guard the picker rendered hundreds of online models alongside the user's curated three, swamping the intended selection. The allowlist guard skips the live probe entirely; the existing fall-through to a live probe still runs when no `models:` list is configured. Composes cleanly with [#​2626](nesquena/hermes-webui#2626) / v0.51.96 — when the live probe is skipped, no `models_endpoint_error` is surfaced (the curated list is the source of truth and probe failures should not show as a user-facing diagnostic in that case). ### [`v0.51.97`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05197--2026-05-20--Release-BU-stage-390--2-PR-batch--startup-session-index-rebuild--config-managed-custom-provider-cards) [Compare Source](nesquena/hermes-webui@v0.51.96...v0.51.97) ##### Fixed - **PR [#​2642](nesquena/hermes-webui#2642 by [@​dso2ng](https://github.com/dso2ng) — Rebuild the WebUI session index during startup recovery when `_index.json` is missing, even when no `.bak` session restore occurs. Previously the rebuild only ran after a `.bak` restore path, leaving large state directories on a repeated full-scan `/api/sessions` fallback after an index loss. Startup recovery now unconditionally calls `_write_session_index()` when no `_index.json` is present on disk, restoring the O(1) sidebar read path. ##### Added - **PR [#​2634](nesquena/hermes-webui#2634 by [@​Michaelyklam](https://github.com/Michaelyklam) (closes [#​2632](nesquena/hermes-webui#2632)) — Show `custom_providers` entries created by `hermes model` (CLI) in Settings → Providers as read-only config-managed provider cards, including their configured models and key status, instead of filtering them out because they are not WebUI-editable API-key providers. The new card variant displays configured models, key-env status (set/unset), and a "config-managed" badge that links to the CLI as the canonical edit surface. </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19--> Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/594
This was referenced May 21, 2026
SysAdminDoc
pushed a commit
to SysAdminDoc/hermes-webui
that referenced
this pull request
Jun 26, 2026
bernyforce
pushed a commit
to bernyforce/hermes-webui
that referenced
this pull request
Jul 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
context_messages, so compression/continuation does not re-feed an already-present tail into the next model turn.Why this happens
WebUI intentionally keeps two related but different histories:
messages: the visible transcript shown in the UI.context_messages: the model-facing context used to start the next turn.Normally, when the agent returns
result.messages, WebUI can treat it as:and append only
new_turn_deltato the visible transcript/context.During compression or session continuation, however, Hermes rotates/continues the underlying agent session and the state-db-backed result may include the previous model context plus a replay of the active tail before the newly generated turn:
That replayed active tail is not a new user/assistant/tool exchange. It is the same suffix that WebUI already has in
messagesand/orcontext_messages, reconstructed after the compression/continuation boundary.The existing prefix-only merge logic only checked whether
result.messagesstarted withprevious_context. Once that was true, it blindly appended everything after that prefix. In the replay case, that means WebUI appendsreplayed_active_tailas if it were fresh output, duplicating a whole contiguous block of user/assistant/tool messages.Once duplicated in
context_messages, the duplicated block is sent into the next turn as model context. That makes token usage jump unexpectedly after what appears to be a small/simple exchange, and can cause repeated compaction on subsequent turns.When this happens
This is most likely to occur when all of the following are true:
result.messagessequence that contains the previous context prefix.Before this fix, WebUI saw the whole suffix after
previous_contextas new, so it persisted the replayed A block again. The bug is not normal conversation growth; it is a merge/reconciliation artifact at compression/continuation boundaries.State invariant repaired
After compression or session continuation, WebUI may receive a result whose prefix is the previous model context followed by a replay of the active tail from state.db. That replay is not a new turn.
Both histories should remain append-only with respect to genuinely new turns:
messages) should not duplicate an already-visible suffix.context_messages) should not re-append an already-present suffix and feed it back into the next model call.This PR strips the largest exact suffix/prefix overlap before appending result deltas. It applies the same protection to the display transcript merge and to persisted
context_messages.Test plan
python -m pytest -q tests/test_issue1217_transcript_compaction.pypython -m pytest -q tests/test_session_save_mode.py tests/test_issue1217_transcript_compaction.pygit diff --checkgit merge-tree --write-tree origin/master HEAD