Skip to content

Release v0.51.279 — Release IU (stage-p3h — preserve Activity/streaming turn on mid-stream scroll #3665) - #3686

Merged
nesquena-hermes merged 2 commits into
masterfrom
release/stage-p3h
Jun 5, 2026
Merged

nesquena-hermes merged 2 commits into
masterfrom
release/stage-p3h

Conversation

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Release v0.51.279 — Release IU (stage-p3h)

Phase-3-MEDIUM (high-risk-per-LOC merge/data-loss family) — #3665 (mysoul12138; fixes #3346). Full gate run with deep both-directions scrutiny.

Fixed

Gates

  • Pre-Opus gate: merge-markers clean, node -c + ast.parse clean, ruff CLEAN, ESLint runtime CLEAN, browser smoke CLEAN
  • Full suite: 7884 passed, 0 failed (a first run hit the boot-cascade flake; clean on re-run — confirmed by 30 PR tests + 206/241 broader merge/dedup/sidecar tests passing in isolation)
  • Codex regression gate: SAFE TO SHIP — verified BOTH directions: distinct tool-call turns preserved (stable json.dumps(tool_calls,sort_keys=True) key); genuine same-message sidecar replays still collapse (full dedup runs before the preservation branches); indexed visible-key unpacking safe; no cross-session S.toolCalls leak (cleared on switch)
  • Opus advisor: SHIP — both directions confirmed; the PR's own test_same_tool_calls_..._merge_to_one (no duplication) + test_different_tool_calls_..._both_preserved (no drop) pass

Closes #3665. (Non-blocking nit noted by Opus: the dedup_key not in seen_dedup_keys guard at models.py:4327 is defensive/unreachable-False — left as-is.)

nesquena-hermes and others added 2 commits June 5, 2026 21:32
@nesquena-hermes
nesquena-hermes merged commit b5caf83 into master Jun 5, 2026
11 checks passed
@nesquena-hermes
nesquena-hermes deleted the release/stage-p3h branch June 5, 2026 21:45
@greptile-apps

greptile-apps Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two co-operating bugs that caused the Activity panel and current streaming turn to disappear when a user scrolled up to load earlier messages during an active stream. The backend now includes tool_calls in the message merge/dedup/visible keys so assistant messages invoking different tools with identical empty content and same-second timestamps are no longer collapsed. The frontend now guards _syncToolCallsForLoadedMessages from running while streaming is active, skips the heavy message reload during mid-stream scroll, and adds a 5-second post-stream cooldown to prevent an immediate external-refresh from wiping the Activity panel.

  • Backend (api/models.py): _session_message_merge_key, _session_message_dedup_key, and _session_message_visible_key now each include a stable json.dumps(tool_calls, sort_keys=True) component; a preservation branch in merge_session_messages_append_only lets state.db rows with distinct tool_calls pass through the sidecar-timestamp gate even when the content key is already seen.
  • Frontend (static/sessions.js, static/messages.js, static/ui.js): Streaming guards added at _syncToolCallsForLoadedMessages, jumpToSessionStart, and refreshActiveSessionIfExternallyUpdated; tool-card DOM insertion in renderMessages is allowed whenever S.toolCalls is populated; the _visWithIdxCache invalidation is extended to detect wholesale S.messages replacement at the same array length.

Confidence Score: 4/5

Safe to merge; the two-pronged fix is well-tested and both directions (dedup vs. preserve) are covered by the new regression suite.

The core merge logic change is carefully scoped and the end-to-end tests verify both deduplicated and preserved outcomes. Two minor issues exist: the dedup_key not in seen_dedup_keys sub-expression in the preservation branch is always True (the PR notes this itself), and window._streamJustFinished is not scoped to a session, which could briefly suppress a refresh for a different session after a session switch within the 5-second cooldown window. Neither affects the correctness of the primary fix.

api/models.py (the merge_session_messages_append_only logic change) and static/sessions.js (the cooldown flag and streaming guards) deserve the most scrutiny.

Important Files Changed

Filename Overview
api/models.py Core merge/dedup/visible key functions now include tool_calls (JSON-serialized, sort_keys=True) so assistant messages with different tool invocations but identical empty content/timestamp are no longer collapsed. A preservation branch in merge_session_messages_append_only lets state.db rows with distinct tool_calls pass through the sidecar-timestamp guard.
api/routes.py Removes the conditional that zeroed out session-level tool_calls when truncated messages already had per-message tool metadata; now always sends windowed (or full) session-level tool_calls so the browser can merge older messages that lack per-message variants.
static/sessions.js _syncToolCallsForLoadedMessages now returns early when S.busy
static/messages.js Adds clearVisibleMessageRowCache() in the stream-start session-replace block. Done handler expands _messageRenderWindowSize before the final renderMessages. The 5-second cooldown flag (window._streamJustFinished) is set here.
static/ui.js Adds _visWithIdxCacheSrc reference tracking for cache invalidation on wholesale S.messages replacement. jumpToSessionStart skips the full message load and renderMessages during active streaming. Tool-card DOM insertion unlocked during streaming when S.toolCalls is populated.
tests/test_merge_key_tool_calls.py New regression test suite covering merge_key, dedup_key, visible_key differentiation for messages with distinct tool_calls, and end-to-end merge_session_messages_append_only behaviour in both directions.
tests/test_session_tail_payload.py Test updated to reflect the routes.py change: payload now includes windowed session tool_calls even when messages carry per-message tool metadata.
tests/test_smooth_text_fade.py Fixes the test helper that extracted the _scheduleRender function block — the brace-counting parser chokes on template-literal braces; now searches the full file string.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant SSE as SSE Stream
    participant API as /api/session
    participant Sidecar as Sidecar (memory)
    participant StateDB as state.db

    Note over Browser: User scrolls up mid-stream
    Browser->>API: "GET /api/session?msg_limit=N"
    Note over Browser: S.busy=true - skip _ensureAllMessagesLoaded
    Note over Browser: skip renderMessages during stream

    SSE-->>Browser: tool_call event - S.toolCalls updated
    SSE-->>Browser: done event
    Browser->>Browser: "window._streamJustFinished=true (5s cooldown)"
    Browser->>Browser: Expand _messageRenderWindowSize
    Browser->>Browser: renderMessages preserveScroll
    Note over Browser: S.toolCalls intact - Activity panel preserved

    API->>Sidecar: merge_session_messages_append_only
    Sidecar->>StateDB: fetch state rows
    Note over Sidecar,StateDB: tool_calls now in merge/dedup/visible keys
    Note over Sidecar,StateDB: different tool_calls + same timestamp - PRESERVE
    StateDB-->>Sidecar: state rows
    Sidecar-->>API: merged messages (distinct tool turns retained)
    API-->>Browser: session payload with windowed tool_calls

    Browser->>Browser: _syncToolCallsForLoadedMessages
    Note over Browser: guard: if S.busy OR S.activeStreamId - skip
Loading

Reviews (1): Last reviewed commit: "docs(changelog): v0.51.279 — Release IU ..." | Re-trigger Greptile

Comment thread api/models.py
Comment on lines +4326 to +4327
if _ck in seen_content_keys and dedup_key not in seen_dedup_keys:
pass # different tool_calls from sidecar — preserve

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The dedup_key not in seen_dedup_keys sub-expression is always True here. By line 4272, any message whose dedup_key was already in seen_dedup_keys has already been skipped via continue, so the condition can never be False when execution reaches this point. The guard is harmless but misleading — a reader might assume it provides a meaningful second check. Simplifying to just _ck in seen_content_keys makes the intent clearer.

Suggested change
if _ck in seen_content_keys and dedup_key not in seen_dedup_keys:
pass # different tool_calls from sidecar — preserve
if _ck in seen_content_keys:
pass # different tool_calls from sidecar — preserve

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread static/sessions.js
Comment on lines +2984 to +2987
// Cooldown: don't force-reload immediately after streaming ends — the
// "done" event already delivered the final messages. Reloading here would
// clear S.toolCalls and lose Activity.
if(typeof window !== 'undefined' && window._streamJustFinished) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 window._streamJustFinished is not scoped to the active session. If the user switches sessions while the flag is live (within the 5-second window), refreshActiveSessionIfExternallyUpdated will be silently suppressed for the new session as well, even though that session's stream has nothing to do with the flag. A session-keyed value (e.g. window._streamJustFinishedSid = activeSid set alongside the flag, then checked as window._streamJustFinished && window._streamJustFinishedSid === S.session.session_id) would limit the cooldown to only the session whose stream just ended.

SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
…ng turn on mid-stream scroll nesquena#3665) (nesquena#3686)

* fix(streaming): preserve Activity + streaming turn when loading earlier messages mid-stream (nesquena#3665, nesquena#3346)

Co-authored-by: mysoul12138 <839465496@qq.com>

* docs(changelog): v0.51.279 — Release IU (stage-p3h)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: mysoul12138 <839465496@qq.com>
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.

merge_session_messages_append_only fails to deduplicate legacy messages in state

1 participant