Skip to content

Release v0.51.626 — keep sidebar in sync after Desktop app continues a session (#4834) - #4841

Closed
nesquena-hermes wants to merge 4 commits into
masterfrom
stage/4834-desktop-reconcile
Closed

nesquena-hermes wants to merge 4 commits into
masterfrom
stage/4834-desktop-reconcile

Conversation

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Release v0.51.626 — keep the sidebar in sync after the Desktop app continues a session (#4834)

Ships #4834 (by @franksong2702). Reconciliation fix on the WebUI↔state.db sidebar path.

What it does

When the official Hermes Desktop App appends settled rows to the same Hermes Agent state.db session that was started in the WebUI, the WebUI conversation sidebar showed a stale message count. It now refreshes the WebUI-origin row's count/timestamp from state.db even while external/CLI sessions are hidden. The no-duplicate-prefix merge and single-reconciled-transcript-on-next-turn behaviors (pre-existing) are now covered by regression tests for this Desktop-continuation scenario.

Gate (deep — this is the crown-jewel reconciliation family where the #4772 P0 lives)

  • Codex (GPT-5.5): SAFE TO SHIP — sidebar override applies only count/timestamp/source-tag (no hidden-session content leak), no message-loss/duplication across the reconciliation hunks; 31 reconciliation tests pass.
  • Opus (claude-opus-4-8): SAFE TO SHIP — confirmed the genuinely-new surface is "read-only, webui-gated, content-free, monotonic-up — structurally cannot cause the ghost/message-loss class because it never writes message content and never touches the merge/save path." (Flagged a cosmetic count-vs-watermark display divergence — display-only, no data loss.)
  • Full suite: 10366 passed, 0 failed.

CHANGELOG wording tightened per the Opus review to reflect that the dedup/writeback guarantees are tested here, not introduced.

Closes #4834.

Frank Song and others added 3 commits June 24, 2026 18:32
…iation) + tighten CHANGELOG wording per Opus (sidebar-overlay is the new bit; dedup/writeback are tested-not-introduced)
@greptile-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This release PR ships the Desktop-app sidebar sync fix (#4834): when the Hermes Desktop App appends settled rows to a WebUI-origin state.db session, the WebUI sidebar now refreshes that row's message count and timestamp even while external/CLI sessions are hidden.

  • api/routes.py: Removes the early-return in _session_list_cache_source_stamp that was pinning the cache stamp to all-zeros when show_cli_sessions=False, silently suppressing state.db cache invalidation for the WebUI-origin Desktop-continuation case.
  • api/models.py: Extends _read_state_db_sidebar_overrides to pull message_count from the sessions table and cross-check with a COUNT(*)/MAX(timestamp) query on the messages table; extends _apply_sidebar_state_db_override_metadata to apply the higher count/newer timestamp to the sidebar row when the Desktop has appended more messages than the WebUI currently shows.
  • Tests: Two new regression tests cover the Desktop-continuation sidebar overlay and the no-duplicate-prefix full-load case, plus additional assertions on the saved message list in the existing context-reconciliation test.

Confidence Score: 4/5

Safe to merge for the core scenario; two pre-existing concerns flagged in prior threads (updated_at in the recency guard, isdigit float coercion on actual_message_count) are the only outstanding items and neither causes data loss.

The routes.py fix is clean and the models.py enrichment is read-only and content-free. Two issues already raised in the previous review round remain unaddressed: the updated_at inclusion in current_last can suppress a valid Desktop-turn refresh, and the isdigit() guard silently zeroes float-valued actual_message_count. Both affect only the sidebar display fields with no message content or write path involved.

api/models.py — specifically the recency guard in _apply_sidebar_state_db_override_metadata (lines 3346-3364) where the two concerns from prior threads remain unresolved.

Important Files Changed

Filename Overview
api/models.py Adds message-count/timestamp enrichment to _read_state_db_sidebar_overrides and guard logic to _apply_sidebar_state_db_override_metadata; two known issues (updated_at in guard, isdigit() float coercion) were already flagged in prior review threads.
api/routes.py Removes the all-zeros short-circuit in _session_list_cache_source_stamp that blocked state.db cache invalidation when show_cli_sessions=False; intentional correctness fix with clear explanatory comment.
tests/test_webui_state_db_reconciliation.py Adds two well-scoped regression tests for the Desktop-continuation sidebar overlay and no-duplicate-prefix full-load scenario, plus a helper for appending rows to state.db in tests.
tests/test_webui_state_db_context_reconciliation.py Adds saved-content assertions to verify no deduplication occurs after a WebUI+external-messages reconciliation; drops an unused json import.
CHANGELOG.md Release entry for v0.51.626 added by the release process, as expected for this repo.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant DA as Desktop App
    participant SDB as state.db
    participant CS as Cache Stamp
    participant SO as _read_state_db_sidebar_overrides
    participant AM as _apply_sidebar_state_db_override_metadata
    participant SB as WebUI Sidebar

    Note over DA,SDB: Desktop App continues a WebUI-origin session
    DA->>SDB: Appends settled rows (messages table)
    Note over CS: Previously: all-zeros stamp when show_cli_sessions=False
    Note over CS: Now: state.db mtime/fingerprint always watched
    SB->>CS: Poll /api/sessions
    CS->>SDB: Check mtime + WAL stat + content fingerprint
    SDB-->>CS: Stamp changed to cache miss
    CS->>SO: Rebuild session list
    SO->>SDB: SELECT sessions (id, source, message_count)
    SO->>SDB: "SELECT COUNT(*), MAX(timestamp) FROM messages GROUP BY session_id"
    SDB-->>SO: "state_count=4, state_last=1003.0"
    SO-->>AM: "metadata with _state_db_message_count=4 and _state_db_last_message_at=1003.0"
    AM->>AM: "Guard: state_count(4) > current_count(2) AND state_last > current_last"
    AM->>SB: "session.message_count=4, last_message_at=1003.0"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant DA as Desktop App
    participant SDB as state.db
    participant CS as Cache Stamp
    participant SO as _read_state_db_sidebar_overrides
    participant AM as _apply_sidebar_state_db_override_metadata
    participant SB as WebUI Sidebar

    Note over DA,SDB: Desktop App continues a WebUI-origin session
    DA->>SDB: Appends settled rows (messages table)
    Note over CS: Previously: all-zeros stamp when show_cli_sessions=False
    Note over CS: Now: state.db mtime/fingerprint always watched
    SB->>CS: Poll /api/sessions
    CS->>SDB: Check mtime + WAL stat + content fingerprint
    SDB-->>CS: Stamp changed to cache miss
    CS->>SO: Rebuild session list
    SO->>SDB: SELECT sessions (id, source, message_count)
    SO->>SDB: "SELECT COUNT(*), MAX(timestamp) FROM messages GROUP BY session_id"
    SDB-->>SO: "state_count=4, state_last=1003.0"
    SO-->>AM: "metadata with _state_db_message_count=4 and _state_db_last_message_at=1003.0"
    AM->>AM: "Guard: state_count(4) > current_count(2) AND state_last > current_last"
    AM->>SB: "session.message_count=4, last_message_at=1003.0"
Loading

Reviews (2): Last reviewed commit: "ci: re-trigger (prior run wedged ~25min ..." | Re-trigger Greptile

Comment thread api/models.py
Comment on lines +3346 to +3356
current_last = max(
float(session.get('last_message_at') or 0),
float(session.get('updated_at') or 0),
)
except (TypeError, ValueError):
current_last = 0.0
try:
state_last = float(state_db_last_message_at or 0)
except (TypeError, ValueError):
state_last = 0.0
if state_count > current_count and (state_last <= 0 or state_last > current_last):

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 updated_at in guard can block valid count refreshes

current_last is the max of last_message_at and updated_at, and the gate state_last > current_last must clear both. If the session's updated_at was bumped by a non-message event (e.g., title rename, settings flush) after the last WebUI message, a Desktop-app turn that lands before the next WebUI message may produce a state_last that exceeds last_message_at but still falls below updated_at, silently suppressing the sidebar refresh. Using only last_message_at as the reference point would be a tighter semantic fit for "is the Desktop turn newer than the last message the WebUI knows about?"

Comment thread api/models.py
Comment on lines +3358 to +3361
session['actual_message_count'] = max(
state_count,
int(session.get('actual_message_count') or 0) if str(session.get('actual_message_count') or '').isdigit() else 0,
)

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 isdigit() guard silently zeroes float-valued actual_message_count

str(4.0).isdigit() is False, so if actual_message_count is a float (e.g., came through JSON deserialization without an explicit cast), the guard evaluates to else 0 and the field is written as max(state_count, 0) instead of max(state_count, 4). A plain try: int(...) except: 0 pattern (matching every other integer-coercion in this function) would handle floats correctly and be consistent with the surrounding style.

…ilure; local full suite 10366 + 31 reconciliation tests green)
@nesquena-hermes

Copy link
Copy Markdown
Collaborator Author

Closing — source PR #4834 bounced for a parallel test-isolation regression (full xdist suite reds test_session_list_cache_follower_wait_stage_when_rebuild_inflight). Will re-stage once #4834's CI is green.

@nesquena-hermes
nesquena-hermes deleted the stage/4834-desktop-reconcile branch June 24, 2026 12:47
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.

1 participant