From bc1ee9d4002b3112a9a71ff717dd452fafe6b29a Mon Sep 17 00:00:00 2001 From: Rodrigo Gomes da Silva Date: Fri, 14 Aug 2026 20:43:39 -0300 Subject: [PATCH] fix(sessions): run the assigned recovery pass in the all-profiles view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `?all_profiles=1` passed `project_assigned_limit=False`, on the premise that its `visible_session_limit=None` first pass already returns every conversation. It does not: `_load_cli_sessions_uncached` maps `visible_session_limit is None` to CLI_VISIBLE_SESSION_LIMIT for the interactive pass, so that view truncates by recency exactly like the single-profile one. Only cron/webhook/kanban limits reach the reader as `limit=` directly, where None does mean unbounded. Net effect of the old value: in the "All profiles" sidebar a project-assigned CLI session older than the newest 20 interactive conversations was missing from /api/sessions while its project chip still claimed it — the #6659 bug itself, in the one view that shows every profile at once. Recovery pass 2 (the unassigned refill) still ran there, so the two passes disagreed as well. Pass PROJECT_ASSIGNED_CLI_LIMIT instead of None so the per-project bound and the PROJECT_ASSIGNED_CLI_SCAN_CEILING guard apply per profile, rather than issuing an unbounded assigned scan for every profile on the box. --- api/models.py | 13 ++++-- ...test_project_assigned_cli_session_limit.py | 43 +++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/api/models.py b/api/models.py index 56265252..2cc0d857 100644 --- a/api/models.py +++ b/api/models.py @@ -8905,12 +8905,17 @@ def get_cli_sessions( merged: list[dict] = [] for idx, (ctx_home, ctx_db_path, ctx_profile) in enumerate(contexts): load_kwargs = { + # NOTE: visible_session_limit=None is NOT "unbounded" for the + # interactive pass — it resolves to CLI_VISIBLE_SESSION_LIMIT + # above, so this view truncates assigned conversations by + # recency exactly like the single-profile one and needs the + # same recovery passes. project_assigned_limit therefore keeps + # its default per-project bound here. Only the three limits + # below are handed straight to the reader as ``limit=``, where + # None really does mean unbounded. 'source_filter': source_filter, 'visible_session_limit': None, - # An unbounded first pass already returns every assigned and - # unassigned conversation, so the bounded recovery passes - # have nothing to recover — skip their queries. - 'project_assigned_limit': False, + 'project_assigned_limit': PROJECT_ASSIGNED_CLI_LIMIT, 'cron_project_limit': None, 'webhook_project_limit': None, 'kanban_project_limit': None, diff --git a/tests/test_project_assigned_cli_session_limit.py b/tests/test_project_assigned_cli_session_limit.py index ca8829f6..d4d8ff31 100755 --- a/tests/test_project_assigned_cli_session_limit.py +++ b/tests/test_project_assigned_cli_session_limit.py @@ -738,3 +738,46 @@ def test_kanban_second_pass_still_projects_rows(fake_hermes_home, tmp_path): assert "kanban-old" in by_id # Upstream semantics: kanban rows get no chip from this helper. assert by_id["kanban-old"]["project_id"] is None + + +# ── The all-profiles sidebar view runs the same recovery passes ─────────────── + + +def test_all_profiles_view_recovers_project_assigned_sessions( + fake_hermes_home, tmp_path, monkeypatch +): + """``?all_profiles=1`` truncates by recency too, so it needs both passes. + + That path passes ``visible_session_limit=None``, which reads like + "unbounded" but is mapped to ``CLI_VISIBLE_SESSION_LIMIT`` for the + interactive pass — only the cron/webhook/kanban limits reach the reader as + ``limit=`` directly, where None really does mean unbounded. Skipping the + assigned recovery pass here therefore lost exactly the sessions this + regression is about, in the one view that shows every profile at once. + """ + monkeypatch.setattr(models, "CLI_VISIBLE_SESSION_LIMIT", 5) + _register_projects(tmp_path, "project-123") + + db_path = fake_hermes_home / "state.db" + rows = [ + _session(f"recent-{index:02d}", BASE_TS + 100 + index) + for index in range(25) + ] + rows.append(_session("older-assigned", BASE_TS, project_id="project-123")) + _write_state_db(db_path, rows) + + # Enumerating real profiles shells out to the agent CLI; pin the one context. + monkeypatch.setattr( + models, + "_all_profiles_cli_contexts", + lambda: ([(fake_hermes_home, db_path, "default")], (("home", "default", 1),)), + ) + + sessions = models.get_cli_sessions(all_profiles=True) + by_id = {session["session_id"]: session for session in sessions} + + assert "older-assigned" in by_id + assert by_id["older-assigned"]["project_id"] == "project-123" + assert [s["session_id"] for s in sessions].count("older-assigned") == 1 + # The unassigned window is still bounded — the recovery pass is additive. + assert sum(1 for s in sessions if s["project_id"] is None) == 5