Conversation
Two fixes in api/streaming.py: 1. _find_current_user_turn: return LAST match instead of first. After context compression, result_messages contain the full conversation history. If the user asked a similar question in an earlier turn, first-match would return that old index, causing the merge to replay the entire history from that point as new messages. 2. _merge_display_messages_after_agent_result: add general seen-set dedup. The existing dedup only handled 3 specific cases (current user checkpoint, adjacent same assistant, compression marker). Old messages from prior compression cycles with matching identity but not fitting those 3 cases were appended as duplicates. Added a general check: skip any candidate whose identity is already in the seen set (unless it's the current user turn which has its own checkpoint handling).
|
Reading the diff at The two changes do different thingsChange 1 — Change 2 — the unconditional seen-set skip — is the problem. It lives in the shared candidate loop, so it runs in both branches, including the prefix branch: if (
key is not None
and key in seen
and not ((key == current_user_key) or is_current_user_turn)
):
continue
Why this breaks an existing invariantThe immediately-preceding adjacent-assistant check (streaming.py:3393-3403) is deliberately adjacency-scoped, with this comment: The new block discards exactly that scoping. Trace
This takes the prefix branch ( So I'd expect this test (and likely RecommendationAfter the last-match fix,
A regression test mirroring |
The general seen-set safety net in the candidate loop was running in both the prefix (normal append) and compression branches. In the prefix branch, it could drop a legitimate second assistant message whose text happened to match an earlier turn's answer — breaking the invariant that identical answers in separate user turns remain visible. After the last-match fix to _find_current_user_turn, turn_candidates already starts at the current turn, so old history is excluded from the delta. The general net is no longer needed. Thanks to @nesquena-hermes for the detailed review.
|
Great catch, thank you. You're right — the general seen-set net was breaking the prefix branch invariant. I reproduced the exact failure you described: Removed the over-broad safety net in 59f36a2. The last-match fix to All 81 tests pass including |
## Release v0.51.235 — Release HC (stage-q5) Single fix in the data-sensitive transcript-merge path, with a Codex-found MUST-FIX applied. ### Fix | PR | Author | Fix | |----|--------|-----| | #3468 | @jasonjcwu | `_find_current_user_turn` returns the **last** matching user turn instead of the first, so post-compression `result_messages` (which carries the full history) no longer replays the entire conversation when the user repeats a similar question (137-msg session → 89 duplicate replays). | ### Pre-release review caught a CORE issue (fixed before ship) Codex's regression gate found that a naive last-match could be overridden by a **synthetic `role:"user"` continuation prompt** (the agent loop injects "Continue"/empty-recovery nudges — verified at `conversation_loop.py:1763/4183/4356`) that only *substring*-matches the user text — anchoring the merge **past** the real turn and dropping the assistant/tool output in between. Applied the fix: track **strong** (exact `_looks_like_current_user_turn`) and **weak** (substring) matches separately and return `last_strong → last_weak → fallback`. The real turn (strong) always wins over a later synthetic continuation (weak). Opus reviewed the original and said ship; the applied fix is strictly safer than what it reviewed. ### Gate results - **Full pytest suite**: 7465 passed, 8 skipped, 3 xpassed, **0 failed** - **ruff forward gate**: CLEAN - **browser-smoke gate**: CLEAN - **Codex (regression)**: SHIP ONLY WITH FIXES → fix applied → re-reviewed **SAFE TO SHIP** - **Opus (correctness)**: SHIP IT (on the pre-fix code; applied fix is strictly safer) - **Regression test** (`tests/test_issue3468_duplicate_after_compression.py`, 7 cases): pins the last-match behavior, the strong-beats-later-weak invariant, and the end-to-end no-duplicate-replay invariant — each **verified to fail against the pre-fix logic**. Co-authored-by: jasonjcwu <jasonjcwu@users.noreply.github.com>
|
Shipped in v0.51.235 (Release HC) via release PR #3490 — thank you @jasonjcwu! 🙏 Your fix to One adjustment absorbed on the way in: the pre-release Codex regression gate found that a naive last-match could be overridden by a synthetic Note: the PR body also described a "general seen-set dedup" change, but that wasn't in the committed diff — only the Authorship preserved via |
…inst live worker state (#3492) ## Release v0.51.237 — Release HE (stage-q8) Phase 3 MEDIUM-ring pick (3-factor framework): a real concurrency/state-consistency fix from a regular contributor (@franksong2702, ★★★ 38 merges). The hardest review of the sweep — the dual gate caught **two** silent data-loss bugs across two review rounds. ### Fixed | PR | Author | Fix | |----|--------|-----| | #3475 | @franksong2702 | Cancelling a live turn immediately after send now reliably stops the worker and settles the session to a cancelled state (was: spinner over a blank page). `cancel_stream()` falls back to the live active-run registry (`ACTIVE_RUNS`) + session agent cache when `STREAMS` has already detached, so the worker still receives `interrupt("Cancelled by user")`. `/api/session` reports run-journal active state from the live registry instead of trusting a persisted `active_stream_id`. | ### Pre-release dual gate caught TWO silent data-loss bugs (both fixed + regression-tested) The PR's refactor moved `agent.interrupt()` ahead of the partial-text/reasoning/tool-call snapshot, and that snapshot was no longer under `streams_lock`: 1. **Codex round 1** — the worker's `finally` (which pops `STREAM_PARTIAL_TEXT`/`STREAM_REASONING_TEXT`/`STREAM_LIVE_TOOL_CALLS` under `STREAMS_LOCK`) could clear those buffers the instant `interrupt()` wakes it, so a cancelled turn **silently lost its already-streamed text**. (Opus reviewed the original and said ship — it assumed the snapshot was still lock-protected; the stale comment claimed so but the code wasn't. Verified against the actual code → Codex was right.) 2. **Codex round 2** — my first fix only snapshotted on the `STREAMS`-present path; the detached `ACTIVE_RUNS`-only path (the case this PR adds) still lost text. Fixed by hoisting the snapshot above the `if stream_present` branch so it runs unconditionally under the lock. Both fixes have regression tests **verified to fail against the buggy versions** (`test_cancel_preserves_partial_text_when_interrupt_pops_buffers` + `test_cancel_preserves_partial_text_on_detached_active_run_path`). ### Gate results - **Full pytest suite**: 7481 passed, 9 skipped, 3 xpassed, **0 failed** - **ruff forward gate**: CLEAN - **browser-smoke gate**: CLEAN - **Codex (regression)**: SHIP ONLY WITH FIXES (×2 rounds) → both MUST-FIXes applied → re-reviewed **SAFE TO SHIP** - **Opus (correctness)**: reviewed the original (SAFE); the shipped version is strictly safer (adds the under-lock snapshot Opus deemed unnecessary) - Deadlock concern cleared: no path takes `ACTIVE_RUNS_LOCK` then `STREAMS_LOCK`. Rebased onto current master (streaming.py/routes.py merged clean — no overlap with the #3468 dedup change shipped in HC). Closes #3475. Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
…➔ 0.51.252) (#813) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.230` → `0.51.252` | --- ### Release Notes <details> <summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary> ### [`v0.51.252`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051252--2026-06-03--Release-HT-stage-q24--selection-bleed-fix--compatibility-docs) [Compare Source](nesquena/hermes-webui@v0.51.251...v0.51.252) ##### Fixed - The floating "selected-text reply" button no longer lets its own label get caught in a text selection (`user-select:none`), so dragging a selection near the button doesn't bleed into it. ([#​2481](nesquena/hermes-webui#2481), [@​rodboev](https://github.com/rodboev)) ##### Docs - README now has a **Compatibility** section documenting that the WebUI is tested against the matching hermes-agent release and that both should be upgraded together (until the stable agent API [#​2491](nesquena/hermes-webui#2491) lands). ([@​rodboev](https://github.com/rodboev)) ### [`v0.51.251`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051251--2026-06-03--Release-HS-stage-q23--composer--path-autocomplete) [Compare Source](nesquena/hermes-webui@v0.51.250...v0.51.251) ##### Fixed - Typing a `~/` path token in the composer (e.g. `check this file ~/`) now opens a home-directory path-suggestion dropdown, matching the TUI's path completion. It reuses the existing slash-command dropdown (positioning + keyboard nav) and the server's trusted `/api/workspaces/suggest` endpoint, and only replaces the matched path token on selection (surrounding message text is preserved). Slash-command autocomplete still takes precedence for `/`-prefixed input. ([#​3433](nesquena/hermes-webui#3433), [@​puneetdixit200](https://github.com/puneetdixit200)) ### [`v0.51.250`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051250--2026-06-03--Release-HR-stage-q22--Zeus-appearance-skin) [Compare Source](nesquena/hermes-webui@v0.51.249...v0.51.250) ##### Added - New **Zeus** appearance skin (Settings → Appearance, or `/theme skin zeus`) — OLED-near-black dark surfaces that keep the default gold accent, for a high-contrast "gold on black" look that no existing skin offered. All visual changes are scoped to `data-skin="zeus"`; it's dark-focused and falls back to the default light palette in light mode. ([#​3328](nesquena/hermes-webui#3328), [@​heagandev](https://github.com/heagandev)) ### [`v0.51.249`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051249--2026-06-03--Release-HQ-stage-q21--auto-expand-terminal-on-output-toggle) [Compare Source](nesquena/hermes-webui@v0.51.248...v0.51.249) ##### Added - New **"Auto-expand terminal on output"** preference (Settings → Preferences, **off by default**). When enabled, the collapsed embedded terminal panel surfaces itself automatically the first time a running command emits output, so long-running command output isn't silently collected behind a collapsed panel. The auto-expand does not steal focus from the composer, and fires once per stream (not per output chunk). Mirrors the existing `simplified_tool_calling` setting pattern; default-off means no behavior change on upgrade. ([#​2974](nesquena/hermes-webui#2974), [@​rodboev](https://github.com/rodboev)) ### [`v0.51.248`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051248--2026-06-03--Release-HP-stage-q20--self-heal-deleted-WebUI-sessions-instead-of-bricking-the-chat) [Compare Source](nesquena/hermes-webui@v0.51.247...v0.51.248) ##### Fixed - A WebUI session whose sidecar was deleted server-side (e.g. after `docker compose --force-recreate`) but whose messages still live in `state.db` no longer **bricks the chat** — it looked alive (`GET /api/session` returned 200 from a synthesized CLI stub) while every action failed (`POST /api/session/draft` and `/api/chat/start` returned 404). Now the GET handler consults `_index.json` (the canonical WebUI session registry): if the id was a WebUI-origin session (empty/`webui`/`fork` source) whose sidecar is gone, it returns 404 so the client can self-heal — clearing the saved session id and stripping the stale `/session/<id>` URL — and falls through to the welcome screen. Genuine CLI-origin sessions keep their existing read-only stub. The client self-heal now also covers the mid-session case (the current session's sidecar disappearing), not just boot. ([#​2782](nesquena/hermes-webui#2782), [@​rodboev](https://github.com/rodboev)) ### [`v0.51.247`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051247--2026-06-03--Release-HO-stage-q19--coerce-reasoning-effort-to-model-supported-levels) [Compare Source](nesquena/hermes-webui@v0.51.246...v0.51.247) ##### Fixed - A globally-configured reasoning effort (`agent.reasoning_effort`) is now **coerced to the closest level the active model/provider actually supports** before each request, instead of being sent verbatim and rejected. For example `openai-codex` `gpt-5` rejects `max` (now degraded to `xhigh`) and `o1`/`o3`/`o4` only accept `low`/`medium`/`high` (so `max`/`xhigh` degrade to `high`). Coercion only ever steps *down* to a supported level (never escalates), and `none`/unset are preserved. The model/provider effort-capability filter is applied consistently across the heuristic, models.dev metadata, GitHub Copilot, and LM Studio detection paths. ([#​3505](nesquena/hermes-webui#3505), [@​franksong2702](https://github.com/franksong2702)) ### [`v0.51.246`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051246--2026-06-03--Release-HN-stage-q18--WebUI-rename-syncs-to-agent-statedb) [Compare Source](nesquena/hermes-webui@v0.51.245...v0.51.246) ##### Fixed - Renaming a session in the WebUI now writes the new title through to the agent's `state.db`, so the TUI and CLI no longer keep showing the old name. The `/api/session/rename` handler now calls `_sync_session_title_to_insights()` (gated on the `sync_to_insights` setting) — exactly like the sibling `/api/session/title/regenerate` handler already did. ([#​3225](nesquena/hermes-webui#3225), [@​rodboev](https://github.com/rodboev)) ### [`v0.51.245`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051245--2026-06-03--Release-HM-stage-q17--messaging-source-badge-in-chat-topbar) [Compare Source](nesquena/hermes-webui@v0.51.244...v0.51.245) ##### Fixed - Messaging sessions (Telegram, Discord, WeChat, etc.) now show their platform source badge in the **chat-pane topbar**, not just the sidebar. The topbar badge was gated on `is_cli_session`, which is intentionally `false` for messaging sources, so the badge silently disappeared once you opened the session. The gate is removed; a recovered native session whose sidecar stamps `source_label: "WebUI"` is still left un-badged (it isn't a foreign source). ([#​3338](nesquena/hermes-webui#3338), [@​rodboev](https://github.com/rodboev)) ### [`v0.51.244`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051244--2026-06-03--Release-HL-stage-q16--workspace-OS-import-drop--composer-drop-zone-polish) [Compare Source](nesquena/hermes-webui@v0.51.243...v0.51.244) ##### Added - **Drop OS files/folders onto a specific workspace folder row or breadcrumb segment** to upload into that directory (not only the current directory). OS folder drops are traversed via `webkitGetAsEntry`/`readEntries` and their nested structure is preserved on upload. Composer `@path` drags ([#​1097](nesquena/hermes-webui#1097)), the internal tree-move ([#​3402](nesquena/hermes-webui#3402)), and OS-drop isolation ([#​3411](nesquena/hermes-webui#3411)) are all preserved. ([#​3402](nesquena/hermes-webui#3402), [#​3424](nesquena/hermes-webui#3424), [@​pamnard](https://github.com/pamnard)) ##### Fixed - The composer drop-zone overlay no longer looks garbled when you drag a workspace file (or OS file) over the footer. Previously the translucent overlay let the textarea, attach/mic icons, and model/profile chips bleed through and collide with the hint text. The overlay is now a clean, fully-opaque box with a single centered, context-aware label — **"Drop to insert workspace reference"** when dragging a workspace file (which inserts an `@path` reference) vs **"Drop files to attach"** for an OS file (which attaches it to the message). ### [`v0.51.243`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051243--2026-06-03--Release-HK-stage-q15--drag-to-move-files-within-the-workspace) [Compare Source](nesquena/hermes-webui@v0.51.242...v0.51.243) ##### Added - You can now **drag a file or folder in the workspace tree onto another folder row (or a breadcrumb segment) to move it** within the workspace. A new `POST /api/file/move` performs the move server-side, confined to the workspace root (`safe_resolve` on both source and destination, rejects `..` destinations, and refuses to move a folder into itself or a descendant). Name collisions and no-op moves are handled, and the drop handlers use `stopPropagation` so the existing composer `@path` drag ([#​1097](nesquena/hermes-webui#1097)) and OS-file upload-on-drop ([#​3411](nesquena/hermes-webui#3411)) are unchanged. ([#​3402](nesquena/hermes-webui#3402), [#​3422](nesquena/hermes-webui#3422), [@​pamnard](https://github.com/pamnard)) ### [`v0.51.242`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051242--2026-06-03--Release-HJ-stage-q14--Graphite-skin) [Compare Source](nesquena/hermes-webui@v0.51.241...v0.51.242) ##### Added - New **Graphite** appearance skin — a quiet, neutral-gray "workbench" alternative to the default gold/cream, selectable from Settings → Appearance (and `/theme skin graphite`). All visual changes are scoped to `data-skin="graphite"` so the default appearance is unchanged; the skin ships both light and dark palettes built on the existing CSS-variable token system (no new dependency or build step). Tightens typography, shadows, active-sidebar spacing, and code-block framing, and uses a neutral gray palette rather than an olive-tinted one. ([#​3440](nesquena/hermes-webui#3440), [@​t3chn0pr13st](https://github.com/t3chn0pr13st)) ### [`v0.51.241`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051241--2026-06-03--Release-HI-stage-q13--New-Chat-returns-to-your-unsent-draft-after-visiting-history) [Compare Source](nesquena/hermes-webui@v0.51.240...v0.51.241) ##### Fixed - Starting a **New Chat** draft, peeking at a previous conversation, then clicking **New Chat** again no longer loses your unsent prompt. Zero-message New Chat sessions are intentionally hidden from the sidebar, so after you navigated away there was no way back to the empty session that held your draft — New Chat just created another fresh empty session and the draft was stranded. The New Chat entrypoint now remembers the candidate empty draft session (a single `localStorage` pointer) and, before creating a fresh session, re-validates it through `/api/session` and routes back only if it is still a safe empty draft (zero messages, no active stream, no pending message, not worktree-backed, matching profile, and a non-empty server-side `composer_draft`). The composer draft is also flushed to the server before a session switch so typing and immediately navigating away can't drop it. Clearing the draft (e.g. after sending) clears the pointer, so an emptied draft never traps you on New Chat. ([#​3333](nesquena/hermes-webui#3333), [#​3471](nesquena/hermes-webui#3471), [@​starGazerK](https://github.com/starGazerK)) ### [`v0.51.240`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051240--2026-06-03--Release-HH-stage-q12--mobile-swipe-up-stops-streaming-auto-scroll) [Compare Source](nesquena/hermes-webui@v0.51.239...v0.51.240) ##### Fixed - On mobile/touch devices you can now swipe up to stop the auto-scroll-during-streaming behavior. Previously the stream snapped back to the bottom on every token and there was no way to read earlier content while a response was arriving: `_recordNonMessageScrollIntent()` only detected upward intent on the wheel path (`typeof e.deltaY === 'number'`), but touch events carry no `deltaY`, so a finger swipe never unpinned the view. The handler now tracks the `touchstart` Y position and treats a `touchmove` that moves the finger up by >8px as upward-scroll intent — the same authoritative unpin (`_messageUserUnpinned`) the wheel path uses — so auto-follow stops until you scroll back to the bottom or tap the ↓ button. ([#​3470](nesquena/hermes-webui#3470), [@​cnogrin](https://github.com/cnogrin)) ### [`v0.51.239`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051239--2026-06-03--Release-HG-stage-q10--ignore-SIGPIPE-so-a-dropped-client-cant-kill-the-server) [Compare Source](nesquena/hermes-webui@v0.51.238...v0.51.239) ##### Fixed - The server no longer dies silently when a client drops the connection mid-response. Python's default action for `SIGPIPE` is `Term`, so a single broken-pipe `socket.send()` in any `ThreadingHTTPServer` worker thread (browser tab closed mid-stream, network drop, mobile backgrounding, a dropped long-poll, an `/api/updates/check` timeout) could terminate the entire WebUI process — no exception, no log, no `/health` response. `server.py` now sets `SIGPIPE` to `SIG_IGN` at import time: the kernel surfaces the broken pipe as a catchable `BrokenPipeError`, the per-request handler unwinds, the connection closes, and the server keeps serving. The handler is `getattr`-guarded so it is a no-op on Windows, where `SIGPIPE` does not exist (preserves native-Windows support, [#​1952](nesquena/hermes-webui#1952)) (salvaged from [#​3407](nesquena/hermes-webui#3407), [@​PatrickNoFilter](https://github.com/PatrickNoFilter)). ### [`v0.51.238`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051238--2026-06-03--Release-HF-stage-q9--New-Conversation-hits-the-fast-path-on-cold-start) [Compare Source](nesquena/hermes-webui@v0.51.237...v0.51.238) ##### Fixed - Clicking **New Conversation** on a cold start no longer hangs for 3–4s on a catalog rebuild. `POST /api/session/new`'s fast path (`_resolve_compatible_session_model_state`) returns immediately only when the request carries both a `model` and a truthy `model_provider`; on a cold/unhydrated dropdown the client sent `model_provider=null`, so the request fell into `get_available_models()` and rebuilt the full catalog (the "first click slow, later clicks fast" asymmetry from [#​2518](nesquena/hermes-webui#2518)). `newSession()` (`static/sessions.js`) now falls back to `window._activeProvider` (then the previous session's `model_provider`) when the dropdown option carries no provider, so the first click takes the fast path too. **Two guards keep this safe:** (1) a slash-qualified (`gemini/…`) or `@provider:model` slug already carries a foreign provider namespace from a prior backend, so the fallback deliberately leaves `model_provider=null` for those; (2) even a *bare* model can carry a known family prefix (`gpt`→openai, `claude`→anthropic, `gemini`→google) — if that family maps to a different provider than the fallback we'd attach, `model_provider` is left null too. Both cases preserve the server slow-path's family-aware cross-provider repair rather than silently re-pointing the new session at the wrong backend ([#​2518](nesquena/hermes-webui#2518) follow-up, [@​franksong2702](https://github.com/franksong2702)). ### [`v0.51.237`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051237--2026-06-03--Release-HE-stage-q8--reconcile-early-cancel-against-live-worker-state) [Compare Source](nesquena/hermes-webui@v0.51.236...v0.51.237) ##### Fixed - Cancelling a live turn immediately after sending now reliably stops the worker and settles the session to a cancelled state, instead of leaving the UI showing a running spinner over a blank session page. The bug was an early-cancel race: the browser SSE could detach (removing the entry from `STREAMS`) before the worker was fully reflected there, so `cancel_stream()` returned early and never interrupted the agent. `cancel_stream()` now falls back to the live active-run registry (`ACTIVE_RUNS`) and the session agent cache when `STREAMS` has already detached, so the worker still receives `interrupt("Cancelled by user")` and the session is cleaned up. Relatedly, `/api/session` now reports run-journal active state from the live active-run registry rather than treating any persisted `active_stream_id` as proof the worker is still alive ([#​3475](nesquena/hermes-webui#3475), [@​franksong2702](https://github.com/franksong2702)). ### [`v0.51.236`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051236--2026-06-03--Release-HD-stage-q7--native-Windows-support-for-bootstrap-and-terminal) [Compare Source](nesquena/hermes-webui@v0.51.235...v0.51.236) ##### Added - Native Windows support for `bootstrap.py` and the embedded terminal ([#​1952](nesquena/hermes-webui#1952)). Hermes WebUI already ran on Windows when invoked as `python server.py` directly; this unblocks the supported `python bootstrap.py` path. `api/terminal.py` no longer hard-imports the POSIX-only `fcntl`/`termios`/`select` at module load — they're guarded behind `_TERMINAL_SUPPORTED = sys.platform != "win32"`, and the embedded-terminal entry points raise `NotImplementedError` (or no-op) on Windows, following the existing optional-feature guard pattern (`api/turn_journal.py`, `api/providers.py`). The bootstrap native-Windows block becomes a warning instead of a hard `RuntimeError`; auto-install (which shells out to `/bin/bash`) still errors clearly on native Windows (WSL is unaffected), and the foreground launch path uses `subprocess.Popen` + exit on Windows (where `os.execv` spawns rather than replaces the process, orphaning it from a supervisor) instead of `os.execv`. POSIX behavior is unchanged on every path ([#​1952](nesquena/hermes-webui#1952), [@​rodboev](https://github.com/rodboev)). ### [`v0.51.235`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051235--2026-06-03--Release-HC-stage-q5--no-duplicate-transcript-replay-on-repeated-questions-after-compression) [Compare Source](nesquena/hermes-webui@v0.51.234...v0.51.235) ##### Fixed - The chat transcript no longer accumulates duplicate messages after multiple context-compression cycles when the user asks similar (or identical) questions across turns. `_find_current_user_turn` (`api/streaming.py`) located the slice point for the current turn's new messages by scanning `result_messages` for the user text — but after compression `result_messages` carries the full conversation history, so a *first*-match scan returned an **older** turn's index, making the merge re-append the entire replayed history from that point (observed: a 137-message session where 89 were duplicate replays, burying the real new messages). It now returns the **last** matching user turn, so the candidate slice begins at the current turn and the replayed history is not re-appended. To stay correct when the agent loop appends synthetic `role:"user"` continuation prompts (e.g. "Continue" / empty-recovery nudges) after the real turn, an exact (strong) match is preferred over a later substring (weak) match — so a synthetic continuation can't anchor the merge past the real turn and drop the assistant/tool output in between. Behavior on the no-match path (fall back to the last user index) is unchanged ([#​3468](nesquena/hermes-webui#3468), [@​jasonjcwu](https://github.com/jasonjcwu)). A regression test pins the unit behavior, the strong-beats-later-weak invariant, and the end-to-end no-duplicate-replay invariant (each verified to fail against the pre-fix logic). ### [`v0.51.234`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051234--2026-06-03--Release-HB-stage-q4--duplicate-instance-startup-guard--remote-terminal-workspace-paths) [Compare Source](nesquena/hermes-webui@v0.51.233...v0.51.234) ##### Fixed - The server now refuses to start when a live instance is already responding on the configured port, instead of silently sharing it (a Windows/macOS hazard where `SO_REUSEADDR` semantics let two processes bind 8787 at once, [#​3289](nesquena/hermes-webui#3289)). Rather than globally disabling `SO_REUSEADDR` (which would brick legitimate fast restarts — `ctl.sh restart` and the `os.execv` self-update path rebind immediately and would hit the TIME\_WAIT window), startup now runs a live-listener probe (`_abort_if_already_serving`): a TCP connect + `GET /health` with a 2s timeout. A live instance answers and startup aborts with a clear message; a dying instance whose socket still lingers in the kernel backlog accepts the connection but never responds, so the probe times out and startup proceeds — preserving fast restart. On Windows, `SO_EXCLUSIVEADDRUSE` is set in a `server_bind()` override to get true exclusive binding (POSIX keeps the inherited `allow_reuse_address = True`) ([#​3289](nesquena/hermes-webui#3289), [@​rodboev](https://github.com/rodboev)). - Remote/SSH terminal profiles can now use target-side workspace paths that don't exist on the WebUI host. Workspace validation/resolution previously `stat()`-ed every path against the WebUI server's local filesystem, so a `terminal.cwd` (or session workspace) living on the remote target was rejected as nonexistent. For profiles whose terminal backend is non-local, paths **under the configured `terminal.cwd`** now pass validation without a server-local existence check, and stale server-local `last_workspace` values are ignored unless they fall under the remote cwd. Local profiles are unchanged — the bypass only fires for remote backends and only for paths contained within `terminal.cwd` ([#​3486](nesquena/hermes-webui#3486), [@​dso2ng](https://github.com/dso2ng)). ### [`v0.51.233`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051233--2026-06-03--Release-HA-stage-q3--session-truncate-keepcount-guard-against-silent-transcript-loss) [Compare Source](nesquena/hermes-webui@v0.51.232...v0.51.233) ##### Fixed - `POST /api/session/truncate` no longer silently wipes a session transcript on a negative `keep_count`, and no longer returns an HTTP 500 on a non-numeric one. `keep_count` fed a bare `int()` straight into the destructive `s.messages = s.messages[:keep]` slice followed by `s.save()`, so a negative value sliced as `messages[:-N]` — **deleting the most recent N messages and persisting the result to disk** (e.g. `keep_count=-5` on a 3-message session wiped the entire transcript and returned HTTP 200). `keep_count` is now validated before the slice — non-integer → `400 "keep_count must be an integer"`, negative → `400 "keep_count must be non-negative"` — mirroring the guard the sibling `/api/session/branch` handler already applies (`keep_count=0` keeps its existing "clear all messages" meaning) ([#​3472](nesquena/hermes-webui#3472), [@​Mubashirrrr](https://github.com/Mubashirrrr)). ### [`v0.51.232`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051232--2026-06-03--Release-GZ-stage-q2--cron-endpoint-query-param-guards--Japanese-locale-translations) [Compare Source](nesquena/hermes-webui@v0.51.231...v0.51.232) ##### Fixed - The cron output (`/api/crons/output`) and cron recent (`/api/crons/recent`) endpoints no longer return a confusing HTTP 500 on a malformed numeric query param. A non-numeric `limit` (e.g. `?limit=abc`) or `since` previously let `int()`/`float()` raise `ValueError` up to the top-level handler; both are now parsed defensively (falling back to their defaults). The cron-output `limit` is also clamped to `[1, 500]` so a negative value can't reach the newest-first `files[:limit]` slice as `files[:-n]` (which would drop the oldest entries — or return an empty list when the magnitude exceeds the count — instead of the newest outputs), mirroring the guard `_handle_cron_run_detail` already uses ([#​3473](nesquena/hermes-webui#3473), [@​Mubashirrrr](https://github.com/Mubashirrrr)). ##### Changed - Japanese (`ja`) locale: translated 80 previously-untranslated UI strings (MCP server controls, tool summaries, and related toasts) from their English fallbacks to Japanese, with all `${…}` interpolation placeholders preserved. No locale keys added or removed ([#​3480](nesquena/hermes-webui#3480), [@​koshikai](https://github.com/koshikai)). ### [`v0.51.231`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051231--2026-06-03--Release-GY-stage-q1--model-extras-tail-resolution--plugins-tab-auto-hide--search-depth-guard--symlink-home-suggestions) [Compare Source](nesquena/hermes-webui@v0.51.230...v0.51.231) ##### Fixed - `/model <name>` can now select a model that lives in the **truncated `extra_models` tail** of a large provider catalog, completing the [#​3368](nesquena/hermes-webui#3368) fix that v0.51.229 left half-done. On Nous-style catalogs with >25 models the picker renders only a featured subset as `<option>` entries and pushes the rest into `extra_models`; the `/model` resolver previously matched only against the rendered `sel.options`, so a bare model living only in the extras tail (e.g. `xiaomi/mimo-v2.5` alongside the featured `xiaomi/mimo-v2.5-pro`) was un-selectable and produced a misleading "did you mean -pro?" toast. A new `_buildModelCandidates()` (`static/commands.js`) now builds the candidate set from the full `/api/models` catalog (featured `models` + `extra_models`) — the same complete list the CLI and `/model` autocomplete use — and an extras-only winner is injected via `_ensureModelOptionInDropdown()` before selection so the correct `model` + `model_provider` persist end-to-end. The [#​3437](nesquena/hermes-webui#3437) tier-guard is fully preserved: a genuinely off-catalog versioned name still refuses to snap to a `-pro`/`-flash` tier and shows the suggestion toast ([#​3368](nesquena/hermes-webui#3368), [@​nesquena-hermes](https://github.com/nesquena-hermes); with [@​garyd9](https://github.com/garyd9), confirmation [@​yutaotie](https://github.com/yutaotie)). - The **Plugins** tab in Settings is now auto-hidden when no plugins are installed (`/api/plugins` returns `empty: true`), and deep-linking to the hidden plugins pane falls back to the Conversation section. The tab reappears automatically when plugins are detected ([#​3457](nesquena/hermes-webui#3457), [@​pix0127](https://github.com/pix0127)). - `GET /api/sessions/search?...&depth=<x>` no longer returns a confusing HTTP 500 on a non-numeric `depth` (e.g. `?depth=deep`) and no longer silently excludes the newest messages on a negative `depth` (which sliced as `messages[:-n]`). `depth` is now parsed defensively and clamped to `>= 0` (0 keeps its existing "search the full transcript" meaning), mirroring the guard sibling handlers already use ([#​3474](nesquena/hermes-webui#3474), [@​Mubashirrrr](https://github.com/Mubashirrrr)). - Workspace path autocomplete now expands `~/` suggestions even when the WebUI process home path is a symlink or alias of the trusted home root, so prefixes like `~/Doc` still list home-directory matches instead of returning an empty dropdown. The typed `~` target is now resolved before the trust comparison ([#​3433](nesquena/hermes-webui#3433), [@​sjh9714](https://github.com/sjh9714)). </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/813
## Release v0.51.235 — Release HC (stage-q5) Single fix in the data-sensitive transcript-merge path, with a Codex-found MUST-FIX applied. ### Fix | PR | Author | Fix | |----|--------|-----| | nesquena#3468 | @jasonjcwu | `_find_current_user_turn` returns the **last** matching user turn instead of the first, so post-compression `result_messages` (which carries the full history) no longer replays the entire conversation when the user repeats a similar question (137-msg session → 89 duplicate replays). | ### Pre-release review caught a CORE issue (fixed before ship) Codex's regression gate found that a naive last-match could be overridden by a **synthetic `role:"user"` continuation prompt** (the agent loop injects "Continue"/empty-recovery nudges — verified at `conversation_loop.py:1763/4183/4356`) that only *substring*-matches the user text — anchoring the merge **past** the real turn and dropping the assistant/tool output in between. Applied the fix: track **strong** (exact `_looks_like_current_user_turn`) and **weak** (substring) matches separately and return `last_strong → last_weak → fallback`. The real turn (strong) always wins over a later synthetic continuation (weak). Opus reviewed the original and said ship; the applied fix is strictly safer than what it reviewed. ### Gate results - **Full pytest suite**: 7465 passed, 8 skipped, 3 xpassed, **0 failed** - **ruff forward gate**: CLEAN - **browser-smoke gate**: CLEAN - **Codex (regression)**: SHIP ONLY WITH FIXES → fix applied → re-reviewed **SAFE TO SHIP** - **Opus (correctness)**: SHIP IT (on the pre-fix code; applied fix is strictly safer) - **Regression test** (`tests/test_issue3468_duplicate_after_compression.py`, 7 cases): pins the last-match behavior, the strong-beats-later-weak invariant, and the end-to-end no-duplicate-replay invariant — each **verified to fail against the pre-fix logic**. Co-authored-by: jasonjcwu <jasonjcwu@users.noreply.github.com>
…inst live worker state (nesquena#3492) ## Release v0.51.237 — Release HE (stage-q8) Phase 3 MEDIUM-ring pick (3-factor framework): a real concurrency/state-consistency fix from a regular contributor (@franksong2702, ★★★ 38 merges). The hardest review of the sweep — the dual gate caught **two** silent data-loss bugs across two review rounds. ### Fixed | PR | Author | Fix | |----|--------|-----| | nesquena#3475 | @franksong2702 | Cancelling a live turn immediately after send now reliably stops the worker and settles the session to a cancelled state (was: spinner over a blank page). `cancel_stream()` falls back to the live active-run registry (`ACTIVE_RUNS`) + session agent cache when `STREAMS` has already detached, so the worker still receives `interrupt("Cancelled by user")`. `/api/session` reports run-journal active state from the live registry instead of trusting a persisted `active_stream_id`. | ### Pre-release dual gate caught TWO silent data-loss bugs (both fixed + regression-tested) The PR's refactor moved `agent.interrupt()` ahead of the partial-text/reasoning/tool-call snapshot, and that snapshot was no longer under `streams_lock`: 1. **Codex round 1** — the worker's `finally` (which pops `STREAM_PARTIAL_TEXT`/`STREAM_REASONING_TEXT`/`STREAM_LIVE_TOOL_CALLS` under `STREAMS_LOCK`) could clear those buffers the instant `interrupt()` wakes it, so a cancelled turn **silently lost its already-streamed text**. (Opus reviewed the original and said ship — it assumed the snapshot was still lock-protected; the stale comment claimed so but the code wasn't. Verified against the actual code → Codex was right.) 2. **Codex round 2** — my first fix only snapshotted on the `STREAMS`-present path; the detached `ACTIVE_RUNS`-only path (the case this PR adds) still lost text. Fixed by hoisting the snapshot above the `if stream_present` branch so it runs unconditionally under the lock. Both fixes have regression tests **verified to fail against the buggy versions** (`test_cancel_preserves_partial_text_when_interrupt_pops_buffers` + `test_cancel_preserves_partial_text_on_detached_active_run_path`). ### Gate results - **Full pytest suite**: 7481 passed, 9 skipped, 3 xpassed, **0 failed** - **ruff forward gate**: CLEAN - **browser-smoke gate**: CLEAN - **Codex (regression)**: SHIP ONLY WITH FIXES (×2 rounds) → both MUST-FIXes applied → re-reviewed **SAFE TO SHIP** - **Opus (correctness)**: reviewed the original (SAFE); the shipped version is strictly safer (adds the under-lock snapshot Opus deemed unnecessary) - Deadlock concern cleared: no path takes `ACTIVE_RUNS_LOCK` then `STREAMS_LOCK`. Rebased onto current master (streaming.py/routes.py merged clean — no overlap with the nesquena#3468 dedup change shipped in HC). Closes nesquena#3475. Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
Problem
After multiple context compression cycles, the UI transcript accumulates duplicate messages. In a session with 137 messages, 89 were duplicates from repeated history replays. The user sees old conversation fragments re-appear, and the actual new messages get buried — looking like messages are "swallowed".
Root Cause
Two issues in
api/streaming.py:1.
_find_current_user_turnreturns first match (not last)When the user asks similar questions across turns (e.g., "推荐一下股票" twice), this function scans forward and returns the first matching user message. After context compression,
result_messagescontains the full history, soturn_candidates = result_messages[first_match:]includes the entire old conversation.2. Merge dedup only handles 3 specific cases
The dedup loop in
_merge_display_messages_after_agent_resultonly skips:Old messages from prior compression cycles with matching identity but not fitting these 3 cases were appended as new messages.
Fix
_find_current_user_turn: Changed to return the last matching user turn instead of the first. This ensuresturn_candidatesonly includes messages from the current turn onward.General seen-set dedup: Added a safety net check — skip any candidate whose
_message_identityis already in theseenset (unless it's the current user turn, which has its own checkpoint handling).Testing