diff --git a/CHANGELOG.md b/CHANGELOG.md index e6755608..d54c5d80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ _No unreleased changes. Entries are moved into their version block when a releas ### Fixed +- **Delegated subagent sessions no longer vanish from the sidebar.** A subagent (delegate_task) child whose sidebar row was built from a stale sidecar reporting 0 messages now receives its real message count from the agent state.db, so it stays visible (nested under its parent) instead of being silently dropped by the sidebar visibility filter. (#5308) + - **No more scroll jump-back on mobile while messages load.** When the app realigns the viewport after loading older messages, mobile browsers no longer double-compensate the scroll position (their native overflow-anchor fighting the app's own scroll write), which caused a visible jump. Desktop behavior is unchanged. (#5338) - **No more white flicker/flash while the assistant streams on light themes.** Live token-by-token markdown updates no longer inherit the global dark/light `color`/`background` transition, so the streaming turn paints instantly instead of briefly fading on each token. Theme-switch transitions elsewhere are unchanged. (#5328) diff --git a/api/models.py b/api/models.py index 060b2b11..9e5ab1b9 100644 --- a/api/models.py +++ b/api/models.py @@ -4028,6 +4028,20 @@ def _apply_sidebar_state_db_override_metadata(sessions: list[dict], metadata: di session['session_source'] = state_db_session_source session['source_label'] = state_db_source_label session['is_cli_session'] = False + # Overlay the real state.db message count for WebUI-owned rows AND for + # delegated subagent children (#5308). A subagent child + # (state_db_source == 'subagent') is backed by the delegate runner's + # state.db session, but its sidebar row is built from a stale sidecar + # that often reports message_count == 0. Without overlaying the true + # count, the front-end visibility predicate + # (_sidebarRowHasVisibleMessages) drops the row and the subagent + # session vanishes from the sidebar entirely (regression seam behind + # #5308, same state.db-blind-metadata root as the #5307 transcript + # recovery). The count overlay keeps the same conservative + # anti-resurrection guard used for WebUI rows. The source-tag / title + # reassignment above stays WebUI-only — a subagent child keeps its + # subagent classification. + if state_db_source in ('webui', 'subagent'): try: current_count = max(0, int(session.get('message_count') or 0)) state_count = max(0, int(state_db_message_count or 0)) diff --git a/tests/test_webui_state_db_reconciliation.py b/tests/test_webui_state_db_reconciliation.py index 441e1a31..3e2f1175 100644 --- a/tests/test_webui_state_db_reconciliation.py +++ b/tests/test_webui_state_db_reconciliation.py @@ -155,6 +155,88 @@ def test_sidebar_state_db_overlay_preserves_numeric_actual_count(): assert sessions[0]["actual_message_count"] == 5 +def test_sidebar_state_db_overlay_counts_subagent_child_5308(): + """#5308: a delegated subagent child whose stale sidecar reports + message_count == 0 must receive its real state.db message count so the + front-end sidebar visibility predicate does not drop the row (the subagent + session vanishing regression). The overlay applies to source == 'subagent' + just like 'webui', while the subagent source classification is preserved. + """ + import api.models as models + + sid = "subagent_child_5308" + sessions = [ + { + "session_id": sid, + "source_tag": "subagent", + "raw_source": "subagent", + "session_source": "other", + "relationship_type": "child_session", + "parent_session_id": "parent_abc", + "message_count": 0, + "actual_message_count": 0, + "last_message_at": 0, + "updated_at": 0, + } + ] + + models._apply_sidebar_state_db_override_metadata( + sessions, + { + sid: { + "_state_db_source": "subagent", + "_state_db_message_count": 6, + "_state_db_last_message_at": 2002.0, + } + }, + ) + + # Real state.db count is now overlaid (was 0) -> row survives the sidebar + # visibility predicate instead of vanishing. + assert sessions[0]["message_count"] == 6 + assert sessions[0]["actual_message_count"] == 6 + assert sessions[0]["last_message_at"] == 2002.0 + # Subagent classification is preserved (source-tag reassignment stays + # WebUI-only) — the child does not get re-tagged as a webui session. + assert sessions[0]["source_tag"] == "subagent" + assert sessions[0].get("is_cli_session") is not False + + +def test_sidebar_state_db_overlay_does_not_count_foreign_cli_source_5308(): + """Guard the #5308 overlay scope: a non-webui, non-subagent foreign source + (e.g. a messaging/cron/tui CLI row) must NOT get the count overlay — only + WebUI-owned rows and delegated subagent children do. + """ + import api.models as models + + sid = "cron_row_5308" + sessions = [ + { + "session_id": sid, + "source_tag": "cron", + "message_count": 0, + "actual_message_count": 0, + "last_message_at": 0, + "updated_at": 0, + } + ] + + models._apply_sidebar_state_db_override_metadata( + sessions, + { + sid: { + "_state_db_source": "cron", + "_state_db_message_count": 9, + "_state_db_last_message_at": 3003.0, + } + }, + ) + + # cron is neither webui nor subagent -> count overlay must NOT apply. + assert sessions[0]["message_count"] == 0 + assert sessions[0]["actual_message_count"] == 0 + + def test_tail_cancelled_partial_blocks_state_db_replay(): from api.models import merge_session_messages_append_only