diff --git a/CHANGELOG.md b/CHANGELOG.md index 346995be..59adee7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## [Unreleased] +### Fixed + +- Agent-side cron sessions imported from state.db now remain available to their assigned project chip as `default_hidden` rows instead of being filtered out before project reveal logic can see them (#3134). + ## [v0.51.165] — 2026-05-30 — Release EK (stage-batch47 — stop EventSource reconnect storm on long-lived SSE streams) ### Fixed diff --git a/api/routes.py b/api/routes.py index 70f96eb6..cc8bd761 100644 --- a/api/routes.py +++ b/api/routes.py @@ -2530,6 +2530,29 @@ def _is_duplicate_webui_state_projection(session: dict, represented_webui_ids: s return bool(_session_lineage_ids(session) & represented_webui_ids) +def _dedupe_cli_sidebar_sessions_for_api(cli: list[dict], represented_webui_ids: set[str]) -> list[dict]: + """Return CLI/state sidebar rows while preserving project-hidden cron rows. + + Agent-side cron sessions come from state.db rather than the WebUI session + store. They should stay hidden from the default sidebar, but project-assigned + messageful rows must remain in the `/api/sessions` payload with + `default_hidden` so the matching project chip can reveal them (#3134). + """ + from api.models import ( + _hide_from_default_sidebar as _cron_hide, + _include_project_hidden_background_sidebar_sessions, + ) + + candidates = [ + s for s in cli + if s["session_id"] not in represented_webui_ids + and not _is_duplicate_webui_state_projection(s, represented_webui_ids) + and is_cli_session_row_visible(s) + ] + visible = [s for s in candidates if not _cron_hide(s)] + return _include_project_hidden_background_sidebar_sessions(candidates, visible) + + CLI_VISIBLE_SESSION_CAP = 20 @@ -4625,14 +4648,7 @@ def handle_get(handler, parsed) -> bool: represented_webui_ids = set() for s in webui_sessions: represented_webui_ids.update(_session_lineage_ids(s)) - from api.models import _hide_from_default_sidebar as _cron_hide - deduped_cli = [ - s for s in cli - if s["session_id"] not in represented_webui_ids - and not _is_duplicate_webui_state_projection(s, represented_webui_ids) - and is_cli_session_row_visible(s) - and not _cron_hide(s) - ] + deduped_cli = _dedupe_cli_sidebar_sessions_for_api(cli, represented_webui_ids) else: diag.stage("filter_webui_sessions") webui_sessions = [s for s in webui_sessions if not _is_cli_session_for_settings(s)] diff --git a/tests/test_gateway_sync.py b/tests/test_gateway_sync.py index 7181972d..5cd3fb92 100644 --- a/tests/test_gateway_sync.py +++ b/tests/test_gateway_sync.py @@ -1529,7 +1529,11 @@ def test_cron_sessions_hidden_from_sidebar_by_default(): sessions = data.get('sessions', []) ids = {s['session_id'] for s in sessions} assert 'gw_noncron_001' in ids, "Non-cron agent session should still appear" - assert 'cron_job123_20260427' not in ids, "Cron session should be hidden by default" + cron_row = next((s for s in sessions if s['session_id'] == 'cron_job123_20260427'), None) + assert cron_row is None or cron_row.get('default_hidden') is True, ( + "Cron session should either be omitted from /api/sessions or marked " + "default_hidden so the default sidebar filter keeps it hidden" + ) finally: try: _remove_test_sessions(conn, 'cron_job123_20260427', 'gw_noncron_001') diff --git a/tests/test_issue3019_cron_project_sessions.py b/tests/test_issue3019_cron_project_sessions.py index bd27157f..dd112b4b 100644 --- a/tests/test_issue3019_cron_project_sessions.py +++ b/tests/test_issue3019_cron_project_sessions.py @@ -38,6 +38,56 @@ def test_project_assigned_cron_rows_are_returned_but_default_hidden(): assert by_id["cron_visible"]["default_hidden"] is True +def test_agent_side_cron_rows_keep_project_chip_visibility(): + from api.routes import _dedupe_cli_sidebar_sessions_for_api + + represented_webui_ids = {"webui-1"} + cli_rows = [ + { + "session_id": "cli-normal", + "source_tag": "cli", + "title": "Manual deployment notes", + "message_count": 1, + "updated_at": 5, + }, + { + "session_id": "cron-agent-project", + "source_tag": "cron", + "title": "Cron agent run", + "message_count": 3, + "project_id": "cron-project", + "updated_at": 4, + }, + { + "session_id": "cron-agent-unassigned", + "source_tag": "cron", + "title": "Cron hidden", + "message_count": 3, + "updated_at": 3, + }, + { + "session_id": "cron-agent-empty", + "source_tag": "cron", + "title": "Cron empty", + "message_count": 0, + "project_id": "cron-project", + "updated_at": 2, + }, + { + "session_id": "webui-1", + "source_tag": "cli", + "title": "Duplicate imported copy", + "message_count": 1, + }, + ] + + rows = _dedupe_cli_sidebar_sessions_for_api(cli_rows, represented_webui_ids) + + by_id = {row["session_id"]: row for row in rows} + assert set(by_id) == {"cli-normal", "cron-agent-project"} + assert by_id["cron-agent-project"]["default_hidden"] is True + + def test_session_list_project_filter_can_reveal_default_hidden_cron_rows(): src = ( __import__("pathlib").Path(__file__).parent.parent / "static" / "sessions.js").read_text(encoding="utf-8")