fix(webui): overlay real state.db message count for subagent children so they don't vanish from the sidebar (#5308)

Regression seam behind #5308: a delegated subagent child's sidebar row is built
from a stale sidecar that reports message_count==0, and the state.db count
overlay in _apply_sidebar_state_db_override_metadata was gated on
`state_db_source == 'webui'`. A subagent child (state_db_source=='subagent')
therefore never received its true message count, so the front-end visibility
predicate (_sidebarRowHasVisibleMessages) dropped the row and the subagent
session disappeared entirely (not nested, not orphaned) after #5244+#5306.

Fix: widen the count/last-message overlay to `state_db_source in ('webui','subagent')`,
keeping the same conservative anti-resurrection guard. The source-tag/title
reassignment stays WebUI-only so a subagent child keeps its subagent
classification. Same state.db-blind-metadata root as the #5307 transcript
recovery, fixed server-side rather than by loosening the front-end predicate
(which would fight the #5306 active-parent scoping).

Tests: subagent child gets its count overlaid + classification preserved; a
non-webui/non-subagent foreign source (cron) still gets NO overlay.

Fixes #5308.
This commit is contained in:
nesquena-hermes committed 2026-07-01 18:06:21 +00:00
1 parent 412cf01ac2
commit 8b8ee92eca
2 files changed
+96

No files matched your search

+14
View File
@@ -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))
@@ -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