From 9650b387fd7df70e88d1e6c64c2e8710dccdd4dd Mon Sep 17 00:00:00 2001 From: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com> Date: Wed, 27 May 2026 22:36:32 +0200 Subject: [PATCH] fix: keep webui mirrored sessions out of cli filter --- api/agent_sessions.py | 19 +++- api/models.py | 11 +++ api/routes.py | 17 +++- ...est_issue2351_cli_session_source_filter.py | 91 +++++++++++++++++++ tests/test_session_lineage_metadata_api.py | 35 +++++++ 5 files changed, 168 insertions(+), 5 deletions(-) diff --git a/api/agent_sessions.py b/api/agent_sessions.py index 1b235f7c..61e6e2ed 100644 --- a/api/agent_sessions.py +++ b/api/agent_sessions.py @@ -142,14 +142,16 @@ def is_cli_session_row(row: dict) -> bool: if not isinstance(row, dict): return False source = _safe_lower(row.get("session_source")) - if source == "messaging": - return False - if source == "cli": - return True source_tag = _safe_lower(row.get("source_tag")) raw_source = _safe_lower(row.get("raw_source")) source_name = _safe_lower(row.get("source")) source_label = _safe_lower(row.get("source_label")) + if "webui" in {source, source_tag, raw_source, source_name, source_label}: + return False + if source == "messaging": + return False + if source == "cli": + return True if source_tag == "cli" or raw_source == "cli" or source_name == "cli" or source_label == "cli": return True @@ -727,6 +729,15 @@ def read_session_lineage_metadata(db_path: Path, session_ids: list[str] | set[st state_title = str(row.get('title') or '').strip() if state_title: metadata.setdefault(sid, {})['_state_db_title'] = state_title + state_source = str(row.get('source') or '').strip().lower() + if state_source: + entry = metadata.setdefault(sid, {}) + entry['_state_db_source'] = state_source + source_meta = normalize_agent_session_source(state_source) + entry['_state_db_source_tag'] = state_source + entry['_state_db_raw_source'] = source_meta.get('raw_source') + entry['_state_db_session_source'] = source_meta.get('session_source') + entry['_state_db_source_label'] = source_meta.get('source_label') parent_id = row.get('parent_session_id') parent_row = rows.get(parent_id) if parent_id else None diff --git a/api/models.py b/api/models.py index e060f94e..a92c2840 100644 --- a/api/models.py +++ b/api/models.py @@ -2295,7 +2295,18 @@ def _enrich_sidebar_lineage_metadata(sessions: list[dict]) -> None: if sid in metadata: entry = dict(metadata[sid]) state_db_title = entry.pop('_state_db_title', None) + state_db_source = entry.pop('_state_db_source', None) + state_db_source_tag = entry.pop('_state_db_source_tag', None) + state_db_raw_source = entry.pop('_state_db_raw_source', None) + state_db_session_source = entry.pop('_state_db_session_source', None) + state_db_source_label = entry.pop('_state_db_source_label', None) session.update(entry) + if state_db_source == 'webui': + session['source_tag'] = state_db_source_tag + session['raw_source'] = state_db_raw_source + session['session_source'] = state_db_session_source + session['source_label'] = state_db_source_label + session['is_cli_session'] = False title = session.get('title') if ( state_db_title diff --git a/api/routes.py b/api/routes.py index c80d74aa..9510eb15 100644 --- a/api/routes.py +++ b/api/routes.py @@ -2385,6 +2385,15 @@ def _is_cli_session_for_settings(session: dict) -> bool: ) +def _normalize_sidebar_source_flags(session: dict) -> dict: + """Return a sidebar row with the frontend CLI flag matching source metadata.""" + if not isinstance(session, dict): + return session + normalized = dict(session) + normalized["is_cli_session"] = is_cli_session_row(normalized) + return normalized + + CLI_VISIBLE_SESSION_CAP = 20 @@ -2414,7 +2423,11 @@ def _merge_cli_sidebar_metadata(ui_session: dict, cli_meta: dict) -> dict: if not cli_meta: return dict(ui_session) merged = dict(ui_session) - merged["is_cli_session"] = True + # Only preserve the CLI flag when the imported metadata is actually a CLI + # row. WebUI sessions are also mirrored into state.db; treating every + # matching state row as CLI hides long WebUI continuations from the default + # sidebar source tab. + merged["is_cli_session"] = is_cli_session_row(cli_meta) for key in ( "source_tag", "raw_source", @@ -4415,6 +4428,7 @@ def handle_get(handler, parsed) -> bool: diag.stage("load_settings") settings = load_settings() show_cli_sessions = bool(settings.get("show_cli_sessions")) + webui_sessions = [_normalize_sidebar_source_flags(s) for s in webui_sessions] if show_cli_sessions: diag.stage("get_cli_sessions") cli = get_cli_sessions() @@ -4432,6 +4446,7 @@ def handle_get(handler, parsed) -> bool: for key in ("source_tag", "raw_source", "session_source", "source_label"): if not s.get(key) and meta.get(key): s[key] = meta[key] + webui_sessions = [_normalize_sidebar_source_flags(s) for s in webui_sessions] # Apply the same CLI visibility semantics to imported local copies so # low-value imported artifacts do not leak into the sidebar. webui_sessions = [s for s in webui_sessions if is_cli_session_row_visible(s)] diff --git a/tests/test_issue2351_cli_session_source_filter.py b/tests/test_issue2351_cli_session_source_filter.py index efe2a8f6..2e88bf38 100644 --- a/tests/test_issue2351_cli_session_source_filter.py +++ b/tests/test_issue2351_cli_session_source_filter.py @@ -29,3 +29,94 @@ def test_session_source_tabs_have_dedicated_sidebar_styles(): assert ".session-source-tabs" in css assert ".session-source-tab.active" in css assert ".session-empty-note" in css + + +def test_webui_state_db_mirror_does_not_become_cli_sidebar_row(): + from api.routes import _merge_cli_sidebar_metadata + + merged = _merge_cli_sidebar_metadata( + {"session_id": "webui-tip", "title": "Long WebUI session", "source_tag": "webui"}, + { + "session_id": "webui-tip", + "source_tag": "webui", + "session_source": "webui", + "message_count": 1740, + }, + ) + + assert merged["is_cli_session"] is False + assert merged["source_tag"] == "webui" + assert merged["session_source"] == "webui" + assert merged["message_count"] == 1740 + + +def test_real_cli_state_db_mirror_stays_cli_sidebar_row(): + from api.routes import _merge_cli_sidebar_metadata + + merged = _merge_cli_sidebar_metadata( + {"session_id": "cli-tip", "title": "CLI session", "source_tag": "cli"}, + { + "session_id": "cli-tip", + "source_tag": "cli", + "session_source": "cli", + "message_count": 12, + }, + ) + + assert merged["is_cli_session"] is True + assert merged["session_source"] == "cli" + + +def test_stale_webui_sidebar_cli_flag_is_cleared_before_frontend_response(): + from api.routes import _normalize_sidebar_source_flags + + normalized = _normalize_sidebar_source_flags( + { + "session_id": "webui-tip", + "title": "Long WebUI session", + "source_tag": "webui", + "session_source": "webui", + "is_cli_session": True, + "message_count": 1740, + } + ) + + assert normalized["is_cli_session"] is False + assert normalized["source_tag"] == "webui" + assert normalized["session_source"] == "webui" + + + +def test_webui_source_overrides_stale_cli_flag_even_with_default_title(): + from api.agent_sessions import is_cli_session_row + from api.routes import _normalize_sidebar_source_flags + + stale_webui = { + "session_id": "webui-default-title", + "title": "CLI Session", + "source_tag": "webui", + "session_source": "webui", + "source_label": "WebUI", + "is_cli_session": True, + "message_count": 23, + } + + assert is_cli_session_row(stale_webui) is False + assert _normalize_sidebar_source_flags(stale_webui)["is_cli_session"] is False + + +def test_real_cli_sidebar_cli_flag_is_preserved_before_frontend_response(): + from api.routes import _normalize_sidebar_source_flags + + normalized = _normalize_sidebar_source_flags( + { + "session_id": "cli-tip", + "title": "CLI session", + "source_tag": "cli", + "session_source": "cli", + "is_cli_session": True, + "message_count": 12, + } + ) + + assert normalized["is_cli_session"] is True diff --git a/tests/test_session_lineage_metadata_api.py b/tests/test_session_lineage_metadata_api.py index 2cb835b0..38d0ef1e 100644 --- a/tests/test_session_lineage_metadata_api.py +++ b/tests/test_session_lineage_metadata_api.py @@ -271,6 +271,41 @@ def test_cross_surface_child_session_metadata_marks_orphan_top_level_candidate(_ conn.close() +def test_state_db_webui_source_overrides_stale_cli_json_metadata(_isolate): + """State-db WebUI mirrors should clear stale CLI source fields in sidebar rows.""" + conn = _ensure_state_db(_isolate) + t0 = time.time() - 100 + try: + session = Session( + session_id="lineage_api_stale_cli_source", + title="WebUI Chatnachrichten verschwinden nach Neustart #9", + messages=[{"role": "user", "content": "hello"}, {"role": "assistant", "content": "hi"}], + updated_at=t0, + is_cli_session=True, + source_tag="cli", + raw_source="cli", + session_source="cli", + source_label="CLI", + ) + session.save(touch_updated_at=False) + _insert_state_row( + conn, + "lineage_api_stale_cli_source", + source="webui", + started_at=t0, + ) + + row = {row["session_id"]: row for row in all_sessions()}["lineage_api_stale_cli_source"] + + assert row["source_tag"] == "webui" + assert row["raw_source"] == "webui" + assert row["session_source"] == "webui" + assert row["source_label"] == "WebUI" + assert row["is_cli_session"] is False + finally: + conn.close() + + def test_generic_webui_title_gets_read_only_state_db_display_title(_isolate): """Sidebar rows can display the fresher state.db title without mutating JSON.""" conn = _ensure_state_db(_isolate)