Skip to content

fix(sessions+compression+tool-card): stale unread dot, timer leak, tool card duplication (#3020, #2973) - #3116

Closed
mysoul12138 wants to merge 2 commits into
nesquena:masterfrom
mysoul12138:fix/3020-2973
Closed

mysoul12138 wants to merge 2 commits into
nesquena:masterfrom
mysoul12138:fix/3020-2973

Conversation

@mysoul12138

Copy link
Copy Markdown
Contributor

Summary

Three bugfixes, rebased onto current master (post-#3069):

What's NOT in this PR

Test plan

  • Manual: view session with new messages → navigate away → no stale dot
  • Manual: trigger auto-compression → timer clears on completion
  • Manual: run terminal tool → card header shows progress, detail shows result (no duplication)

🤖 Generated with Claude Code

…esquena#3020, nesquena#2973)

nesquena#3020: Sync viewed-count in the polling path for actively-viewed sessions
so navigating away doesn't show a stale unread dot. Defensive clear of
completion-unread marker in _setSessionViewedCount.

nesquena#2973: Clear elapsed-timer attributes and interval when a live compression
card transitions from running to done, preventing the orphan timer from
overwriting the completed card state. Guarded by active-session check.

Tool card duplication: Route tool_complete result to tc.snippet (detail)
instead of tc.preview (header) to prevent identical content appearing in
both the card header and expanded detail section.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Review — three changes, mixed verdict

Pulled the PR worktree and read all three diffs against origin/master. Two of the three look good; the third has a regression risk I want to flag before merge.

#3020 (unread dot) — LGTM

The _setSessionViewedCount change in static/sessions.js:142-147:

counts[sid] = next;
_saveSessionViewedCounts();
// If the viewed count is now current, any prior completion-unread marker is
// stale — clear it so _hasUnreadForSession doesn't short-circuit (#3020).
_clearSessionCompletionUnread(sid);

…plus the _markPollingCompletionUnreadTransitions change at :407-419:

if (completedObservedStream || completedPersistedObservedStream || completedWithNewMessages) {
  if (!_isSessionActivelyViewedForList(sid)) {
    _markSessionCompletionUnread(sid, s.message_count);
  } else {
    // Sync viewed count so we don't flag stale unread on tab switch (#3020)
    _setSessionViewedCount(sid, messageCount);
  }
}

This matches the earlier triage prescription on #3020 exactly — sync the viewed count for actively-viewed sessions on every polling cycle, and defensively clear the completion-unread marker inside the count setter. Belt-and-braces, good.

#2973 (compression timer leak) — LGTM

The appendLiveCompressionCard patch at static/ui.js:5675-5689 clears data-compression-started-at and data-compression-message on the completion path, and conditionally calls _clearCompressionElapsedTimer() only when no other session has a running auto-compression:

const _activeCompState = _compressionStateForCurrentSession();
if (!_activeCompState || !_activeCompState.automatic || _activeCompState.phase !== 'running') {
  _clearCompressionElapsedTimer();
}

The active-session guard is important — _compressionElapsedTimer at static/ui.js:2392-2393 is module-global, not per-session, so unconditionally clearing it on every compressed event would kill the timer driving a different session's display. The guard is correct.

Tool-card duplication — please reconsider

Reading the live tool event flow:

  • tool event at static/messages.js:1598 creates tc={name, preview:d.preview||'', args, snippet:'', done:false, tid:...} and pushes it.
  • tool_complete at :1650 (origin/master) does tc.preview = d.preview || tc.preview || ''; (overwrites header with result).

The PR's replacement at the same site:

if(d.preview){
  tc.snippet=tc.snippet||d.preview;
  if(!tc.preview) tc.preview=d.preview;
}

Trace through the two real cases against buildToolCard at static/ui.js:6905-6937 (hasDetail = tc.snippet || hasArgs, previewText = tc.preview||displaySnippet||''):

With-progress tool (agent emitted progress text via tool event):

  • Old: header = result, no detail block. Loses progress text.
  • New: header = last progress text, detail = result. Better.

No-progress tool (most tools — tool event arrives with empty preview):

  • Old: tc.preview = result, tc.snippet = '', hasDetail = false. Header = result, no detail.
  • New: tc.preview = result (empty fills from fallback), tc.snippet = result, hasDetail = true. Header = result, detail = result. Duplication introduced.

The "no progress" case is the common one — most non-streaming tools (shell exec, read_file, search) don't emit interim tool events with a preview payload. They go straight from tool-with-empty-preview to tool_complete-with-result-snippet. After this PR, those cards render the same text in both the header pill and the expanded detail body.

Suggested fix

Either:

  1. Make detail-block rendering skip the duplicate case. In buildToolCard at static/ui.js:6909, change hasDetail to:

    const hasDetail = (tc.snippet && tc.snippet !== tc.preview) || (tc.args && Object.keys(tc.args).length>0);

    Cheap, contained to one file, no logic change in the event handler.

  2. Or only set tc.snippet when d.preview differs from tc.preview. In the tool_complete handler:

    if(d.preview){
      if(tc.preview && tc.preview !== d.preview){
        tc.snippet = tc.snippet || d.preview;   // genuine progress->result split
      } else {
        tc.preview = d.preview;                 // no-progress: keep old behavior
      }
    }

I'd lean toward option 1 — same effect with less branching, and it also covers the reload-path duplication at static/ui.js:6660-6695 where derived entries get snippet:... populated but no preview, so reload-time also produces equal-content header/detail on master.

Other notes

  • S.toolCalls accumulation: static/messages.js:1605 in the tool handler still blindly pushes a new tc on every progress event. For a tool with two progress events plus a completion, you end up with three entries in inflight.toolCalls, only one of which gets matched in the tool_complete reverse-search at :1639-1644 (the first not-done one). The others stay done:false. Not introduced by this PR — already on master — but worth filing separately if it's affecting per-card render count.
  • The "What's NOT in this PR" list is helpful. Dropping test_pr_3019_3020_2973.py is fine for now, but I'd want regression coverage on the Session completion unread dot persists after navigating away from actively-viewed session #3020 path before this lands so the actively-viewed-count-sync logic doesn't silently regress.

Verdict

#3020 part: LGTM. #2973 part: LGTM. Tool-card-duplication part: please reconsider — under the current shape it fixes one class of card (with-progress) by regressing another (no-progress, which is most tools). I'd be inclined to merge the first two changes now and pull the third out for revision with option 1 above. Happy to look at a v2 of just the tool-card change.

When tc.snippet === tc.preview (common for no-progress tools where
both are set to the same result_snippet), the detail block would show
identical content as the header. Skip the detail block in this case.

This also handles the reload-path where derived entries get snippet
populated but no preview, so displaySnippet falls back to the snippet
content for the header — same deduplication applies.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Shipped in v0.51.161 (Release EG, stage-batch43) via release PR #3147 — thank you, @mysoul12138! 🎉

Your change (stale unread dot + compression-timer leak + tool-card dedup (#3020, #2973)) is now on master, authorship preserved.

This PR was cherry-picked onto current master (its branch base was several releases stale; a naive merge would have reverted intervening releases).

GitHub didn't auto-close because the release merged the reparented commit rather than your branch's exact head SHA, so closing manually. Full diff + tests verified on master; the merged release branch passed the full sequential pytest suite (6823 tests, 0 failures), Opus-advisor reviewed. Closing as shipped.

pull Bot pushed a commit to jw5812018/hermes-webui that referenced this pull request May 30, 2026
mysoul12138 added a commit to mysoul12138/hermes-webui that referenced this pull request May 30, 2026
…quena#3116

Upstream sessions.js _setSessionViewedCount now directly calls
_clearSessionCompletionUnread (PR nesquena#3116, merged via nesquena#3116 stage-batch43).
Bug 1 was already removed in commit 1a7385a (upstream PR nesquena#3069).
session-patches.js is now fully redundant.
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants