Files
hermes-webui/api
Rodrigo Gomes da Silva ac6b5d0b37 fix(sessions): bound project-assigned CLI sessions per project and budget the unassigned window separately
Letting assigned sessions past the *recent* cap was right; letting them past
*all* bounds was not. Three linked defects from review of #6659:

1. The assigned list was effectively unbounded. models.py's 200 cap only applied
   to state.db's own pass; imported CLI sidecars from all_sessions() never pass
   through it and _cap_recent_cli_sessions() retained every assigned row (1,000
   assigned rows -> 1,000 returned). The FINAL MERGED payload is the only place
   that sees every source of assigned rows, so bound it there - per project, so
   one busy project cannot evict another's history (the >200-assigned case
   greptile flagged), and counting logical conversations, not raw rows.

2. Assigned rows spent the unassigned quota: 3 assigned + 20 unassigned yielded
   17 unassigned, and the model fetched 20 rows total up front so bumping only
   the route counter could not refill. Count the two separately in the route cap
   and add a bounded unassigned refill pass, keyed on the lineage, so 20 unique
   unassigned logical conversations survive sidecar/lineage dedup. The refill
   only runs when the window was actually saturated AND came up short.

3. A deleted or cross-profile project_id hid the session outright: it was
   surfaced unchecked, so the row left "Unassigned" and its overflow became
   default_hidden with no chip left to reveal it. Resolve non-system ids against
   profile-scoped load_projects() and coerce a miss to None BEFORE either cap -
   the mirror of the bug this PR fixes.

read_importable_agent_session_rows() grows a `project_assignment` filter
('assigned'/'unassigned') replacing the require_project_id boolean. Both arms
key on the whole compression lineage and are exact complements, so neither
budget can double-count a lineage.

_state_row_project_id() keeps its (sid, source) signature: master's kanban pass
calls it that way, and giving it a row-dict signature turned that whole pass
into a swallowed TypeError that dropped every kanban row.
2026-09-24 11:03:46 -03:00
..
2026-09-24 03:15:02 +00:00