Skip to content

Fix for #2914: stop agent from seeing deleted messages after Edit/Undo (from state.db) - #3102

Closed
AlexeyDsov wants to merge 4 commits into
nesquena:masterfrom
AlexeyDsov:alexeydsov-2026-05-27-session-context-duplicate-fix
Closed

Fix for #2914: stop agent from seeing deleted messages after Edit/Undo (from state.db)#3102
AlexeyDsov wants to merge 4 commits into
nesquena:masterfrom
AlexeyDsov:alexeydsov-2026-05-27-session-context-duplicate-fix

Conversation

@AlexeyDsov

Copy link
Copy Markdown
Contributor

Problem

After EditRetry, or Undo in the WebUI, the agent could still see original messages from state.db even though the UI correctly showed only the truncated conversation. If you asked the agent "what messages do you see?", it would report both the old and new versions.

Note: This issue (#2914) was partially addressed in #2933, which introduced the truncation_watermark mechanism. However, two gaps remained: context_messages was not truncated in sync with messages, and the watermark filter only checked m_ts > watermark, allowing original edited messages (with older timestamps) to slip through.

Root Cause

When a user edits or truncates a message, the WebUI sidecar (.json session file) is updated correctly, but state.db is an append-only store — the original messages remain there forever. During session load, state.db rows are replayed and merged with the sidecar. The truncation_watermark mechanism was supposed to filter out pre-truncation rows, but had two gaps:

  1. context_messages was not truncated alongside messages, so the agent's model-facing context retained rows the user deliberately removed.
  2. Watermark filtering only checked m_ts > watermark — but original edited messages have an older timestamp (before the edit), so they passed through the filter unblocked.

Fix

  • session_ops.pyretry_last() and undo_last() now truncate context_messages in sync with messages.
  • models.pyreconciled_state_db_messages_for_session() now uses the sidecar's message IDs as authoritative — state.db rows whose content duplicates a sidecar message at a different timestamp are deduplicated, and rows preceding the watermark are properly excluded.
  • truncation_watermark is persisted on every truncation operation (Edit, Retry, Undo) and survives reload.

Reproduction Examples

Example 1 — Edit leaks original into context:

  1. Send: "What is the sum of angles in a triangle?" → Agent replies about 180°
  2. Edit the message to: "What is the sum of angles in a square?" → Agent replies about 360°
  3. Send: "List all my messages in this session" → Bug: Agent reports both "triangle" and "square", even though the UI only shows "square"

Example 2 — Edit then Undo leaks original:

  1. Send: "What is the sum of angles in a triangle?" → Agent replies about 180°
  2. Edit the message to: "What is the sum of angles in a square?" → Agent replies about 360°
  3. Send another message: "What is the speed of light?"
  4. Undo — removes the "speed of light" message
  5. Send: "List all my previous messages" → Bug: Agent reports both the original "triangle" message AND the edited "square" message, even though the UI only shows "square"

Testing

12 regression tests in tests/test_issue2914_truncation_watermark.py:

  • Merge filter (3) — state.db tail beyond watermark excluded; empty sidecar with watermark=0 blocks replay; full Edit → new turn → Undo scenario proves original messages no longer leak
  • Truncate endpoint (2)context_messages truncated in sync with messages; proves the bug without the fix
  • Context clamp (5)_clamp_context_to_watermark unit tests: filters beyond watermark, passes through when None, blocks all at zero, preserves messages without timestamp, handles empty input
  • Watermark invariant (1) — watermark is NOT auto-cleared on save() even when newer messages exist

AI Assistance

This fix was developed with assistance from Qwen3.6-27B. The AI helped analyze the reconciliation logic, identify the dual-gap root cause (context_messages sync + watermark timestamp filtering), improve the code and draft the test scenarios.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Review notes — looks right, two small follow-ups before merge

Reading the diff at api/models.py:607-621, api/routes.py:5733-5754, api/streaming.py:2622-2645 plus the 12 regression assertions in tests/test_issue2914_truncation_watermark.py. The dual-gap diagnosis matches what I see in the existing code — _maybe_clear_truncation_watermark() was unconditionally clearing the watermark whenever any message timestamp exceeded it, and the merge filter at models.py:3782-3789 only blocked m_ts > watermark, leaving the older pre-edit timestamps to slip through.

Code reference

The new merge filter at api/models.py:3802-3816:

# When a truncation watermark is active, state.db may contain original
# messages that were replaced by Edit (old content with old timestamp).
# The timestamp-based filter above catches messages AFTER the watermark,
# but messages BEFORE it (like the original pre-edit content) slip through.
if (
    watermark_timestamp is not None
    and timestamp is not None
    and timestamp < watermark_timestamp
    and key not in seen_message_keys
    and _session_message_content_key(msg) not in seen_content_keys
):
    continue

The content_key not in seen_content_keys guard is what protects legitimate pre-watermark history when the sidecar still contains it (e.g. after duplicate where context_messages was deep-copied). It only drops state.db rows whose content is unique to state.db — i.e. truly replaced messages. That's the right shape.

The _clamp_context_to_watermark helper at api/streaming.py:2622-2645 is invoked at all three _next_context_messages = … writeback paths in _run_agent_streaming (lines 5175, 5323, 6201) so it covers the streaming-done, mid-stream tool-result, and tool-loop-end branches. Good.

Two follow-ups worth resolving before merge

1. The _maybe_clear_truncation_watermark() no-op leaves dead code. The current diff turns the method body into pass but keeps the call site at models.py:639 and the helper at :607-621. That's fine for now, but it would be cleaner to either:

Recommendation: delete the call + method. The new test test_maybe_clear_truncation_watermark_does_not_clear_on_save (lines 393-427) pins the invariant from the outside, which is the right place to enforce it.

2. The _clamp_context_to_watermark filter uses m_ts <= _tw_ts (line 2637, generator expression — if (m_ts := _message_timestamp_as_float(m)) is None or m_ts <= _tw_ts). But the test asserts _clamp_context_to_watermark_zero_watermark_blocks_all expects everything dropped when watermark is 0.0. That works only because timestamps >= 1.0 in the test fixtures fail the <= 0.0 check. Worth confirming: is "messages with timestamp == watermark" intended to be kept or dropped?

Looking at the merge filter at models.py:3784-3789: it uses timestamp > watermark_timestamp (strict greater) to drop — so messages at exactly the watermark are kept. The <= in _clamp_context_to_watermark matches that semantics. Consistent. Just worth a one-line comment on streaming.py:2637 cross-referencing the merge filter so the asymmetry (<= to keep vs > to drop) is obvious to the next reader.

Test plan

Beyond what's already in tests/test_issue2914_truncation_watermark.py, one scenario worth thinking about for the next PR if you have time:

  • Edit on a session that has a duplicate child. The fix in PR Fix session duplicate and branch field propagation #3101 adds deep-copy of context_messages + truncation_watermark to duplicate. But what happens if the parent is edited after the duplicate was made? The parent's state.db tail will now contain rows the child wouldn't want to inherit, but the child has its own sidecar so the child's reconcile should be insulated by its own truncation_watermark. Worth at least one cross-session test asserting that case, but probably out of scope for this PR — it's Fix session duplicate and branch field propagation #3101's territory.

Overall: this is a clear, well-investigated fix for a real data-leak bug. The test set is appropriately broad. Once the dead-code cleanup is decided one way or the other, I'd merge.

@AlexeyDsov
AlexeyDsov force-pushed the alexeydsov-2026-05-27-session-context-duplicate-fix branch from 902175a to bc471c8 Compare May 29, 2026 06:31
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Re-review of the force-pushed v2 (bc471c8) — both follow-ups resolved, LGTM

Re-fetched the branch (it was force-pushed to a single amended commit at bc471c8) and diffed it against origin/master. Both items I flagged in the last review are addressed cleanly.

Follow-up 1 (dead-code cleanup) — done, the better way

You took the delete option rather than the documented no-op. _maybe_clear_truncation_watermark() is gone and so is its save() call site. The diff at api/models.py:

-    def _maybe_clear_truncation_watermark(self) -> None:
-        watermark = _message_timestamp_as_float({"timestamp": self.truncation_watermark})
-        ...
-        if max_message_timestamp is not None and max_message_timestamp > watermark:
-            self.truncation_watermark = None

and at the save() body:

-        self._maybe_clear_truncation_watermark()

The invariant is now pinned from the outside by the merge-filter tests rather than an internal no-op — which is exactly where I'd want it. The stale test_maybe_clear_truncation_watermark_does_not_clear_on_save test was correctly dropped along with the method, so there's no dangling reference to a symbol that no longer exists.

Follow-up 2 (the <= vs > asymmetry) — comment added, plus a bonus

api/streaming.py:2638-2640 now carries the cross-reference I asked for, so the boundary semantics are self-documenting:

# Keep m_ts <= watermark (messages AT the watermark boundary are kept).
# Matches the merge filter in models.py merge_session_messages_append_only,
# which drops state.db rows with timestamp > watermark_timestamp (strict).
if (m_ts := _message_timestamp_as_float(m)) is None or m_ts <= _tw_ts

That matches the strict timestamp > watermark_timestamp drop in merge_session_messages_append_only at api/models.py:3782-3784, so a message exactly at the watermark is kept on both the merge path and the clamp path — consistent. The logger.info("clamping context_messages: %d → %d …") line you added is a nice touch for diagnosing this in the field without re-instrumenting.

Test coverage

The _clamp_context_to_watermark unit tests now cover the boundary cases the comment describes — test_clamp_context_to_watermark_zero_watermark_blocks_all (tests/...:343), _keeps_messages_without_timestamp (:360), and _empty_messages (:380). The no-timestamp-passes-through case (m_ts is None or m_ts <= _tw_ts) is pinned, which is the one easy-to-regress branch. 12 test functions total in the file.

Verdict

LGTM. Both follow-ups resolved, CI is green across 3.11 / 3.12 / 3.13, and the clamp is wired into all three _next_context_messages writeback paths (streaming.py:5181, :5329, :6207). No further blockers from my side — ready to merge.

The cross-session "edit-after-duplicate" scenario I mentioned remains #3101's territory, not a blocker here. Thanks for the quick turnaround, @AlexeyDsov.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Thanks for the thorough work on this, @AlexeyDsov — the diagnosis of #2914 is correct (the > watermark filter missed original pre-edit content that sorts below the watermark, and context_messages wasn't being truncated in sync). Parts of this PR are exactly right: truncating context_messages alongside messages in the truncate endpoint, and the content-fingerprint filter in merge_session_messages_append_only.

But the review (independent maintainer trace + Opus advisor, both with empirical repros) found a severe regression in the watermark-clamp approach that we can't ship as-is. Marking hold + changes-requested.

The blocking problem: legitimate new turns are dropped from agent context after any Edit

_clamp_context_to_watermark keeps only messages with m_ts <= watermark. Combined with removing _maybe_clear_truncation_watermark, the watermark is now set only by truncate/undo/retry and never advanced on a normal turn (confirmed: truncation_watermark is only ever read in api/streaming.py:2633, never written). So:

  1. User edits a message → truncate sets watermark = 2.0 (ts of the kept tail).
  2. User sends a brand-new question (ts=3.0); agent replies (ts=4.0).
  3. The post-result clamp runs with the stale watermark=2.0 and drops both ts=3.0 and ts=4.0 — the new turn vanishes from the agent's context on the next turn.

Empirical repro (run against this branch):

_clamp_context_to_watermark(session{watermark=2.0}, [u@1, a@2, u@3 "NEW Q", a@4 "NEW REPLY"])
→ ['first (pre-edit)', 'reply first']      # NEW Q and NEW REPLY dropped

This means every conversation that has ever had an Edit/Retry/Undo would silently lose all subsequent turns from the agent's context — a worse and much more frequent bug than #2914. There's also a secondary issue: because the auto-clear was removed, a watermark set once is now permanent — it survives across future compressions and will silently block legitimate older-context re-merges indefinitely.

What we'd need to merge this

Keep the good parts (context_messages truncation sync + the content-fingerprint merge filter). The watermark just can't double as a permanent deletion oracle. Concrete options (pick one):

  1. Advance the watermark to the tail timestamp at the end of every successful turn (after the post-result s.save()). That keeps the deletion oracle alive for exactly the one immediate post-edit turn (where Bug: /undo and message edit appear to succeed but have no visual effect #2914 bites) and self-clears thereafter, so normal subsequent turns aren't clamped. Smallest change that keeps your fingerprint filter.
  2. Restore a (stricter) _maybe_clear_truncation_watermark — clear the watermark once the next turn's content has been reconciled, rather than letting it persist forever. The original logic was on the right track, just too eager.
  3. Use the dropped messages' content-fingerprints as the deletion oracle (snapshot them at truncate/undo time) instead of a timestamp watermark. Then the timestamp watermark can self-clear on the next turn since it no longer carries deletion semantics.

Tests to add

Please add an integration-level test that drives a real turn through the streaming path (_run_agent_streaming or the equivalent post-result merge) after an Edit, and asserts the next turn's context_messages contains the new user+assistant pair (not just the unit tests on _clamp_context_to_watermark in isolation). That's the case the current test suite doesn't cover and where the regression hides.

Really appreciate the depth here — the #2914 analysis is solid and the fingerprint filter is the right instinct. It's specifically the permanent-watermark clamp that needs rework before this is safe. Re-request review once the new-turn-survival path is covered and green.

@nesquena-hermes nesquena-hermes added hold changes-requested Maintainer left detailed feedback requesting changes; PR is waiting on author to address labels May 30, 2026
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Pre-release regression gate caught a brick-class issue — holding

I picked this up to advance toward release (your v2 had my LGTM), cherry-picked the tip onto fresh master, and ran it through our pre-release gate (full test suite + Opus advisor + the Codex regression gate). The full suite passed, but both advisors independently flagged a brick-class regression, which I then reproduced empirically. Holding before it ships.

The regression

_clamp_context_to_watermark() is called on all three streaming-finalize paths (api/streaming.py:5292, 5440, 6348) and drops every message with timestamp > truncation_watermark. Combined with this PR removing _maybe_clear_truncation_watermark() from save() (the function that previously auto-cleared the watermark once newer messages arrived), nothing ever advances or clears the watermark after Edit/Retry/Undo sets it.

Result: the watermark becomes a permanent ceiling. Every turn after an Edit/Retry/Undo has its own new messages clamped out of context_messages — so the agent permanently loses memory of everything after the edit point. Symptom: do an Edit, ask a question (answers fine), then ask "what was my previous question?" — the agent only sees the pre-Edit conversation.

Why CI was green

The PR's regression test (test_edit_then_new_turn_then_undo_leaks_original_via_state_db) sets session.context_messages directly with the new turn already inlined — it never exercises the _clamp_context_to_watermark finalize path, so it passes while the real streaming flow breaks.

Empirical repro (clean, from a fresh stage worktree)

from api.streaming import _clamp_context_to_watermark
class S: session_id="post-edit"; truncation_watermark=101.0
result = [
    {"role":"user","content":"kept-q","timestamp":100.0},
    {"role":"assistant","content":"kept-a","timestamp":101.0},
    {"role":"user","content":"EDITED-q","timestamp":200.0},   # new turn after edit
    {"role":"assistant","content":"EDITED-a","timestamp":201.0},
]
_clamp_context_to_watermark(S(), result)
# → keeps only ['kept-q','kept-a'] — the new turn is dropped from agent context

Suggested fix (smallest, safest)

Drop the streaming clamp entirely and rely on the merge-side watermark filter in merge_session_messages_append_only (which already addresses #2914 on the state.db replay path), OR restore the _maybe_clear_truncation_watermark() auto-clear in save() so the watermark is a one-shot fence rather than a permanent ceiling. Keep the routes.py context_messages[:keep] slice and the new sub-watermark filter in merge_session_messages_append_only — those parts are correct.

Please also add a streaming-level integration test: Edit → new turn → second new turn, asserting the second turn sees the first new turn in the agent's context.

Re-applying hold + changes-requested. The diagnosis of #2914 itself is correct and the merge-side parts are good — this is fixable, just not shippable as-is. Thanks for the solid investigation work on the root cause.

@AlexeyDsov
AlexeyDsov force-pushed the alexeydsov-2026-05-27-session-context-duplicate-fix branch from bc471c8 to 7a3f441 Compare June 1, 2026 08:02
@AlexeyDsov

AlexeyDsov commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

@nesquena-hermes thanks for the review. I rebased the branch onto the latest master and addressed your comments in the following two commits:

Changes:

  • Removed _clamp_context_to_watermark() entirely (definition + 3 call sites in api/streaming.py)
  • Kept the correct parts of the original PR: context_messages[:keep] in routes.py, sub-watermark content filter in merge_session_messages_append_only(), and removal of _maybe_clear_truncation_watermark() from save()
  • Added tests:
  • test_streaming_finalize_preserves_new_turns_after_edit — verifies new turns survive finalize
  • test_streaming_finalize_does_not_leak_original_after_edit — verifies old replaced messages don't leak through finalize
  • test_edit_does_not_leak_original_message_into_context_via_reconcile — verifies old replaced messages don't leak through reconcile

@AlexeyDsov
AlexeyDsov force-pushed the alexeydsov-2026-05-27-session-context-duplicate-fix branch from 12be435 to 0ae12a9 Compare June 1, 2026 10:29
nesquena-hermes added a commit that referenced this pull request Jun 1, 2026
v0.51.197: stop agent replaying edited/undone messages (#3102)
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Shipped in v0.51.197 (stage-batch9, via #3349). Thanks @AlexeyDsov — the diagnosis of #2914 was spot-on and the v3 (removing _clamp_context_to_watermark() entirely, timestamp-filtering stale pre-edit rows, and syncing context_messages on /api/session/truncate) is exactly right.

One maintainer fix applied on the way in: because Session.save() no longer auto-clears the watermark, the unconditional timestamp > watermark skip in merge_session_messages_append_only would have become a permanent ceiling — a genuine future state.db-only recovery row (compaction/recovery, missed by the sidecar) would be silently dropped from /api/session forever once the session advanced past the edit. I scoped the skip to only fire while the sidecar hasn't advanced past the watermark, and added 2 revert-verified regression tests (advanced-session recovery row merges; deleted-tail still filtered when not advanced). Co-authored-by attribution preserved.

Full suite 7162 passed, Opus + Codex regression gate both cleared (Codex re-verified the watermark-ceiling fix by execution). Closes #2914. 🎉

AJV20 pushed a commit to AJV20/hermes-webui that referenced this pull request Jun 1, 2026
…context_messages truncation (nesquena#3102)

Co-authored-by: AlexeyDsov <AlexeyDsov@users.noreply.github.com>
AJV20 pushed a commit to AJV20/hermes-webui that referenced this pull request Jun 1, 2026
AJV20 pushed a commit to AJV20/hermes-webui that referenced this pull request Jun 1, 2026
…ture state.db recovery rows

Codex regression-gate finding: since Session.save() no longer auto-clears the
truncation_watermark, the unconditional 'timestamp > watermark' skip in
merge_session_messages_append_only became a permanent ceiling — a genuine future
state.db-only row (recovery/compaction, missed by the sidecar) would be silently
dropped from /api/session and model-context reconstruction forever. Only apply the
above-watermark skip while the sidecar has NOT advanced past the watermark. Preserves
the nesquena#2914 deleted-tail filtering (revert-verified). Adds 2 regression tests.
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request Jun 1, 2026
…➔ 0.51.197) (#749)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.157` → `0.51.197` |

---

### Release Notes

<details>
<summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary>

### [`v0.51.197`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051197--2026-06-01--Release-FQ-stage-batch9--stop-agent-replaying-editedundone-messages)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.196...v0.51.197)

##### Fixed

- Editing or undoing a message no longer lets the agent replay the original pre-edit content from `state.db`: the truncation-watermark filter now also skips replaced/stale rows whose timestamp sorts *below* the watermark, and `POST /api/session/truncate` truncates `context_messages` in sync with `messages` so the agent's context matches the visible transcript after Edit/Regenerate. The earlier `_clamp_context_to_watermark()` approach (which turned the watermark into a permanent ceiling that dropped every new turn) is removed. Closes [#&#8203;2914](https://github.com/nesquena/hermes-webui/issues/2914) ([#&#8203;3102](https://github.com/nesquena/hermes-webui/issues/3102), [@&#8203;AlexeyDsov](https://github.com/AlexeyDsov)).

### [`v0.51.196`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051196--2026-06-01--Release-FP-stage-batch8--file-manager-external-sessions--artifacts-tool-metadata--edge-toggle-icon--type-hints)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.195...v0.51.196)

##### Fixed

- File manager (folder download, raw file fetch, and related handlers) now falls back to a `state.db` lookup for sessions created by Telegram/CLI rather than the WebUI, resolving them against the active WebUI workspace instead of returning a 404. Closes [#&#8203;3280](https://github.com/nesquena/hermes-webui/issues/3280) ([#&#8203;3314](https://github.com/nesquena/hermes-webui/issues/3314), [@&#8203;Sanjays2402](https://github.com/Sanjays2402)).
- Artifacts tab now detects files from structured `tool_calls` (OpenAI format) and `tool_use` content blocks (Anthropic format) on messages, not just text-mined diff fences, so artifacts surface even when `S.toolCalls` is cleared after a reload; display paths are trimmed of the workspace prefix ([#&#8203;3329](https://github.com/nesquena/hermes-webui/issues/3329), [@&#8203;mysoul12138](https://github.com/mysoul12138)).
- Workspace panel edge-toggle chevron now points left (toward the panel it reveals) instead of right ([#&#8203;3318](https://github.com/nesquena/hermes-webui/issues/3318), [@&#8203;xz-dev](https://github.com/xz-dev)).

##### Internal

- `api/state_sync.py` now uses `Optional[T]` annotations for parameters defaulting to `None` instead of the implicit `T = None` form ([#&#8203;3323](https://github.com/nesquena/hermes-webui/issues/3323), [@&#8203;kuishou68](https://github.com/kuishou68)).

### [`v0.51.195`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051195--2026-06-01--Release-FO-stage-batch7--hide-attachment-path-markers-in-chat-UI)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.194...v0.51.195)

##### Fixed

- Uploaded image attachment path context (`[Attached files: …]`) remains available to the agent in the stored message, but the chat transcript, sidebar display title, and server-derived provisional titles no longer show the raw path suffix to the user ([#&#8203;3296](https://github.com/nesquena/hermes-webui/issues/3296), [@&#8203;AJV20](https://github.com/AJV20)).

### [`v0.51.194`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051194--2026-06-01--Release-FN-stage-batch6--profiles-config-import-cycle-fix)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.193...v0.51.194)

##### Fixed

- Profile startup now shares platform-default Hermes home resolution through a small `api/paths.py` helper instead of importing the full `api.config` module from `api.profiles`, so importing profiles before config no longer hits a latent circular-load that silently skipped active-profile initialization. Closes [#&#8203;3283](https://github.com/nesquena/hermes-webui/issues/3283) ([#&#8203;3303](https://github.com/nesquena/hermes-webui/issues/3303), [@&#8203;AJV20](https://github.com/AJV20)).

### [`v0.51.193`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051193--2026-06-01--Release-FM-stage-batch5--ctl-dotenv-opt-out--workspace-inline-open--gateway-reply-polish)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.192...v0.51.193)

##### Fixed

- `ctl.sh` now honors `HERMES_WEBUI_NO_DOTENV=1`, letting tests and scripted launches opt out of repo-local `.env` loading so host-specific `HERMES_WEBUI_STATE_DIR` values do not make the ctl test suite flaky. Closes [#&#8203;3246](https://github.com/nesquena/hermes-webui/issues/3246) ([#&#8203;3304](https://github.com/nesquena/hermes-webui/issues/3304), [@&#8203;AJV20](https://github.com/AJV20)).
- Workspace **Open in browser** now opens HTML files inline (with the same `inline=1` + CSP sandbox isolation as the file preview) instead of forcing a download, and uses `noopener` for the new tab ([#&#8203;3305](https://github.com/nesquena/hermes-webui/issues/3305), [@&#8203;xz-dev](https://github.com/xz-dev)).
- Gateway-backed chat now carries the same WebUI final-answer polish guidance as the in-process chat paths, so terse scratchpad fragments such as "Need script" are not encouraged as visible assistant replies ([#&#8203;3301](https://github.com/nesquena/hermes-webui/issues/3301), [@&#8203;AJV20](https://github.com/AJV20)).

### [`v0.51.192`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051192--2026-05-31--Release-FL-stage-batch4--per-model-contextlength-default-only-guard)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.191...v0.51.192)

##### Fixed

- A global `model.context_length` cap (set in config for the default model, e.g. 232000) no longer silently shrinks **non-default** models' real context windows. The cap is now applied only when the session model equals `model.default`; other models (e.g. a 1M-context variant) keep their real metadata window. The guard is applied consistently across the session context-length resolver (`api/routes.py`), the per-turn persistence path, and the live SSE usage payload, and the auto-compress `threshold_tokens` is rescaled to the real cap so the context-window indicator and compression trigger reflect the actual window. The live-usage perf path caches the resolved per-model window once per stream (it runs \~10×/sec during streaming) so non-default-model streams don't take a config/metadata lookup on every metering tick. Backend-only; default-model sessions are unaffected. Closes [#&#8203;3256](https://github.com/nesquena/hermes-webui/issues/3256) ([#&#8203;3263](https://github.com/nesquena/hermes-webui/issues/3263), [@&#8203;allenliang2022](https://github.com/allenliang2022)).

### [`v0.51.191`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051191--2026-05-31--Release-FK-stage-batch3--skills-detail-markdown-styling--launchd-duplicate-start-guard)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.190...v0.51.191)

##### Fixed

- Skills detail view now renders `SKILL.md` markdown with the same `.preview-md` typography used by Memory and Notes, instead of unstyled `renderMd()` output. Linked markdown skill files opened from the detail view use the same wrapper plus scoped post-render `highlightCode` / KaTeX enhancement ([#&#8203;3284](https://github.com/nesquena/hermes-webui/issues/3284), [@&#8203;pamnard](https://github.com/pamnard)).
- `ctl.sh start` now refuses to launch a second WebUI instance when a launchd-managed job already owns it (macOS), instead of racing the launchd instance into repeated `Address already in use` churn on port 8787. The guard is macOS/launchd-only, no-ops on every non-launchd path, and can be overridden with `HERMES_WEBUI_CTL_ALLOW_LAUNCHD_CONFLICT=1`; `docs/supervisor.md` documents launchd as the single source of truth. Closes [#&#8203;3289](https://github.com/nesquena/hermes-webui/issues/3289) ([#&#8203;3291](https://github.com/nesquena/hermes-webui/issues/3291), [@&#8203;andrewkangkr](https://github.com/andrewkangkr)).

### [`v0.51.190`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051190--2026-05-31--Release-FJ-stage-batch2--Windows-upgrade-state-stranding-hotfix--gateway-banner--quiet-tool-previews)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.189...v0.51.190)

##### Fixed

- **Windows upgrade no longer strands WebUI state (data-loss-class, priority).** v0.51.134 moved the Windows default Hermes home from `%USERPROFILE%\.hermes` to `%LOCALAPPDATA%\hermes` without a migration, so upgrading users opened an empty app (sessions, pins, UI settings appeared lost — actually just at the address the new build no longer read). `_platform_default_hermes_home()` now prefers the legacy `%USERPROFILE%\.hermes` **only** on the exact post-upgrade fingerprint (legacy holds real `webui/` state AND the new location is not yet established), and `api/profiles.py` delegates to the same resolver so the two can never drift. Non-destructive and self-healing — no files are moved; affected users recover on next launch with no action. Markers key on WebUI-owned `webui/` state only (not agent `auth.json`/`config.yaml`) so a long-time agent user installing WebUI fresh isn't wrongly diverted. Closes [#&#8203;2905](https://github.com/nesquena/hermes-webui/issues/2905) ([#&#8203;3279](https://github.com/nesquena/hermes-webui/issues/3279)).
- **"Gateway not configured" banner on two-container Docker first deploy.** `GET /api/gateway/status` treated all `alive is None` health payloads as unconfigured-unless-`identity_map`, so a freshly-started gateway that hadn't ticked `updated_at` yet (and had no conversations) reported "not configured." A stale-but-**running** gateway (reason `gateway_stale_running_state`, or a `gateway_state == "running"` detail) now reports `configured = True`; a stale-**stopped** gateway deliberately still falls through to the `identity_map` signal so a stopped root gateway reads as "not configured" per [#&#8203;1944](https://github.com/nesquena/hermes-webui/issues/1944). Closes [#&#8203;3194](https://github.com/nesquena/hermes-webui/issues/3194) ([#&#8203;3279](https://github.com/nesquena/hermes-webui/issues/3279)).
- Collapsed tool-call previews stay quiet: instead of falling back to raw result JSON (which made tool-heavy turns look like debug logs), a settled collapsed tool card now shows a compact argument summary (with verbose/secret-bearing keys like `content`/`patch`/`api_key`/`token` excluded) or a short status, keeping the full result inside the expandable detail body ([#&#8203;3267](https://github.com/nesquena/hermes-webui/issues/3267), [@&#8203;ai-ag2026](https://github.com/ai-ag2026)).

### [`v0.51.189`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051189--2026-05-31--Release-FI-stage-batch1--ruff-lint-gate--SSE-refresh-dedupe--tooltip-i18n)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.188...v0.51.189)

##### Added

- **Forward-looking Python lint gate (ruff).** A curated `[tool.ruff]` ruleset (E9 syntax/runtime + F pyflakes + B bugbear — high-signal correctness rules, no style/formatting families) now gates **new and changed Python code** in CI (`tests.yml` `lint` job) and as part of the maintainer pre-release pre-gate. It is the Python twin of the existing ESLint runtime guard for `static/*.js`. Crucially it is **line-scoped** (`scripts/ruff_lint.py --diff`): it flags violations only on lines a change adds or modifies, so it keeps incoming code clean **without** reformatting the existing tree's cosmetic backlog (a separate, deferred, maintainer-run decision). `tests/test_ruff_forward_lint.py` additionally holds the whole tree free of E9 errors and skips cleanly when ruff isn't installed. See TESTING.md > "Python lint gate (ruff)". Closes [#&#8203;3273](https://github.com/nesquena/hermes-webui/issues/3273) ([#&#8203;3275](https://github.com/nesquena/hermes-webui/issues/3275)).

##### Fixed

- Gateway SSE reconnect no longer triggers a phantom "new dialog created" sidebar refresh: the initial sessions snapshot pushed on every reconnect is now compared against the current gateway-session set and `renderSessionList()` is skipped when nothing changed ([#&#8203;3270](https://github.com/nesquena/hermes-webui/issues/3270), [@&#8203;PINKIIILQWQ](https://github.com/PINKIIILQWQ)).
- Raw-audio recording mic tooltip now uses a recording-specific i18n key instead of the dictation "Stop" label; sidebar lineage/child tooltip suffixes are localized across the locale catalog; and the localized read-only title hover hint for imported sessions is restored. Closes [#&#8203;3242](https://github.com/nesquena/hermes-webui/issues/3242), [#&#8203;3214](https://github.com/nesquena/hermes-webui/issues/3214) ([#&#8203;3272](https://github.com/nesquena/hermes-webui/issues/3272), [@&#8203;ai-ag2026](https://github.com/ai-ag2026)).

### [`v0.51.188`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051188--2026-05-31--Release-FH-stage-batchH--configured-runner-client-boundary-default-off)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.187...v0.51.188)

##### Added

- **Configured runner-client boundary** for the `runner-local` runtime adapter (RFC `hermes-run-adapter-contract`, tracking [#&#8203;1925](https://github.com/nesquena/hermes-webui/issues/1925), Slice 4c/4d). When — and only when — an operator sets `HERMES_WEBUI_RUNNER_BASE_URL`, WebUI delegates the agent run to that external/supervised runner over a small JSON HTTP client (start / observe / status / cancel / approval / clarify / queue / goal) and bridges the runner's events through the existing SSE stream route, instead of owning the run in the main WebUI process. **Default-off and fully reversible:** with no endpoint configured, the factory preserves the existing bounded "runner-local not configured" path and the live in-process streaming path is unchanged — no behavior change for existing users. New `api/runner_client.py` (`HttpRunnerClient` + `runner_client_configured()`) plus additive `_runner_*` SSE-bridge helpers in `api/routes.py`; the legacy `_run_agent_streaming` control flow is untouched ([#&#8203;3073](https://github.com/nesquena/hermes-webui/issues/3073), [@&#8203;AJV20](https://github.com/AJV20)).

### [`v0.51.187`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051187--2026-05-31--Release-FG-stage-batchG--workspace-preview-persistence--scroll-intent-window)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.186...v0.51.187)

##### Fixed

- Workspace file preview no longer closes when a chat response finishes and the UI refreshes the workspace file tree. Background `loadDir('.')` on stream `done` now preserves an open preview instead of always calling `clearPreview()`, and reloads the open file when a write/edit tool touched that path during the turn (skipping reload while the preview has unsaved local edits) ([#&#8203;3262](https://github.com/nesquena/hermes-webui/issues/3262), [@&#8203;pamnard](https://github.com/pamnard)).
- During streaming, scrolling up to read earlier content no longer snaps back to the bottom after a brief pause: the upward-scroll intent window was widened from 450ms to 2000ms so DOM-layout changes from the markdown parser / tool-card insertions are still recognized as co-occurring with user intent and don't re-pin the view. Downward scroll, the scroll-to-bottom button, and trackpad-momentum protection are unaffected ([#&#8203;3250](https://github.com/nesquena/hermes-webui/issues/3250), [@&#8203;emanon312](https://github.com/emanon312)).

### [`v0.51.186`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051186--2026-05-31--Release-FF-stage-batchF--update-checker-ff-reachability-fall-through--utf-8-git-output-test-coverage)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.185...v0.51.186)

##### Fixed

- Agent self-update no longer advertises or applies unreachable release tags when the checkout tracks `main` past an older tag but the newest published tag lives on a divergent side branch (for example `v2026.5.29` → `v2026.5.29.2`). The update checker and apply path now fall through to the configured upstream branch when `git pull --ff-only <latest-tag>` cannot fast-forward, matching the existing [#&#8203;2653](https://github.com/nesquena/hermes-webui/issues/2653)/[#&#8203;3140](https://github.com/nesquena/hermes-webui/issues/3140) release-vs-branch routing ([#&#8203;3257](https://github.com/nesquena/hermes-webui/issues/3257), [@&#8203;pamnard](https://github.com/pamnard)).
- Added regression coverage pinning `_run_git()`'s UTF-8 decoding (`encoding='utf-8'`, `errors='replace'`) and its defensive `None`-stdout guard, so version detection cannot crash on non-UTF-8 Windows console output ([#&#8203;3254](https://github.com/nesquena/hermes-webui/issues/3254), [@&#8203;zapabob](https://github.com/zapabob)).

### [`v0.51.185`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051185--2026-05-31--Release-FE-stage-batchE--clarify-card-bug-fix-batch-identical-prompt-dedup--autofill-guard--GBK-startup-crash)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.184...v0.51.185)

##### Fixed

- The clarify popup could remain visible with stale input state after sending a response when the next queued clarification prompt was text-identical to the previous one. The dedupe signature now includes the prompt's `clarify_id`, so a newly queued identical prompt is treated as new instead of being mistaken for the one already answered ([#&#8203;3245](https://github.com/nesquena/hermes-webui/issues/3245), closes [#&#8203;3241](https://github.com/nesquena/hermes-webui/issues/3241)).
- Chrome's password manager could autofill the clarify-card input with saved credentials (typically a provider base URL), causing a phantom "Clarification closed" toast on every session completion and injecting the saved URL into the main composer. The clarify input now carries `autocomplete="off"` plus a `readonly` guard that is lifted only when an actual clarification prompt is shown (or on focus), so the browser's heuristic autofill skips it ([#&#8203;3247](https://github.com/nesquena/hermes-webui/issues/3247)).
- Prevent a server startup crash on non-UTF-8 Windows locales (e.g. Chinese GBK codepage): `_run_git()` now decodes git subprocess output as UTF-8 with `errors='replace'` and defensively guards against `None` streams, instead of letting a `UnicodeDecodeError` on binary `git diff --binary` output leave `stdout=None` and take down `import api.updates` with an `AttributeError` ([#&#8203;3249](https://github.com/nesquena/hermes-webui/issues/3249)).

### [`v0.51.184`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051184--2026-05-31--Release-FD-stage-batchD--raw-audio-upload-mode--scroll-preserve--non-POSIX-test-skip)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.183...v0.51.184)

##### Added

- Optional **raw audio upload mode** (Settings → Sound, off by default): when enabled, the composer mic button sends the recorded audio as a file attachment instead of transcribing it locally, so you can use external STT, raw-audio/emotion analysis, or multimodal models. Classic push-to-talk dictation is unchanged when the toggle is off. The mic tooltip reflects the active mode; localized across all 12 locales ([#&#8203;3169](https://github.com/nesquena/hermes-webui/issues/3169)).

##### Fixed

- Transcript scroll position is now preserved during same-session CLI/gateway import SSE refreshes (and the active session's metadata is synced from the refreshed transcript), instead of jumping to the bottom on each refresh ([#&#8203;3237](https://github.com/nesquena/hermes-webui/issues/3237)).

##### Changed

- `tests/test_terminal_process_cleanup.py` (POSIX terminal coverage that imports `fcntl` at module load) is now skipped at collection time on non-POSIX hosts instead of erroring ([#&#8203;3235](https://github.com/nesquena/hermes-webui/issues/3235)).

### [`v0.51.183`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051183--2026-05-31--Release-FC-stage-batchC--inline-file-media-artifacts--apimedia-state-file-confinement)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.182...v0.51.183)

##### Fixed

- Render assistant-emitted `file://` image artifact links inline through the authenticated `/api/media` route, so browser clients can view generated local images instead of seeing unusable server-local file paths. Only bare (line-start or whitespace-delimited) `file://` URLs are rewritten — `[label](file://...)` markdown anchors keep the normal link path ([#&#8203;3219](https://github.com/nesquena/hermes-webui/issues/3219)).

##### Security

- `/api/media` now hard-denies WebUI state and secret/config files even when they fall under an allowed root (the WebUI state dir, `settings.json`, `state.db`, `auth.json`, `auth.lock`, `config.yaml`, `.env`, signing/PBKDF2 keys, the `sessions`/`memories`/`profiles` state subdirs). Previously the whole Hermes home was an allowed root, so an authenticated session viewing attacker-influenced agent output that emitted a `file://`/`MEDIA:` link to such a file could fetch it. Hardened the boundary at the route for every entry path (bare `file://`, markdown anchors, and `MEDIA:` tokens), closing [#&#8203;3234](https://github.com/nesquena/hermes-webui/issues/3234).

### [`v0.51.182`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051182--2026-05-31--Release-FB-stage-batchB2--headless-browser-smoke-gate--consolidated-client-disconnect-handling)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.181...v0.51.182)

##### Added

- CI now runs a headless browser smoke test (`tests/browser_smoke.py`, `.github/workflows/browser-smoke.yml`) on every PR and push: it boots the real server agent-free and loads the key pages in Chromium, failing on any console error or uncaught JS exception. This catches the runtime-JS brick class (e.g. a `const` reassigned at runtime as in [#&#8203;3162](https://github.com/nesquena/hermes-webui/issues/3162), or a `function`/`window` name collision as in [#&#8203;2715](https://github.com/nesquena/hermes-webui/issues/2715)/[#&#8203;2771](https://github.com/nesquena/hermes-webui/issues/2771)) that `node --check`, ESLint, and the mocked test suite cannot see because they only manifest when a real browser executes the page. The smoke is credential-free — it strips `*_API_KEY` from the environment and drives no real model ([#&#8203;3231](https://github.com/nesquena/hermes-webui/issues/3231)).

##### Fixed

- Client disconnects during response writes (browser tab close, SSE reconnect races, mobile network switches, half-closed sockets) are now handled through a single `_CLIENT_DISCONNECT_ERRORS` set and a `_safe_write` helper instead of ad-hoc per-call-site `try/except`, so an expected disconnect no longer surfaces as a misleading server 500 in the logs. The error-response path is itself wrapped so a disconnect while sending a 500 is swallowed quietly rather than cascading. A bare `TimeoutError` from the Joplin notes integration's `urlopen` is now converted to a clean "not reachable" error at the route rather than escaping to the dispatch-level disconnect handler ([#&#8203;3210](https://github.com/nesquena/hermes-webui/issues/3210)).

### [`v0.51.181`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051181--2026-05-30--Release-FA-stage-batchA--agent-cache-eviction-teardown--streaming-finalize-race)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.180...v0.51.181)

##### Fixed

- Cached agents evicted after session-identity mismatches, unsafe runtime refresh, credential self-heal, or skipped compression migration now go through the normal session-boundary teardown path, committing pending memory and closing provider/session resources instead of silently dropping the cache entry ([#&#8203;3218](https://github.com/nesquena/hermes-webui/issues/3218), closes [#&#8203;3215](https://github.com/nesquena/hermes-webui/issues/3215)).
- Assistant streaming text is no longer lost when a stream completes while you have switched to a different session tab: the SSE `done` handler now sets the stream-finalized flag immediately (before the fade window), so a `stream_end` event arriving mid-fade can no longer trigger `_restoreSettledSession()` and overwrite the live messages with a stale server snapshot ([#&#8203;3201](https://github.com/nesquena/hermes-webui/issues/3201), closes [#&#8203;3195](https://github.com/nesquena/hermes-webui/issues/3195)).

### [`v0.51.180`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051180--2026-05-30--Release-EZ-stage-batch62--sessionagent-cache-ownership-hardening)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.179...v0.51.180)

##### Fixed

- Guard session and agent caches against compression/continuation id drift: `GET /api/session` now evicts a cached `Session` whose own `session_id` no longer matches the requested key (instead of trusting the LRU), the background title-update/refresh paths only adopt a cached session when its identity matches, and the compression checkpoint migration no longer re-stores a stale object under the old lineage id. Prevents a stale cached object from making `/api/session?session_id=<tip>` return an older transcript segment, which looked like a disappeared session ([#&#8203;3191](https://github.com/nesquena/hermes-webui/issues/3191)).
- Evicted cached agents are now torn down cleanly at the WebUI session boundary: pending session memory is committed first, and only if the lifecycle entry is clean afterward does the agent get unregistered and its memory provider shut down via `shutdown_memory_provider(messages)` (closing provider-owned clients such as Hindsight's aiohttp session) before the session DB is closed — instead of leaking those resources until garbage collection ([#&#8203;3166](https://github.com/nesquena/hermes-webui/issues/3166)).

### [`v0.51.179`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051179--2026-05-30--Release-EY-stage-batch61--custom-provider-reasoning-efforts--clearer-sidebar-tooltips)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.178...v0.51.179)

##### Fixed

- Reasoning effort selector now appears for thinking-capable models served through custom API aggregators (New API, One API, etc.) that use non-standard model naming — bare names like `deepseek-v4-flash` or dot-separated `moonshotai.kimi-k2.5` rather than the OpenRouter-style `vendor/model` slash format. The heuristic now also strips a dot-vendor prefix and recognizes a `thinking`/`reasoning` token anywhere in the model name; plain non-reasoning models stay hidden as before ([#&#8203;3202](https://github.com/nesquena/hermes-webui/issues/3202)).
- Sidebar session row tooltips now explain the fork, prior-turn, child-session, and running/unread status badges, and hovering a truncated chat title shows the full title instead of the old "Double-click to rename" hint ([#&#8203;3203](https://github.com/nesquena/hermes-webui/issues/3203)). The localized pending-approval/clarify attention tooltip retains precedence over the generic running/unread state tooltip on the status dot, and the fork tooltip keeps its localized "Forked from" base.

### [`v0.51.178`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051178--2026-05-30--Release-EX-stage-batch60--parallel-sharded-CI-test-runs)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.177...v0.51.178)

##### Changed

- CI: the test suite now runs in 3 parallel shards per Python version (9 jobs total) via `pytest-shard`, cutting wall-clock test time roughly in half (slowest shard \~70s vs \~180s sequential). To make sharding safe, several tests that asserted a pristine default while a sibling test mutated shared process/server state were fixed to establish their own preconditions: onboarding-completed flag reset (`test_onboarding_mvp`), password-hash cache invalidation (`test_issue693_system_health_panel`), authoritative sessions-file path (`test_auth_session_persistence`), and — the root cause of the worst leak — `test_profile_env_isolation` no longer deletes + re-imports `api.profiles` (which poisoned the module's cached base-home global for every later test); it now points the cached path via `monkeypatch.setattr`. A conftest fixture also restores `HERMES_HOME`/`HERMES_BASE_HOME` after each test as defense-in-depth. Completes the test-sharding half of [#&#8203;3197](https://github.com/nesquena/hermes-webui/issues/3197) (the Docker-cache half shipped in v0.51.177).

### [`v0.51.177`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051177--2026-05-30--Release-EW-stage-batch59--Docker-smoke-test-layer-caching)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.176...v0.51.177)

##### Changed

- CI: the Docker smoke-test workflow now builds the image once and caches its layers via the GitHub Actions cache (`type=gha`), then each compose variant restores from that cache instead of rebuilding from scratch — saving \~1-3 minutes per variant. The image is still built from the PR's local Dockerfile (`load: true`), so PR changes are tested, not the released image. (Partial adoption of [#&#8203;3197](https://github.com/nesquena/hermes-webui/issues/3197) — the Docker half; the test-sharding half is deferred pending test-suite shard-safety work.)

### [`v0.51.176`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051176--2026-05-30--Release-EV-stage-batch58--sidebar-attention-indicators)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.175...v0.51.176)

##### Added

- Sidebar session rows now surface pending approval and clarify work with a color-coded status dot (red for approvals, amber for clarifies) plus a matching left rail and tinted background, so inactive conversations that need a permission decision or an answer are easy to spot at a glance. A distinct two-tone attention sound also plays for approval/clarify prompts, separate from the completion sound ([#&#8203;3190](https://github.com/nesquena/hermes-webui/issues/3190)).

### [`v0.51.175`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051175--2026-05-30--Release-EU-stage-batch57--internal-conversation-links)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.174...v0.51.175)

##### Added

- Internal conversation links: a "Copy conversation link" action copies a Markdown reference (`session://<id>`) for any conversation, and `session://` references render as same-origin in-app links that open the target conversation without a full page reload ([#&#8203;3179](https://github.com/nesquena/hermes-webui/issues/3179)). Links are sanitized through the existing safe-URL allowlist (rewritten to `/session/<id>`, label escaped, sid URL-encoded — verified against quote-breakout and script-injection payloads).
- Conversation filtering now recognizes pasted session references directly: raw session IDs, `session://...` references, `/session/...` URLs, and Markdown session links surface the target conversation while preserving normal content-search hits ([#&#8203;3179](https://github.com/nesquena/hermes-webui/issues/3179)).

### [`v0.51.174`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051174--2026-05-30--Release-ET-stage-batch56--CLIgateway-session-usage-in-Insights)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.173...v0.51.174)

##### Added

- The Insights page now includes CLI and gateway sessions (Telegram, Discord, cron, TUI) from the Hermes agent's `state.db` in usage totals, model breakdown, and daily activity — not just WebUI-native sessions ([#&#8203;3189](https://github.com/nesquena/hermes-webui/issues/3189)). WebUI sessions are de-duplicated so they are counted once, not double-counted against their `state.db` row.

### [`v0.51.173`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051173--2026-05-30--Release-ES-stage-batch55--Windows-pathjournal-safety--pin-quota-snapshot-fix--tool-card-paging-anchor--sidebar-dedupe--quieter-tool-cards)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.172...v0.51.173)

##### Fixed

- Windows native runs now skip the POSIX-only turn-journal directory fsync instead of raising `AttributeError` for missing `os.O_DIRECTORY` on every submitted turn ([#&#8203;3170](https://github.com/nesquena/hermes-webui/issues/3170)).
- `/api/media` now treats Windows cross-drive `commonpath()` comparisons as non-matches instead of 500ing when media paths and allowed roots live on different drives ([#&#8203;3171](https://github.com/nesquena/hermes-webui/issues/3171)).
- Hidden pre-compression snapshots no longer keep stale pin state or count toward the visible pinned-session quota ([#&#8203;3181](https://github.com/nesquena/hermes-webui/issues/3181)).
- Tool-call cards stay anchored when scrolling back through paginated history; legacy session-level tool-call indices are rebased to the returned message window and the browser refreshes tool-call anchors whenever a larger history window is loaded ([#&#8203;3120](https://github.com/nesquena/hermes-webui/issues/3120)).
- Avoid duplicate sidebar rows when a compressed session completes after both the preserved snapshot id and continuation id are already present in the session list.

##### Changed

- Restored the legacy compact tool-call card chrome by removing the persistent "Tool output" badge and returning the left rail to the muted border treatment. This keeps tool activity visually quieter while preserving the existing collapsible tool details.

### [`v0.51.172`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051172--2026-05-30--Release-ER-stage-batch54--model-label-fallback--dev-cache-bust-hash--tilde-workspace-completion--cron-project-chip-sessions)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.171...v0.51.172)

##### Fixed

- Session model labels now fall back to the friendly `getModelLabel()` form instead of the raw model id when gateway routing info is unavailable ([#&#8203;3174](https://github.com/nesquena/hermes-webui/issues/3174)).
- Dev-build cache-busting now includes a short hash of the tracked dirty diff, so local asset URLs change on each edit instead of staying at a constant `-dirty` suffix ([#&#8203;3159](https://github.com/nesquena/hermes-webui/issues/3159)).
- Workspace path autocomplete now preserves `~/` suggestions while browsing under the user's home directory ([#&#8203;3173](https://github.com/nesquena/hermes-webui/issues/3173)).
- CLI-sourced cron sessions that were squeezed past the default sidebar window now stay addressable under their project chip via a dedicated cron-only lookup pass ([#&#8203;3172](https://github.com/nesquena/hermes-webui/issues/3172)).

### [`v0.51.171`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051171--2026-05-30--Release-EQ-stage-batch53--tool-output-card-badge--Neon-opt-in-skin)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.170...v0.51.171)

##### Added

- Tool-call output cards now carry a persistent "Tool output" badge and accent rail so tool output stays visually distinct from final assistant responses without requiring hover ([#&#8203;2867](https://github.com/nesquena/hermes-webui/issues/2867)).
- New opt-in "Neon" cyberpunk skin (dark-first, purple/cyan accents). Default-off; select it from the skin list like Catppuccin or Nous.

### [`v0.51.170`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051170--2026-05-30--Release-EP-stage-batch52--run-aware-SSE-replay-cursors)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.169...v0.51.170)

##### Fixed

- SSE run-journal replay cursors are now run-aware: a stale `after_seq` from an interrupted prior stream can no longer suppress replay events in a newer stream whose sequence numbers reset from 1. The reconnect cursor now carries a run-scoped `after_event_id` (`run_id:seq`) and the server ignores it when the run id differs, falling back to same-run `after_seq` dedupe ([#&#8203;3124](https://github.com/nesquena/hermes-webui/issues/3124)).

### [`v0.51.169`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051169--2026-05-30--Release-EO-stage-batch51--skill-toggle-profile-scoping--update-tag-filter--Docker-docs)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.168...v0.51.169)

##### Fixed

- Docker docs now explain host-localhost URLs (`host.docker.internal` / `host.containers.internal`) and the `sudo docker compose` `$HOME=/root` bind-mount pitfall for users whose WebUI cannot reach host APIs or see `~/.hermes` ([#&#8203;3012](https://github.com/nesquena/hermes-webui/issues/3012), [#&#8203;3006](https://github.com/nesquena/hermes-webui/issues/3006)).
- Skills panel disabled/enabled state and toggle writes now resolve `config.yaml` from the active WebUI profile instead of the process default Hermes home or startup config override ([#&#8203;3066](https://github.com/nesquena/hermes-webui/issues/3066)).
- Update checks no longer advertise a newer release tag when a main-tracking checkout already contains that tag; the banner now falls through to the branch comparison path instead of offering an update that cannot fast-forward ([#&#8203;3140](https://github.com/nesquena/hermes-webui/issues/3140)).

### [`v0.51.168`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051168--2026-05-30--Release-EN-stage-batch50--hotfix-mobile-Failed-to-load-conversation-messages)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.167...v0.51.168)

##### Fixed

- Fixed a `TypeError` in `_ensureMessagesLoaded` that surfaced as a "Failed to load conversation messages" toast on mobile after most messages: a `const msgs` binding was reassigned by the [#&#8203;3018](https://github.com/nesquena/hermes-webui/issues/3018) ephemeral-field carry-forward (introduced v0.51.161), which throws at runtime. Changed to `let`. Mobile triggered it most because SSE/visibility events fire the session-reload path more aggressively ([#&#8203;3162](https://github.com/nesquena/hermes-webui/issues/3162)).

##### Added

- Static JS runtime-error lint guard (`eslint.runtime-guard.config.mjs` + `tests/test_static_js_runtime_lint.py`): a curated, zero-false-positive ESLint check (`no-const-assign`, `no-import-assign`) over `static/**/*.js` that catches the brick-class of runtime errors `node --check` and source-presence tests miss. Runs in the test suite when ESLint is present and skips gracefully otherwise. See `TESTING.md` > "Static JS runtime lint".

### [`v0.51.167`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051167--2026-05-30--Release-EM-stage-batch49--iOS-style-swipe-actions-for-touch-devices--session-list-FLIP-reflow)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.166...v0.51.167)

##### Added

- Touch devices now support iOS-mail-style swipe actions on session rows: swipe left to reveal a Delete action, swipe right to reveal Archive (Restore for already-archived sessions). Swipe is gated to touch/coarse-pointer input, so desktop click, context-menu, and drag behavior are unchanged. Delete still routes through the existing confirmation dialog. The session list also gains FLIP-based reflow animation when rows are archived, deleted, or reordered, honoring `prefers-reduced-motion`.

### [`v0.51.166`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051166--2026-05-30--Release-EL-stage-batch48--shared-OpenCode-runtime-key--cron-project-chip-sessions)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.165...v0.51.166)

##### Fixed

- OpenCode provider key lookup now honors the shared `OPENCODE_API_KEY` fallback for both Zen and Go runtime paths, matching model-picker detection when provider-specific keys are absent ([#&#8203;3145](https://github.com/nesquena/hermes-webui/issues/3145)).
- Agent-side cron sessions imported from state.db now remain available to their assigned project chip as `default_hidden` rows instead of being filtered out before project reveal logic can see them ([#&#8203;3134](https://github.com/nesquena/hermes-webui/issues/3134)).

### [`v0.51.165`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051165--2026-05-30--Release-EK-stage-batch47--stop-EventSource-reconnect-storm-on-long-lived-SSE-streams)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.164...v0.51.165)

##### Fixed

- The session-events and gateway SSE streams no longer emit `Connection: close`, which browsers interpreted as a signal that the EventSource lifecycle had ended — triggering an instant reconnect loop that thrashed the session list roughly once per second and forced repeated re-renders / scroll-to-bottom ([#&#8203;3103](https://github.com/nesquena/hermes-webui/issues/3103), regression from the HTTP/1.1 keep-alive change in [`598fd4f`](https://github.com/nesquena/hermes-webui/commit/598fd4ff)). Finite responses that lack a `Content-Length` (e.g. the on-the-fly workspace ZIP download) keep `Connection: close` for unambiguous HTTP/1.1 message framing.

### [`v0.51.164`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051164--2026-05-30--Release-EJ-stage-batch46--passive-performance-hardening-refresh-coalescing--draft-save-dedup--activity-placeholders--bounded-restart-safety-wait)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.163...v0.51.164)

##### Fixed

- Coalesced duplicate in-flight sidebar/project refreshes and suppressed overlapping approval/clarify fallback polls, reducing repeated passive requests while preserving latest-refresh-wins sidebar state.
- Skipped duplicate composer-draft writes when the normalized autosave payload is unchanged, avoiding redundant full session JSON rewrites during debounced input/focus churn.
- Refined empty Activity waiting placeholders to distinguish stream creation, first-token wait, post-tool model wait, and running-tool wait states.
- Self-update restart safety now checks active agent runs as well as open SSE streams and waits for in-flight work before re-exec, avoiding update-triggered interruption when a run outlives its browser stream. The wait is bounded (300s) with a logged fallback to re-exec so a long-running or stuck agent run cannot soft-jam the self-update indefinitely.
- Documented the WebUI prefill context budget in README and architecture notes so operators can keep new-browser-turn startup context compact.

### [`v0.51.163`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051163--2026-05-30--Release-EI-stage-batch45--session-duplicatebranch-field-propagation)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.162...v0.51.163)

##### Fixed

- Duplicating or branching a session now carries over the fields that were previously dropped, so the copy behaves identically to the original until further edits: `truncation_watermark`, model-facing `context_messages` (deep-copied for independence), gateway routing + routing history, context-engine state, cache-token counters, composer draft, LLM-title flag, and per-session settings (model provider, project, personality, toolsets, context length, threshold). Compression anchors and last-prompt-token counts are intentionally not carried so the copy re-derives them. Prevents data-loss scenarios where, e.g., editing a message in a duplicated session would drop messages during state.db merge.

### [`v0.51.162`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051162--2026-05-30--Release-EH-stage-batch44--conversation-filter-clear-button--code-only-title-language-regression-coverage)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.161...v0.51.162)

##### Added

- Conversation filtering now shows a clear button inside the search field whenever text is present, letting users clear the filter with one click.

### [`v0.51.161`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051161--2026-05-29--Release-EG-stage-batch43--3-PR-live-display-fixes-jump-to-question-on-intermediate-assistant-messages--per-turn-usage-badge-persistence--stale-unreadcompression-timertool-card-dedup)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.160...v0.51.161)

##### Fixed

- The jump-to-question button now appears on every assistant message that has a resolvable question target, not only the turn-final one. Multi-step turns (tool call → assistant → tool call → assistant) previously stripped the navigation affordance from intermediate assistant bubbles.
- Per-turn ephemeral fields (`_turnUsage`, `_turnDuration`, `_turnTps`, `_gatewayRouting`) are now carried forward when a session refresh replaces the in-memory message list with fresh server data, so the usage badge / duration / gateway-routing pill no longer flash and disappear after a compaction restore, external active-session poll, or SSE error recovery ([#&#8203;3018](https://github.com/nesquena/hermes-webui/issues/3018)).
- The sidebar unread dot no longer sticks on a session after it has been viewed: syncing the viewed count now clears any stale completion-unread marker, and an actively-viewed session syncs its count instead of being flagged unread on tab switch ([#&#8203;3020](https://github.com/nesquena/hermes-webui/issues/3020)).
- The auto-compression card's elapsed timer is now cleared on completion/error, so a replaced card is no longer treated as a still-running compression; a background-session completion no longer kills the active session's compression timer ([#&#8203;2973](https://github.com/nesquena/hermes-webui/issues/2973)).
- Tool cards no longer duplicate the result text in both the header and the detail row: the completed tool result is routed to the detail snippet (falling back to the header only when no progress text was streamed), and the detail row is suppressed when the snippet equals the header preview.

### [`v0.51.160`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051160--2026-05-29--Release-EF-stage-batch42--3-PR-low-risk-cleanup-OpenCode-shared-key-detection--skills-panel-profile-aware-disabled-read--session-index-metadata-refresh-perf)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.159...v0.51.160)

##### Fixed

- Detect a shared `OPENCODE_API_KEY` as enabling both OpenCode Zen and OpenCode Go provider groups, matching Hermes Agent bridge environments that expose one OpenCode credential.
- The skills panel now reads each skill's disabled state from the active WebUI profile's `config.yaml` (checking `skills.platform_disabled.webui` then falling back to `skills.disabled`) instead of the process-global `HERMES_HOME`, so non-default profiles show the correct enabled/disabled state and stay consistent with the skill-toggle write path.

##### Changed

- WebUI session-sidebar metadata refresh is faster on large session directories: the persisted session-id listing is cached by directory mtime instead of re-globbing under the sessions lock on every `/api/sessions` poll, metadata-only loads skip the per-row session-index read when the sidecar already carries an authoritative `message_count`, and only runtime/lineage-shaped rows are overlaid with fuller sidecar metadata (historical transcripts are no longer scanned on every refresh).

### [`v0.51.159`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051159--2026-05-29--Release-EE-stage-batch41--5-PR-low-risk-cleanup-Gateway-tool-progress-forwarding--shutdown-diagnostics--CLI-snippet-limit-parity--numpad-Enter-submit--sync-chat-notes-guardrail)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.158...v0.51.159)

##### Changed

- WebUI's durable-notes guardrail now also applies to sync chat and explicitly asks agents to leave external notes and durable memory unchanged unless a turn contains an explicit capture or reusable durable signal; durable note writes should be summarized back to the user.

##### Fixed

- Gateway-backed browser chat now forwards Hermes Gateway `hermes.tool.progress` SSE events into WebUI's live tool/activity stream, so Gateway runs no longer appear idle while server-side tools are running.
- WebUI now logs structured shutdown diagnostics when the server exits or `/api/shutdown` is called, including active stream IDs to help diagnose interrupted turns after restarts.
- The chat composer now treats the numeric keypad Enter key as a submit shortcut even when the send-key preference is set to Ctrl/Cmd+Enter, while preserving regular Enter-as-newline behavior in that mode.
- The CLI tool-result snippet limit in the browser now matches the backend (`_TOOL_RESULT_SNIPPET_MAX = 4000`), so longer non-diff CLI tool output is no longer truncated to 200 characters before reaching the tool card.

### [`v0.51.158`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051158--2026-05-29--Release-ED-stage-batch40--5-PR-low-risk-cleanup-numpadkeyboard-composer-fixes--Joplin-search-auth--provider-qualified-model-preservation--SSE-fallback-poll-throttle--assistant-reply-polish)

[Compare Source](https://github.com/nesquena/hermes-webui/compare/v0.51.157...v0.51.158)

##### Changed

- WebUI chat instructions now explicitly prevent terse scratchpad/planning fragments from appearing in visible assistant replies, while still allowing clear user-facing progress updates during tool-heavy work.

##### Fixed

- The chat composer no longer forces mobile newline-on-Enter behavior on touch-primary devices that also have a fine pointer present (tablet plus Bluetooth keyboard, detachable Surface, iPad plus Magic Keyboard), so Enter submits with desktop semantics when a real keyboard is in the picture.
- The active-session external-refresh fallback poll now fires every 30 s instead of every 5 s. The SSE session-events stream already pushes invalidations in real time, so the poll is only a fallback; the slower interval removes visible scroll jitter and a network/CPU floor on long sessions.
- The model picker no longer fuzzy-matches a provider-qualified model id (`@provider:model` or slash-qualified `vendor/model`) to a nearby curated sibling once exact lookup fails, preserving the raw typed value so uncatalogued models stay routable instead of silently snapping to a different model.
- Joplin notes search now keeps the `Authorization` header and adds a query-token compatibility shim only for Web Clipper `/search` calls, covering clipper builds that return HTTP 403 for header-only search auth while keeping other Joplin API URLs token-free.

</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/749
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
…context_messages truncation (nesquena#3102)

Co-authored-by: AlexeyDsov <AlexeyDsov@users.noreply.github.com>
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
…ture state.db recovery rows

Codex regression-gate finding: since Session.save() no longer auto-clears the
truncation_watermark, the unconditional 'timestamp > watermark' skip in
merge_session_messages_append_only became a permanent ceiling — a genuine future
state.db-only row (recovery/compaction, missed by the sidecar) would be silently
dropped from /api/session and model-context reconstruction forever. Only apply the
above-watermark skip while the sidecar has NOT advanced past the watermark. Preserves
the nesquena#2914 deleted-tail filtering (revert-verified). Adds 2 regression tests.
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
v0.51.197: stop agent replaying edited/undone messages (nesquena#3102)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changes-requested Maintainer left detailed feedback requesting changes; PR is waiting on author to address hold

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants