Skip to content

Fix compression-exhausted stream finalization - #3316

Closed
franksong2702 wants to merge 16 commits into
nesquena:masterfrom
franksong2702:franksong2702/fix-auto-compression-tool-heavy-streams
Closed

Fix compression-exhausted stream finalization#3316
franksong2702 wants to merge 16 commits into
nesquena:masterfrom
franksong2702:franksong2702/fix-auto-compression-tool-heavy-streams

Conversation

@franksong2702

@franksong2702 franksong2702 commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Thinking Path

  • Long single-turn, tool-heavy sessions can exhaust context after Hermes Agent fails to compress effectively.
  • WebUI must distinguish streamed interim/progress text from a real final assistant answer.
  • A persisted transcript ending in a tool result, assistant tool-call turn, or internal context-compaction reference marker is not a completed answer.
  • The UI should surface compression exhaustion as an error and keep internal reference-only summaries out of the settled transcript.

What Changed

  • Classifies compression exhaustion errors from agent/provider result text.
  • Treats failed, partial, compression_exhausted, tool-tail transcripts, assistant tool-call tails, and context-compaction marker tails as terminal failures instead of completed turns.
  • Removes the _assistant_added short-circuit so final-answer validation always checks the persisted transcript.
  • Keeps [CONTEXT COMPACTION — REFERENCE ONLY] content out of settled transcript rendering while preserving transient running compression status.
  • Adds regression coverage for terminal failure detection, context-compaction marker filtering, and final-answer semantics.
  • Updates the changelog for the user-visible behavior change.

Why It Matters

This prevents long tool-heavy sessions from appearing completed when Hermes Agent stopped before writing a final assistant answer. It also prevents internal context-compaction reference text from being rendered as user-facing final content.

Related to #3315, NousResearch/hermes-agent#36624, and NousResearch/hermes-agent#36626.

Verification

  • python -m pytest tests/test_auto_compression_terminal_failure.py tests/test_auto_compression_card.py tests/test_issues_373_374_375.py tests/test_issue765_streaming_persistence.py::TestIssue765FollowupHardening::test_silent_failure_path_does_not_reacquire_agent_lock -q
  • node --check static/ui.js
  • node --check static/messages.js
  • git diff --check

Risks / Follow-ups

  • This is the WebUI companion to the Hermes Agent compression fix; it does not by itself reduce agent-side context size.
  • The change intentionally avoids rendering internal compaction reference text in settled transcripts, while still allowing transient compression status during active runs.

Contract Routing

  • Contract family: runtime streaming finalization and session transcript visibility.
  • Evidence: focused regression tests for terminal failure detection, final-answer semantics, and settled transcript rendering.
  • Contract change: none intended; this restores the invariant that completed UI state requires a real final assistant answer.

Model Used

AI-assisted implementation with OpenAI GPT-5 Codex in a local coding workflow. The assistant inspected repository code, wrote targeted tests, implemented the fix, and ran the verification commands above.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Triage: hold + changes-requested — thanks @franksong2702. The goal is right (surfacing compression-exhaustion as a real error instead of a falsely-"completed" turn) and the classifier itself is sound — I verified _session_lacks_final_assistant_answer() empirically: it correctly returns HAS-final-answer for a normal completed turn and a tool→final-text turn, and terminal-failure only for tool-result/tool-call/empty-assistant/marker tails. I picked this up to advance toward release and ran it through the full gate (test suite + Opus + the Codex regression gate). Opus cleared it, but the Codex gate caught a state-consistency ordering bug on the compression path that I then confirmed against the code, so I'm holding it rather than shipping.

(CORE) Terminal-failure handling can run BEFORE the compression session-id migration, leaving frontend/backend session state inconsistent when compression exhaustion fires after the agent rotated session_id.

The new terminal-failure check is at api/streaming.py:5439-5443:

_terminal_failure = (
    _agent_result_terminal_failure(result)
    or _session_lacks_final_assistant_answer(_all_result_messages)
)
if _terminal_failure:
    _assistant_added = False
if _terminal_failure or (not _assistant_added and not _token_sent):
    ... # emits the apperror + returns

But the compression session-id migration + snapshot preservation block runs much later, at api/streaming.py:5680+:

_preserve_pre_compression_snapshot(s, old_sid)   # ~5680
... # migrate locks/cache, register continuation, emit `compressed`, save against new sid

Hermes Agent rotates agent.session_id during compression (agent/conversation_compression.py:505-520); WebUI only mirrors that rotation in the 5680+ block. So on a compression-exhausted result that arrives after the rotation, the terminal-failure path at 5443 emits the error + returns before 5680 ever runs — which means:

  • _preserve_pre_compression_snapshot() is skipped → the pre-compression history may not be archived
  • continuation/session migration is skipped → the error transcript is saved against the old WebUI session id
  • the frontend apperror path appends a synthetic error to the old activeSid, not the migrated continuation session
    → frontend/backend session state diverges (this is the same compression-rotation subsystem where we recently held fix: keep gateway context visible in chat transcripts #3300 for a transcript-loss regression, so we're being extra careful here).

Suggested fix (needs your design call — I didn't want to hot-patch compression-rotation ordering under release pressure):

  1. Factor the compression-rotation side-effect block (_preserve_pre_compression_snapshot + lock/cache migration + continuation registration) into a helper and run it BEFORE any return from the terminal-failure apperror path — OR move the terminal-failure check below the compression migration.
  2. Persist the terminal error on the migrated continuation session, and in the frontend (static/messages.js apperror handler) adopt the settled/migrated session like the done path does, instead of only pushing a local synthetic error into the old active session.
  3. Add a regression test for: compression exhausted AFTER session-id rotation → assert the snapshot is preserved, the continuation session is registered, and the error lands on the migrated session.

Everything else is good — the compression_exhausted classification, the label cascade, and the frontend clear-compression-UI handling are all correct. One small non-blocking note from Opus: _classify_provider_error only inspects the error string, so if a future agent path sets result['compression_exhausted']=True with an empty/non-matching error message it falls back to the generic "No response from provider" label (your included test sets both fields, so the current path is covered).

The rest of this session's transcript/streaming fixes (#3102 edit-replay, #3321 recovery-control filter) already shipped, so this isn't a wholesale rejection — just this one ordering interaction to sort out. Happy to pair on the helper-extraction if useful. No rush.

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

greptile-apps Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes streaming finalization for sessions where Hermes Agent failed before producing a final assistant answer (compression exhaustion, tool-tail transcripts). It moves compression migration logic before terminal-failure detection, adds _agent_result_terminal_failure and _session_lacks_final_assistant_answer helpers, enriches apperror payloads with session data before enqueue, and suppresses [CONTEXT COMPACTION — REFERENCE ONLY] markers from the settled transcript.

  • api/streaming.py: Compression session rotation is now detected and migrated (session ID, lock, agent cache) before terminal-failure classification; silent-failure apperror payloads now carry a full session snapshot and correct old_session_id / new_session_id fields before enqueue.
  • static/messages.js: The apperror handler now uses old_session_id/new_session_id fields for session routing instead of activeSid, replacing S.session and S.messages from the server payload when a session rotation occurred.
  • static/ui.js: _isContextCompactionText is extracted as a standalone helper; the settled-transcript render path unconditionally skips compaction marker messages and suppresses them from reference-card selection via _shouldShowSettledCompressionReference.

Confidence Score: 4/5

Safe to merge with the minor background-error routing note addressed; core logic for compression detection, session migration, and settled-transcript filtering is correct.

The compression migration ordering, terminal-failure classification, payload enrichment ordering, and settled-transcript suppression are all logically sound and covered by focused regression tests. The one open concern is that trackBackgroundError is called with the pre-rotation session ID when the user has navigated away during a compression-exhausted failure, which could direct the background-error notification to the archived snapshot rather than the continuation session where the error was persisted. This does not affect in-view users or the persistence of the error message itself.

static/messages.js — the trackBackgroundError call in the background-stream error path

Important Files Changed

Filename Overview
api/streaming.py Moves compression migration before terminal-failure detection, adds _agent_result_terminal_failure and _session_lacks_final_assistant_answer helpers, enriches apperror payloads with session snapshot and session IDs before enqueue; includes new _classify_provider_error branch for compression_exhausted.
static/messages.js Rewrites the apperror routing to use old_session_id and new_session_id fields for session matching, replacing S.session from the server payload on compression rotation. Adds compression_exhausted label and clearCompressionUi call.
static/ui.js Extracts _isContextCompactionText helper, adds _shouldShowSettledCompressionReference guard to suppress compaction markers from the settled reference card, and unconditionally skips compaction marker messages in the main render loop.
tests/test_auto_compression_terminal_failure.py New regression test file covering terminal failure detection, session rotation snapshot, payload enrichment ordering, apperror routing logic, and context compaction marker handling.
tests/test_auto_compression_card.py Updated to reflect the new rendering behaviour where compaction markers are always skipped and _shouldShowSettledCompressionReference guards the reference card.
tests/test_issue765_streaming_persistence.py Updated silent-failure guard string to match the refactored _terminal_failure combined guard.
CHANGELOG.md Adds accurate user-visible entries for both the terminal-failure finalization fix and the suppression of compaction reference markers.

Sequence Diagram

sequenceDiagram
    participant UI as Browser
    participant Server as streaming.py
    participant Agent as Hermes Agent
    UI->>Server: "POST /stream session=A"
    Server->>Agent: run_conversation()
    Agent-->>UI: stream delta tokens
    Agent-->>Server: "result with failed=True and compression_exhausted=True"
    Server->>Server: Detect session rotation A to C
    Server->>Server: Preserve snapshot and migrate session object
    Server->>Server: _agent_result_terminal_failure returns True
    Server->>Server: Persist error message on session C
    Server->>Server: "Enrich apperror payload with old_session_id=A"
    Server-->>UI: "SSE apperror type=compression_exhausted old_session_id=A new_session_id=C"
    alt User still on session A
        UI->>UI: "eventSid=A matches currentSid"
        UI->>UI: Replace S.session with continuation session C
        UI->>UI: Update URL and render error bubble
    else User navigated away
        UI->>UI: eventMatchesCurrent is false
        UI->>UI: trackBackgroundError for session A
    end
Loading

Reviews (6): Last reviewed commit: "Merge origin/master for PR #3316 conflic..." | Re-trigger Greptile

Comment thread static/messages.js Outdated
Comment thread api/streaming.py
Comment thread api/streaming.py
Frank Song added 3 commits June 4, 2026 12:11
…uto-compression-tool-heavy-streams

# Conflicts:
#	CHANGELOG.md
…uto-compression-tool-heavy-streams

# Conflicts:
#	CHANGELOG.md
@franksong2702

Copy link
Copy Markdown
Contributor Author

Pushed follow-up 473fb51a plus merge updates to current master.

What changed:

  • Addressed the P1 static/messages.js review: the apperror session match no longer treats activeSid as a match fallback. It now applies an error to the visible session only when the payload session id or continuation session id matches the currently loaded session, so background stream failures route through trackBackgroundError instead of attaching to an unrelated session.
  • Addressed the payload-ordering review in api/streaming.py: terminal apperror payloads are now enriched with the session snapshot and session ids before put('apperror', ...), so enqueue no longer relies on mutating the same dict later.
  • Added one extra companion guard while reviewing the worker patch: the generic exception apperror path now includes session_id / old_session_id before enqueue as well. This keeps the stricter frontend matching compatible with existing non-compression errors.
  • Left _agent_result_terminal_failure() partial semantics unchanged. For this PR, partial remains a non-done terminal result unless we separately verify a different Hermes Agent contract.

Verification:

  • node --check static/messages.js
  • ./.venv/bin/pytest tests/test_auto_compression_terminal_failure.py tests/test_auto_compression_card.py tests/test_issue765_streaming_persistence.py tests/test_issues_373_374_375.py -q -> 111 passed
  • git diff --check

PR is now mergeable again after merging current origin/master; GitHub checks are running on head a5f13e1e.

AI assistance: implemented by Codex sub-agent gpt-5.3-codex-spark, reviewed and adjusted by Codex GPT-5.

Comment thread api/streaming.py
@franksong2702

Copy link
Copy Markdown
Contributor Author

Addressed the new Greptile P1 on the exception path in 8ad4dc3d.

What changed:

  • In the generic exception apperror path, session_id remains the persisted/current session id via getattr(s, 'session_id', session_id).
  • old_session_id now uses the original _run_agent_streaming(..., session_id=...) argument, so users still viewing the pre-compression session can match the inline error after an agent-side session rotation.
  • Strengthened the regression test so it asserts the exact routing assignment, not just that both fields exist.

Verification:

  • ./.venv/bin/pytest tests/test_auto_compression_terminal_failure.py tests/test_auto_compression_card.py tests/test_issue765_streaming_persistence.py tests/test_issues_373_374_375.py -q -> 111 passed
  • git diff --check

AI assistance: Codex GPT-5 reviewed the bot comment, made the targeted fix, and re-ran the focused regression gate.

@franksong2702

Copy link
Copy Markdown
Contributor Author

Updated with merge commit e50e6b0 to bring the branch onto latest master and resolve the CHANGELOG conflict while preserving the compression-exhausted finalization fixes. Local verification: git diff --check; pytest tests/test_auto_compression_terminal_failure.py tests/test_run_journal_frontend_static.py tests/test_webui_state_db_context_reconciliation.py tests/test_issue2481_selected_text_reply.py tests/test_readme_compat_section.py -q (25 passed).

@franksong2702

Copy link
Copy Markdown
Contributor Author

Conflict cleanup pushed in c2c7c04b.

What changed:

  • Merged latest origin/master into franksong2702/fix-auto-compression-tool-heavy-streams.
  • Resolved the CHANGELOG.md conflict by keeping the PR's compression-exhausted finalization notes in Unreleased and preserving current release notes.
  • No behavior-code conflicts were needed; the PR diff against current master remains focused on compression-exhausted terminal handling and settled compression-reference rendering.

Verification:

  • python -m pytest tests/test_auto_compression_terminal_failure.py tests/test_run_journal_frontend_static.py tests/test_webui_state_db_context_reconciliation.py tests/test_issue2481_selected_text_reply.py -q -> 25 passed
  • node --check static/messages.js static/ui.js
  • git diff --check
  • GitHub Actions: 11/11 passed on c2c7c04b

AI assistance: Codex coordinated the conflict cleanup, delegated the merge cleanup to a sub-agent, then reviewed the diff and reran focused verification before pushing.

@franksong2702

Copy link
Copy Markdown
Contributor Author

Conflict cleanup pushed in 08c501d0.

What changed:

  • Merged latest origin/master (4c545a33) into franksong2702/fix-auto-compression-tool-heavy-streams.
  • Resolved the CHANGELOG.md conflict by keeping this PR's compression-exhausted / tool-tail finalization notes in Unreleased and preserving current release history.
  • No manual runtime/static conflict resolution was needed; the diff against current master remains scoped to compression-exhausted terminal handling and settled compression-reference rendering.

Verification:

  • git diff --check origin/master..HEAD -> passed
  • node --check static/messages.js static/ui.js -> passed
  • /Users/xuefusong/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_auto_compression_terminal_failure.py tests/test_auto_compression_card.py tests/test_issue765_streaming_persistence.py tests/test_run_journal_frontend_static.py tests/test_webui_state_db_context_reconciliation.py tests/test_issue2481_selected_text_reply.py tests/test_readme_compat_section.py -q -> 93 passed
  • GitHub Actions on 08c501d0 -> 11/11 passed
  • GitHub mergeability -> MERGEABLE / CLEAN

AI assistance: Codex coordinated the conflict cleanup with a sub-agent, then re-reviewed the streaming/static diff and reran focused verification before pushing.

@franksong2702
franksong2702 marked this pull request as draft June 5, 2026 08:54
@franksong2702

Copy link
Copy Markdown
Contributor Author

Marking this PR as Draft to align with the current hold / changes-requested state and reduce reviewer confusion while #3401 is still active.

This remains a valid live-to-final terminal/no-final corner case: compression-exhausted or tool-heavy runs must not settle as a normal completed answer when no real final assistant answer exists. However, #3401 now overlaps some of the same streaming finalization guards, so keeping this PR ready before #3401 lands risks reviewing the same lifecycle logic in two places.

Plan:

Refs #3401 and #3315.

nesquena-hermes added a commit that referenced this pull request Jun 6, 2026
…s surface as errors #3316 fixes #3315) (#3705)

* fix(#3315): surface compression-exhausted/no-final-answer turns as errors (#3316)

When Hermes Agent exhausts context compression in a long tool-heavy turn, the
streamed result can end on a tool result / assistant(tool_calls) turn with no
final assistant answer. WebUI was finalizing that as a completed response.
Now _session_lacks_final_assistant_answer() + _agent_result_terminal_failure()
classify these as terminal failures and surface an apperror instead. The
compression session-id migration + pre-compression snapshot now run BEFORE the
terminal-failure return (ordering bug from the prior hold) so state stays
consistent when exhaustion fires after the agent rotated session_id.

Co-authored-by: Frank Song <franksong2702@gmail.com>

* docs(changelog): v0.51.292 — Release JH (stage-s4, #3316 fixes #3315)

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Shipped in v0.51.292 🎉 Thanks @franksong2702 — you fixed the compression-rotation ordering bug I held on (the migration + pre-compression snapshot + continuation registration now run BEFORE the terminal-failure return, so state stays consistent when exhaustion fires after session_id rotation) and refined the classifier so a streamed answer (_token_sent) is never misclassified as terminal-failure. Re-gated fresh on a 3-way merge onto current master: full suite 7993, Codex SAFE + Opus SAFE (both verified the migration-before-failure ordering, no false-positive on legit completions, error persists on the migrated continuation session, and [CONTEXT COMPACTION] markers are filtered from settled cards). Closes #3315.

eleboucher pushed a commit to eleboucher/homelab that referenced this pull request Jun 6, 2026
…➔ 0.51.293) (#856)

This PR contains the following updates:

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

---

### Release Notes

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

### [`v0.51.293`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051293--2026-06-06--Release-JI-stage-s5--thinking-card-no-longer-renders-twice)

[Compare Source](nesquena/hermes-webui@v0.51.292...v0.51.293)

##### Fixed

- **The "Thinking" card no longer renders twice on a settled turn.** For a turn that had both a tool call and reasoning (e.g. think → call a tool → answer), the thinking card could appear once inside the collapsed **Activity** group at the top of the turn and again as a stranded second card below the answer and the `Done in …` footer. The thinking-only inline render path (added in v0.51.258 for [#&#8203;3592](nesquena/hermes-webui#3592)) now only fires when the turn has no Activity group of its own, and when it does render inline it inserts the card **above** the answer body instead of after the footer. Thinking that echoes the visible answer on a trailing reasoning-only message is also de-duplicated against the whole turn's answer text now, not just the same message's body. Genuinely thinking-only turns still show their thinking inline (the [#&#8203;3592](nesquena/hermes-webui#3592) fix is preserved, not reverted). ([#&#8203;3709](nesquena/hermes-webui#3709); supersedes [#&#8203;3708](nesquena/hermes-webui#3708))

### [`v0.51.292`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051292--2026-06-06--Release-JH-stage-s4--compression-exhausted-turns-surface-as-errors-not-fake-completions)

[Compare Source](nesquena/hermes-webui@v0.51.291...v0.51.292)

##### Fixed

- **Context-compression-exhausted turns are no longer finalized as a falsely "completed" response.** When Hermes Agent exhausts context compression in a long tool-heavy turn, the streamed result can end on a tool result or an assistant `tool_calls` turn with no final assistant answer. WebUI previously rendered that as a settled, completed reply. It now classifies a persisted transcript that ends in a tool/tool-call/empty-assistant tail (or an internal `[CONTEXT COMPACTION — REFERENCE ONLY]` marker) — and `compression_exhausted`/`failed`/`partial` agent results — as a terminal failure and surfaces a clear error instead. The compression session-id migration and pre-compression snapshot now run **before** the terminal-failure path returns, so frontend/backend session state stays consistent when exhaustion fires after the agent rotates `session_id`. ([#&#8203;3316](nesquena/hermes-webui#3316), [@&#8203;franksong2702](https://github.com/franksong2702); fixes [#&#8203;3315](nesquena/hermes-webui#3315))

### [`v0.51.291`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051291--2026-06-06--Release-JG-stage-s2--preserve-live-turn-content-when-switching-away-mid-stream)

[Compare Source](nesquena/hermes-webui@v0.51.290...v0.51.291)

##### Fixed

- **Switching away from a streaming session no longer loses the in-progress thinking/tool content.** When you clicked to another chat while a session was streaming during a quiet window (mid tool-execution or silent reasoning, between content events) and then switched back, the live turn's tool cards and thinking could disappear permanently — only the elapsed-time clock survived — until the response finished and the transcript re-rendered from the server. Cause: the live-turn DOM snapshot was only captured on content/`tool_complete` SSE events, so the switch-away teardown could run with a stale-or-absent snapshot, and the switch-back fallback rebuilt an empty thinking card. `closeLiveStream()` now snapshots the live turn **before** tearing the stream down, so switching back restores the exact state shown at switch-away. ([#&#8203;3668](nesquena/hermes-webui#3668))

### [`v0.51.290`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051290--2026-06-06--Release-JF-stage-s1--profile-providermodel-now-respected-in-session-resolution)

[Compare Source](nesquena/hermes-webui@v0.51.289...v0.51.290)

##### Fixed

- **Profile-bound sessions now resolve their provider and model from the profile** instead of silently falling back to the global active provider. Previously, when a chat was started under a profile and the model string was not `@provider:`-qualified (and no explicit provider was sent), the backend used the catalog's global active provider — so a profile wired to one provider/key could silently run on a different one, causing **wrong credentials/billing** and **silent context truncation** (the global default model's advertised context window could differ from what the provider actually served, so the provider dropped the oldest messages and long chats "forgot" earlier content). Resolution is now authoritative from the profile across all four runtime entry points (chat start, streaming worker incl. background/btw runs, and both deferred `/api/session` display resolvers); stale models are still repaired under the profile provider — including the `openai-codex` profile + stale `openai/…` slash-model case — while native slash IDs on OpenRouter/custom providers are preserved and explicit `@provider:` qualifiers still win. ([#&#8203;3448](nesquena/hermes-webui#3448), [@&#8203;rodboev](https://github.com/rodboev); fixes [#&#8203;3405](nesquena/hermes-webui#3405))

### [`v0.51.289`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051289--2026-06-06--Release-JE-hotfix--sidebar-ReferenceError-3696--scope-undef-prevention-gate)

[Compare Source](nesquena/hermes-webui@v0.51.288...v0.51.289)

##### Fixed

- **Sidebar no longer crashes with `ReferenceError: _sessionAttentionState is not defined`.** The session-attention helper was declared *inside* `renderSessionListFromCache()` and relied on function hoisting, but the top-level `_sidebarRowHasVisibleMessages` (reached via `renderSessionListFromCache` → `_partitionSidebarSessionRows`) called it bare — and hoisting is scoped to the enclosing function, so every sidebar cache-render threw and the session list went blank. `_sessionAttentionState` is now a top-level function reachable by both call sites. Regressed in [#&#8203;3672](nesquena/hermes-webui#3672) (v0.51.269). ([#&#8203;3696](nesquena/hermes-webui#3696))
- **Stale-stream terminal events no longer risk a `ReferenceError: source is not defined`.** `_bailOutOfTerminalEventsFromStaleStream` (declared inside `attachLiveStream`) called `_closeSource(source)` against a `source` that was not in its lexical scope — it would have thrown on the late-finalizing-stream path when the user is back in an active session. `source` is now threaded as an explicit parameter. Found by the new scope gate below during review. ([#&#8203;3696](nesquena/hermes-webui#3696))

##### Internal

- **New static-JS scope/undefined-reference gate (`scripts/scope_undef_gate.py`).** Models the WebUI's classic-`<script>` shared global scope and runs ESLint `no-undef` per file, flagging a function that is defined only *nested* but called from a sibling/top-level scope — the brick class behind [#&#8203;3696](nesquena/hermes-webui#3696) that `node --check`, source-presence tests, and the existing `no-const-assign` runtime gate all miss. Wired into the CI `lint` job alongside the `no-const-assign`/`no-import-assign` runtime gate, with an in-suite test (`tests/test_static_js_scope_undef.py`) and a focused structural regression test (`tests/test_issue3696_session_attention_scope.py`). ([#&#8203;3696](nesquena/hermes-webui#3696))

### [`v0.51.288`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051288--2026-06-06--Release-JD-stage-r24--collapsible-approval-card)

[Compare Source](nesquena/hermes-webui@v0.51.287...v0.51.288)

##### Added

- **The tool-call approval card can be collapsed to a thin header strip.** A chevron toggle in the approval-card header shrinks the card to just its "Approval required" heading so the tool-call rationale and transcript scrolled above it stay readable; clicking again re-expands it. Includes full ARIA (`aria-expanded`/`aria-controls`/`aria-label`), an icon swap, and transcript reflow that preserves a near-bottom scroll position. State resets to expanded for each new approval, so a fresh approval is never hidden. ([#&#8203;3515](nesquena/hermes-webui#3515), [@&#8203;rodboev](https://github.com/rodboev); closes [#&#8203;3007](nesquena/hermes-webui#3007))

### [`v0.51.287`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051287--2026-06-06--Release-JC-stage-r22--WeCom-session-classification--worker-profile-picker-hiding)

[Compare Source](nesquena/hermes-webui@v0.51.286...v0.51.287)

##### Fixed

- **WeCom gateway sessions are now classified as messaging conversations.** Rows arriving with raw sources `wecom` / `wecom_callback` are normalized into the messaging category (alongside weixin/telegram/discord/slack/email) and given proper "WeCom" / "WeCom Callback" display names, so they group and surface correctly in the sidebar. ([#&#8203;3653](nesquena/hermes-webui#3653), [@&#8203;franksong2702](https://github.com/franksong2702))

##### Changed

- **Worker profiles are hidden from the chat profile picker.** Worker profiles (used for orchestrator/Kanban dispatch) are no longer offered as normal human chat targets in the picker, while still appearing in the profile management view with a "Hidden from chat" badge. The active profile is never hidden. ([#&#8203;3662](nesquena/hermes-webui#3662), [@&#8203;Chukwuebuka-20](https://github.com/Chukwuebuka-20))

### [`v0.51.286`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051286--2026-06-06--Release-JB-stage-r21--sidebar-tab-reordering)

[Compare Source](nesquena/hermes-webui@v0.51.285...v0.51.286)

##### Added

- **Drag-reorder for sidebar tabs.** In Settings → Appearance, the "Sidebar tabs" chips (Tasks, Kanban, Skills, Memory, Spaces, Profiles, Todos, Insights, Logs) can be dragged to reorder how they appear in the left rail and sidebar nav, persisted via a sanitized `tab_order` setting (collapses duplicates, rejects `chat`/`settings`, strips non-strings). Chat and Settings stay fixed. Reorder is pointer/desktop-based (consistent with the existing Kanban drag-and-drop); the chips remain tappable for show/hide on touch. ([#&#8203;3067](nesquena/hermes-webui#3067), [@&#8203;ai-ag2026](https://github.com/ai-ag2026))

### [`v0.51.285`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051285--2026-06-06--Release-JA-stage-r19--update-reload-server-identity-race-fix)

[Compare Source](nesquena/hermes-webui@v0.51.284...v0.51.285)

##### Fixed

- **Don't reload the page until the *replacement* server is actually up after an update.** The post-update reload previously compared raw `/health` uptime, which couldn't distinguish a still-running old process from the restarted one (it could reload against the old process or hang). The client now reads a stable `server_started_at` identity before the update POST and reloads only once `/health` reports a *different* identity (with a null-baseline fallback). Both the force-update and regular apply paths read and pass the baseline. ([#&#8203;3654](nesquena/hermes-webui#3654), [@&#8203;franksong2702](https://github.com/franksong2702); [#&#8203;874](nesquena/hermes-webui#874))

### [`v0.51.284`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051284--2026-06-05--Release-IZ-stage-w4--sidebar-status-labels--cron-sessions-toggle)

[Compare Source](nesquena/hermes-webui@v0.51.283...v0.51.284)

##### Added

- **Manual session status labels (Todo / In Progress / Done).** Tag any session from its row's ⋯ menu with a colored status badge (blue Todo / amber In Progress / green Done), stored per-session in localStorage. The badge renders inline on the sidebar row and uses theme variables so it adapts to light/dark and skins. ([#&#8203;3570](nesquena/hermes-webui#3570), [@&#8203;rodboev](https://github.com/rodboev))
- **"Show cron sessions" preference** (Settings → Preferences). Surfaces cron-job output as conversations in the sidebar. Off by default and gated under "Show non-WebUI sessions" — only active once non-WebUI sessions are enabled — with a note that high-frequency jobs can flood the sidebar. ([#&#8203;3514](nesquena/hermes-webui#3514), [@&#8203;rodboev](https://github.com/rodboev); closes [#&#8203;2841](nesquena/hermes-webui#2841))

### [`v0.51.283`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051283--2026-06-05--Release-IY-stage-w2--composer-queue-hint-during-auto-compaction)

[Compare Source](nesquena/hermes-webui@v0.51.282...v0.51.283)

##### Fixed

- **The composer now tells you a message will queue during auto-compaction instead of looking dead.** While automatic compression runs, the send button previously went `disabled` with only a "Waiting for compression to finish" tooltip. It now shows a `queue` action with the placeholder + tooltip "Type a message — it will queue and send after compression", so you can type and have it sent automatically when compaction completes. ([#&#8203;3512](nesquena/hermes-webui#3512), [@&#8203;rodboev](https://github.com/rodboev); closes [#&#8203;3079](nesquena/hermes-webui#3079))

### [`v0.51.282`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051282--2026-06-05--Release-IX-stage-3544--surface-memoryskill-saves-in-Activity-summary)

[Compare Source](nesquena/hermes-webui@v0.51.281...v0.51.282)

##### Added

- **The collapsed Activity summary now shows when the agent saved a memory or updated a skill** — e.g. "Activity: 2 tools, 1 memory saved, 1 skill updated" — so persistent-state changes are visible at a glance without expanding the group. Detection matches the real tool action vocabularies (`memory`: add/replace count as saves, `remove` excluded; `skill_manage`: create/patch/edit/write\_file count as updates, delete/remove\_file excluded), and only completed, non-errored calls are counted. The memory/skill counts are subtracted from the tool count so it reflects only non-memory/skill tools. Classification is stamped as durable `data-*` attributes so the suffix survives the live tool-call group's HTML snapshot/restore on session switch. Sessions with no memory/skill writes render the unchanged "Activity: N tools" label. ([#&#8203;3544](nesquena/hermes-webui#3544), [@&#8203;rodboev](https://github.com/rodboev); closes [#&#8203;3340](nesquena/hermes-webui#3340))

### [`v0.51.281`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051281--2026-06-05--Release-IW-stage-verdigris--Verdigris-emeraldbronze-skin)

[Compare Source](nesquena/hermes-webui@v0.51.280...v0.51.281)

##### Added

- **New "Verdigris" appearance skin** — a dark-only emerald/forest-green palette (`#&#8203;0F1714` background, `#&#8203;121D18` sidebar) with bronze-gold accents (`#C89A5A`), named for the green-bronze patina on aged copper. Selectable in Settings → Appearance and via `/theme verdigris`. Fully scoped under `:root.dark[data-skin="verdigris"]` (no bleed into the default appearance or other skins), with component-level accents for the new-chat button, scrollbar, tool cards, tree viewer, session badges/tags, diff blocks, MCP status, and image lightbox. ([#&#8203;3602](nesquena/hermes-webui#3602), [@&#8203;rodboev](https://github.com/rodboev); closes [#&#8203;3357](nesquena/hermes-webui#3357))

### [`v0.51.280`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051280--2026-06-05--Release-IV-stage-p3i--Windows-self-update-restart-fix)

[Compare Source](nesquena/hermes-webui@v0.51.279...v0.51.280)

##### Fixed

- **Self-update now restarts correctly on Windows.** `os.execv` does not replace the current process on Windows (it spawns a new one while the old keeps running), so the old process held port 8787 and the new process failed to bind ("address already in use"), surfacing as "Update failed" after the timeout. On Windows the restart now launches a detached new process (`subprocess.Popen` with `DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP`) and exits the old one immediately to release the port, plus a bounded bind-retry loop in `server_bind()` (up to 10s) to ride out the `SO_EXCLUSIVEADDRUSE` teardown window. POSIX behavior is unchanged (still `os.execv`). ([#&#8203;3647](nesquena/hermes-webui#3647), [@&#8203;jja881](https://github.com/jja881))

### [`v0.51.279`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051279--2026-06-05--Release-IU-stage-p3h--preserve-Activitystreaming-turn-on-mid-stream-scroll)

[Compare Source](nesquena/hermes-webui@v0.51.278...v0.51.279)

##### Fixed

- **Loading earlier messages during an active stream no longer wipes the Activity panel or the current streaming turn.** Two causes: (1) the message merge/dedup keys didn't include `tool_calls`, so assistant messages invoking *different* tools with identical empty content and same-second timestamps collapsed into one — dropping every state.db tool-call after the first the sidecar registered; (2) `_syncToolCallsForLoadedMessages` cleared `S.toolCalls` while `S.busy` blocked the `renderMessages` rebuild. `tool_calls` is now part of the merge/dedup/visible keys (with a preservation branch so distinct tool invocations within the sidecar timestamp window aren't skipped), and the frontend keeps the live tool-call/streaming state when paging in history. ([#&#8203;3665](nesquena/hermes-webui#3665), [@&#8203;mysoul12138](https://github.com/mysoul12138); fixes [#&#8203;3346](nesquena/hermes-webui#3346))

### [`v0.51.278`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051278--2026-06-05--Release-IT-stage-p3g--repair-inline-PDF-preview)

[Compare Source](nesquena/hermes-webui@v0.51.277...v0.51.278)

##### Fixed

- **Inline PDF preview in chat now renders again.** The PDF.js loader previously created a `<script>` with both `src` and `textContent` set (the latter is ignored when `src` is present), so PDF.js never initialized and the preview hung on the spinner before degrading to a download link. It now loads PDF.js via a blob module script that sets the worker source, passes `isEvalSupported:false` to harden the parser, and revokes the blob URL on load. CSP gains `blob:` in `script-src` and a scoped `worker-src blob: 'self' https://cdn.jsdelivr.net` to permit the worker. ([#&#8203;3652](nesquena/hermes-webui#3652), [@&#8203;xx77yy](https://github.com/xx77yy); closes [#&#8203;3649](nesquena/hermes-webui#3649))

</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/856
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
…s surface as errors nesquena#3316 fixes nesquena#3315) (nesquena#3705)

* fix(nesquena#3315): surface compression-exhausted/no-final-answer turns as errors (nesquena#3316)

When Hermes Agent exhausts context compression in a long tool-heavy turn, the
streamed result can end on a tool result / assistant(tool_calls) turn with no
final assistant answer. WebUI was finalizing that as a completed response.
Now _session_lacks_final_assistant_answer() + _agent_result_terminal_failure()
classify these as terminal failures and surface an apperror instead. The
compression session-id migration + pre-compression snapshot now run BEFORE the
terminal-failure return (ordering bug from the prior hold) so state stays
consistent when exhaustion fires after the agent rotated session_id.

Co-authored-by: Frank Song <franksong2702@gmail.com>

* docs(changelog): v0.51.292 — Release JH (stage-s4, nesquena#3316 fixes nesquena#3315)

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
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