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.