From ab48096de1d32f4b3a90f711b098ad50d30067e4 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Tue, 14 Jul 2026 03:20:25 -0400 Subject: [PATCH] feat(#6033): show delegated goal titles for child sessions --- api/models.py | 41 ++++ tests/test_session_lineage_metadata_api.py | 209 +++++++++++++++++++++ 2 files changed, 250 insertions(+) diff --git a/api/models.py b/api/models.py index 3dc133ea..b3542ada 100644 --- a/api/models.py +++ b/api/models.py @@ -5173,13 +5173,16 @@ def _read_state_db_sidebar_overrides( has_messages_table = cur.fetchone() is not None messages_has_session_id = False messages_has_timestamp = False + messages_has_title_fields = False if has_messages_table: cur.execute("PRAGMA table_info(messages)") message_cols = {str(row[1]) for row in cur.fetchall()} messages_has_session_id = 'session_id' in message_cols messages_has_timestamp = 'timestamp' in message_cols + messages_has_title_fields = {'session_id', 'role', 'content', 'timestamp'}.issubset(message_cols) overrides: dict[str, dict] = {} + delegated_title_ids: set[str] = set() ids = list(wanted) chunk_size = 500 for i in range(0, len(ids), chunk_size): @@ -5200,6 +5203,12 @@ def _read_state_db_sidebar_overrides( if state_title: entry['_state_db_title'] = state_title state_source = str(row['source'] or '').strip().lower() + if ( + state_source == 'subagent' + and sid in count_wanted + and ' '.join(state_title.split()) == 'Subagent Session' + ): + delegated_title_ids.add(sid) if state_source: entry['_state_db_source'] = state_source source_meta = normalize_agent_session_source(state_source) @@ -5244,6 +5253,29 @@ def _read_state_db_sidebar_overrides( entry['_state_db_last_message_at'] = float(row['last_message_at'] or 0) except (TypeError, ValueError): pass + if messages_has_title_fields and delegated_title_ids: + seen_user_messages: set[str] = set() + delegated_title_ids = list(delegated_title_ids) + for i in range(0, len(delegated_title_ids), chunk_size): + chunk = delegated_title_ids[i:i + chunk_size] + placeholders = ','.join('?' * len(chunk)) + cur.execute( + f""" + SELECT session_id, role, content, timestamp + FROM messages + WHERE session_id IN ({placeholders}) AND role = 'user' + ORDER BY session_id, timestamp ASC + """, + chunk, + ) + for row in cur.fetchall(): + sid = str(row['session_id']) + if sid in seen_user_messages: + continue + display_title = title_from([dict(row)], fallback='') + if display_title: + seen_user_messages.add(sid) + overrides.setdefault(sid, {})['_state_db_display_title'] = display_title return overrides except Exception: return {} @@ -5298,6 +5330,7 @@ def _apply_sidebar_state_db_override_metadata(sessions: list[dict], metadata: di state_db_source_label = entry.pop('_state_db_source_label', None) state_db_message_count = entry.pop('_state_db_message_count', None) state_db_last_message_at = entry.pop('_state_db_last_message_at', None) + state_db_display_title = entry.pop('_state_db_display_title', None) if state_db_source == 'webui': session['source_tag'] = state_db_source_tag session['raw_source'] = state_db_raw_source @@ -5357,6 +5390,12 @@ def _apply_sidebar_state_db_override_metadata(sessions: list[dict], metadata: di ): session['_state_db_title'] = state_db_title session['display_title'] = state_db_title + if ( + state_db_display_title + and state_db_source == 'subagent' + and ' '.join(str(title or '').split()) == 'Subagent Session' + ): + session['display_title'] = state_db_display_title def _enrich_sidebar_lineage_metadata(sessions: list[dict]) -> None: @@ -5628,6 +5667,8 @@ def title_from(messages, fallback: str='Untitled'): for m in messages: if m.get('role') == 'user': c = m.get('content', '') + if c is None: + continue if isinstance(c, list): c = ' '.join(p.get('text', '') for p in c if isinstance(p, dict) and p.get('type') == 'text') text = _strip_attached_files_marker(str(c)) diff --git a/tests/test_session_lineage_metadata_api.py b/tests/test_session_lineage_metadata_api.py index d0ab29d4..22635e57 100644 --- a/tests/test_session_lineage_metadata_api.py +++ b/tests/test_session_lineage_metadata_api.py @@ -58,6 +58,27 @@ def _ensure_state_db(path): return conn +def _ensure_messages_table(conn): + conn.execute( + """ + CREATE TABLE messages ( + session_id TEXT, + role TEXT, + content TEXT, + timestamp REAL + ) + """ + ) + + +def _insert_state_message(conn, sid, *, role, content, timestamp): + conn.execute( + "INSERT INTO messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)", + (sid, role, content, timestamp), + ) + conn.commit() + + def _insert_state_row(conn, sid, *, title=None, parent=None, ended_at=None, end_reason=None, started_at=None, source='webui', session_source=None): conn.execute( """ @@ -391,6 +412,194 @@ def test_generic_webui_title_gets_read_only_state_db_display_title(_isolate): conn.close() +def test_generic_subagent_title_gets_goal_display_title(_isolate): + conn = _ensure_state_db(_isolate) + _ensure_messages_table(conn) + t0 = time.time() - 100 + try: + _save_webui_session("lineage_api_subagent_goal", title="Subagent Session", updated_at=t0) + _insert_state_row( + conn, + "lineage_api_subagent_goal", + title="Subagent Session", + source="subagent", + started_at=t0, + ) + _insert_state_message( + conn, + "lineage_api_subagent_goal", + role="user", + content="Find the root cause of the failing sidebar test", + timestamp=t0 + 1, + ) + + row = {row["session_id"]: row for row in all_sessions(include_lineage_metadata=False)}["lineage_api_subagent_goal"] + + assert row["title"] == "Subagent Session" + assert row["display_title"] == "Find the root cause of the failing sidebar test" + finally: + conn.close() + + +def test_custom_subagent_title_stays_authoritative(_isolate): + conn = _ensure_state_db(_isolate) + _ensure_messages_table(conn) + t0 = time.time() - 100 + try: + _save_webui_session("lineage_api_subagent_custom", title="Investigate auth", updated_at=t0) + _insert_state_row( + conn, + "lineage_api_subagent_custom", + title="Investigate auth", + source="subagent", + started_at=t0, + ) + _insert_state_message( + conn, + "lineage_api_subagent_custom", + role="user", + content="A different goal", + timestamp=t0 + 1, + ) + + row = {row["session_id"]: row for row in all_sessions(include_lineage_metadata=False)}["lineage_api_subagent_custom"] + + assert row["title"] == "Investigate auth" + assert "display_title" not in row + finally: + conn.close() + + +def test_generic_subagent_title_falls_back_without_first_user_message(_isolate): + conn = _ensure_state_db(_isolate) + _ensure_messages_table(conn) + t0 = time.time() - 100 + try: + _save_webui_session("lineage_api_subagent_empty", title="Subagent Session", updated_at=t0) + _insert_state_row( + conn, + "lineage_api_subagent_empty", + title="Subagent Session", + source="subagent", + started_at=t0, + ) + _insert_state_message( + conn, + "lineage_api_subagent_empty", + role="assistant", + content="Only assistant output", + timestamp=t0 + 1, + ) + + row = {row["session_id"]: row for row in all_sessions(include_lineage_metadata=False)}["lineage_api_subagent_empty"] + + assert row["title"] == "Subagent Session" + assert "display_title" not in row + finally: + conn.close() + + +def test_generic_subagent_title_skips_null_first_user_message(_isolate): + conn = _ensure_state_db(_isolate) + _ensure_messages_table(conn) + t0 = time.time() - 100 + try: + _save_webui_session("lineage_api_subagent_null_first", title="Subagent Session", updated_at=t0) + _insert_state_row( + conn, + "lineage_api_subagent_null_first", + title="Subagent Session", + source="subagent", + started_at=t0, + ) + _insert_state_message( + conn, + "lineage_api_subagent_null_first", + role="user", + content=None, + timestamp=t0 + 1, + ) + _insert_state_message( + conn, + "lineage_api_subagent_null_first", + role="user", + content="Recover the next usable delegated title", + timestamp=t0 + 2, + ) + + row = {row["session_id"]: row for row in all_sessions(include_lineage_metadata=False)}["lineage_api_subagent_null_first"] + + assert row["title"] == "Subagent Session" + assert row["display_title"] == "Recover the next usable delegated title" + finally: + conn.close() + + +def test_generic_subagent_title_respects_sidebar_override_cap(_isolate, monkeypatch): + conn = _ensure_state_db(_isolate) + _ensure_messages_table(conn) + older = time.time() - 200 + newer = time.time() - 100 + try: + monkeypatch.setenv("HERMES_WEBUI_STATE_DB_OVERRIDE_TOP_N", "1") + _save_webui_session("lineage_api_subagent_old", title="Subagent Session", updated_at=older) + _save_webui_session("lineage_api_subagent_new", title="Subagent Session", updated_at=newer) + _insert_state_row( + conn, + "lineage_api_subagent_old", + title="Subagent Session", + source="subagent", + started_at=older, + ) + _insert_state_row( + conn, + "lineage_api_subagent_new", + title="Subagent Session", + source="subagent", + started_at=newer, + ) + _insert_state_message( + conn, + "lineage_api_subagent_old", + role="user", + content="Older delegated title", + timestamp=older + 1, + ) + _insert_state_message( + conn, + "lineage_api_subagent_new", + role="user", + content="Newest delegated title", + timestamp=newer + 1, + ) + + rows = {row["session_id"]: row for row in all_sessions(include_lineage_metadata=False)} + + assert rows["lineage_api_subagent_new"]["display_title"] == "Newest delegated title" + assert "display_title" not in rows["lineage_api_subagent_old"] + finally: + conn.close() +def test_generic_subagent_title_falls_back_without_messages_table(_isolate): + conn = _ensure_state_db(_isolate) + t0 = time.time() - 100 + try: + _save_webui_session("lineage_api_subagent_no_messages", title="Subagent Session", updated_at=t0) + _insert_state_row( + conn, + "lineage_api_subagent_no_messages", + title="Subagent Session", + source="subagent", + started_at=t0, + ) + + row = {row["session_id"]: row for row in all_sessions(include_lineage_metadata=False)}["lineage_api_subagent_no_messages"] + + assert row["title"] == "Subagent Session" + assert "display_title" not in row + finally: + conn.close() + + def test_state_db_display_title_does_not_override_custom_json_title(_isolate): """Manual/custom JSON titles stay authoritative even when state.db differs.""" conn = _ensure_state_db(_isolate)