fix(session): use second-level timestamp granularity in legacy dedup key - #2620
1 commit merged into
Conversation
The _normalized_message_timestamp_for_key helper was preserving microsecond precision (%.6f). When the same message is persisted by both the WebUI sidecar JSON writer and the Hermes agent state.db writer, their timestamps can differ by a few microseconds, causing _session_message_merge_key to produce different keys for the same logical message and letting both copies survive the dedup pass in merge_session_messages_append_only. Truncating to second-level granularity collapses sub-second drift to the same key, so the duplicate is suppressed correctly. Fixes nesquena#2616
SummaryReading the two-dot diff origin/master..HEAD -- api/models.py (what would actually land if this merges), the change is a clean one-liner that drops the microsecond fallback in Code referenceWhat the merge actually changes in except (TypeError, ValueError):
return str(value)
- if timestamp.is_integer():
- return str(int(timestamp))
- return ("%.6f" % timestamp).rstrip("0").rstrip(".")
+ # Drop sub-second precision so that timing drift between
+ # the sidecar JSON write and the state.db created_at write does not cause
+ # the legacy dedup key to differ for the same logical message.
+ return str(int(timestamp))The helper feeds into the legacy 5-tuple at DiagnosisThe fix is correct for the symptom in #2616. Sub-second drift between the two stores is the failure mode, and the timestamp string is one component of a 6-tuple that already includes Worth noting alongside this PR: the follow-up comment on #2616 confirms the bug reproduces on fresh v0.51.95 sessions (not only legacy-upgraded ones), which strongly implies state.db is persisting Gap — no testThe PR does not add a regression test. A small addition to def test_legacy_dedup_collapses_subsecond_drift():
sidecar = [{"role": "user", "content": "hi", "timestamp": 1000.000123}]
state = [{"role": "user", "content": "hi", "timestamp": 1000.000789}]
assert len(merge_session_messages_append_only(sidecar, state)) == 1Without that, a future refactor of RecommendationThe fix is sound. I would ask @bengdan to add the regression test above before merge so the dedup contract is pinned. Pairs naturally with PR #2618 (which closes a parallel stale-index gap on the metadata-only polling path) — together they cover both halves of the v0.51.95 reconciliation contract. |
1188206 to
ff0aa69
Compare
|
Is this a backwards compatible fix, do existing chats with duplication get fixed or only does it only prevent duplication in future chats? |
|
@heagandev good question — the answer is "yes, retroactively, for the dominant failure mode in #2616, and no rewrite-on-disk is needed." Why
# api/routes.py:3749-3758
_all_msgs = merge_session_messages_append_only(s.messages, state_db_messages)
...
_all_msgs = merge_session_messages_append_only(cli_messages, sidecar_messages)
...
_all_msgs = merge_session_messages_append_only(_metadata_sidecar, state_db_messages)So as soon as you pull this fix, every subsequent session open re-merges the existing sidecar + state.db rows through the new second-granularity legacy key. Pairs that previously diverged only because the sidecar wrote What it does NOT fix retroactivelyTwo narrow cases survive:
for msg in sidecar_messages:
...
key = _session_message_merge_key(msg)
seen_message_keys.add(key)
merged_messages.append(msg)Sidecar messages are appended unconditionally — the dedup is only applied to the
TL;DR for usersPull the fix, hard refresh, reopen the duplicated session — duplicates created by sub-second drift between sidecar and state.db will disappear from the rendered view without any cleanup step. New sessions won't accumulate them. If a small handful of pre-existing duplicates remain after this and PR #2618 land, they're almost certainly the intra-sidecar variant and would need a separate one-shot cleanup pass (worth filing as a follow-up if it shows up). |
6c60925
… 0.51.96) (#593) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.95` → `0.51.96` | --- ### Release Notes <details> <summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary> ### [`v0.51.96`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05196--2026-05-20--Release-BT-stage-389--8-PR-batch--IPv6-dashboard-link-normalization--configured-title-generation-provider-routing--sidebar-pinned-session-3-cap--external-refresh-sidecar-count-preference--Hermes-overview-docs-relocation--legacy-dedup-timestamp-granularity--custom-provider-models-endpoint-error-surfacing--RuntimeAdapter-Slice-4c-harness-gate-RFC) [Compare Source](nesquena/hermes-webui@v0.51.95...v0.51.96) ##### Fixed - **PR [#​2610](nesquena/hermes-webui#2610 by [@​AJV20](https://github.com/AJV20) — Preserve square brackets around IPv6 hosts when normalizing browser-only dashboard URLs, so links like `http://[::1]:9119` remain valid after saving instead of being mangled into invalid IPv6 forms. Closes the regression introduced by the URL-sanitization path added in [#​2533](nesquena/hermes-webui#2533) / v0.51.95 — bracketed IPv6 hosts now round-trip through the dashboard-link save flow unchanged. - **PR [#​2612](nesquena/hermes-webui#2612 by [@​AJV20](https://github.com/AJV20) — Route WebUI session title generation through the configured `auxiliary.title_generation` provider, model, and base URL when present in config, instead of leaving the auxiliary client to silently fall back to the chat model. Users who configure a smaller/cheaper model for title generation (e.g. a fast 8B model on a separate provider) now have that selection honored end-to-end. - **PR [#​2618](nesquena/hermes-webui#2618 by [@​LumenYoung](https://github.com/LumenYoung) — Prefer the persisted sidecar `message_count` over the session-index stored count during external-refresh polling. The metadata-only `/api/session?messages=0` path now reads `Session._metadata_message_count` when sidecar data is available, so legacy sessions whose state.db retains old rows still trip the external-refresh signal correctly on sidecar updates. Composes cleanly with [#​2604](nesquena/hermes-webui#2604) (the legacy-fallback only applies when the reconciled merged count is zero). - **PR [#​2620](nesquena/hermes-webui#2620 by [@​bengdan](https://github.com/bengdan) — Use second-level timestamp granularity in the legacy message-dedup key. Drops the microsecond fallback in `_normalized_message_timestamp_for_dedup_key()` so transcripts that encode timestamps at different sub-second precisions (e.g. `"10.0"` vs `10.000000`) collapse to the same dedup bucket. Retroactively de-duplicates the dominant failure mode in [#​2616](nesquena/hermes-webui#2616) without requiring an on-disk session rewrite. - **PR [#​2626](nesquena/hermes-webui#2626 by [@​Michaelyklam](https://github.com/Michaelyklam) (closes [#​2540](nesquena/hermes-webui#2540)) — Surface named custom-provider `/models` endpoint failures in the model picker instead of silently showing an empty provider group. `_read_custom_endpoint_models` now returns `(models, error)`, so auth/network/HTTP failures propagate as structured `models_endpoint_error` hints on `/api/models` per affected provider. The composer model picker renders the hint as a quiet disabled-option diagnostic; configured fallback models remain selectable. 124 LOC of new regression coverage spans 401/network-error/5xx failure modes plus frontend hook validation. ##### Added - **PR [#​2614](nesquena/hermes-webui#2614 by [@​Michaelyklam](https://github.com/Michaelyklam) (refs [#​2508](nesquena/hermes-webui#2508)) — Cap sidebar-active pinned sessions at three. Right-clicking a conversation row opens the existing action menu, attempted pins beyond the cap render the menu item as disabled with an explanatory tooltip, and the backend rejects a fourth pin attempt with a structured error so the optimistic frontend can roll back the click. Settles the open question from [#​2508](nesquena/hermes-webui#2508) on whether pin count is bounded — the answer is three, configurable in a future PR if user demand surfaces. ##### Documentation - **PR [#​2619](nesquena/hermes-webui#2619 by [@​Michaelyklam](https://github.com/Michaelyklam) (closes [#​2595](nesquena/hermes-webui#2595)) — Move the long human-facing Hermes comparison document from root `HERMES.md` to `docs/why-hermes.md` so Hermes Agent sessions opened in this repository load `AGENTS.md` as the project-specific assistant guidance instead of the marketing overview. README links now point to the new docs path and a regression test (`tests/test_agent_context_docs.py`) prevents root `HERMES.md` / `.hermes.md` context files from silently reappearing. - **PR [#​2627](nesquena/hermes-webui#2627 by [@​Michaelyklam](https://github.com/Michaelyklam) (refs [#​1925](nesquena/hermes-webui#1925)) — Advance the RuntimeAdapter RFC after the Slice 4b `RunnerRuntimeAdapter` facade shipped in v0.51.94. The RFC now defines the next Slice 4c runner-backend harness gate: feature-flagged runner backend selection, explicit start payload validation, durable status/event observation across WebUI adapter recreation, bounded controls, and a deterministic harness for proving the facade's protocol-translation invariants without requiring the future runner/sidecar to exist. </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/593
… 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
…ges so the agent doesn't see duplicates (follow-up to v0.51.96 nesquena#2620) Co-authored-by: AlexeyDsov <AlexeyDsov@users.noreply.github.com>
…➔ 0.51.106) (#614) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.105` → `0.51.106` | --- ### Release Notes <details> <summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary> ### [`v0.51.106`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051106--2026-05-21--Release-CD-stage-399--3-PR-batch--restamped-statedb-replay-dedupe--contextmessages-dedupe-so-agent-doesnt-see-duplicates--empty-partial-bloat-fix) [Compare Source](nesquena/hermes-webui@v0.51.105...v0.51.106) ##### Fixed - **PR [#​2686](nesquena/hermes-webui#2686 by [@​ai-ag2026](https://github.com/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](nesquena/hermes-webui#2705 by [@​AlexeyDsov](https://github.com/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](nesquena/hermes-webui#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](nesquena/hermes-webui#2704 by [@​wirtsi](https://github.com/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. </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/614
…ges so the agent doesn't see duplicates (follow-up to v0.51.96 nesquena#2620) Co-authored-by: AlexeyDsov <AlexeyDsov@users.noreply.github.com>
…ges so the agent doesn't see duplicates (follow-up to v0.51.96 nesquena#2620) Co-authored-by: AlexeyDsov <AlexeyDsov@users.noreply.github.com>
Summary
Fix duplicate messages in chat (closes #2616).
_normalized_message_timestamp_for_keywas preserving microsecond precision (%.6f). When the same message is persisted by both the WebUI sidecar JSON writer and the Hermes agentstate.dbwriter, their timestamps can differ by a few microseconds. This caused_session_message_merge_keyto produce different keys for the same logical message, letting both copies survive the dedup pass inmerge_session_messages_append_only— resulting in every user message and AI response appearing twice in the UI.Fix
Truncate to second-level granularity (
int(timestamp)) so sub-second drift between the two writers collapses to the same key.Reproduction
v0.51.90 is not affected.
Closes #2616
@nesquena Could you review and merge? One-line fix for the regression introduced in stage-387/388.