fix(sessions): bound the merged assigned CLI set by a shared ceiling
The route cap bounded project-assigned CLI rows PER PROJECT at 200, which is not a bound on the payload: 200 rows times N projects grows with the project count. The review's own reproduction still held — 1,000 assigned conversations spread over 5 projects returned all 1,000, because no project exceeded its own 200 (#6659 review finding 1). Imported WebUI sidecars from all_sessions() never pass through a state.db cap, so this merged pass is the only place that can bound them. Give each project an EQUAL share of a global CLI_PROJECT_ASSIGNED_SCAN_CEILING (ceiling // projects, never below 1) once enough projects exist to make that share tighter than the per-project cap. Same shape as the model-side share of PROJECT_ASSIGNED_CLI_SCAN_CEILING, and for the same reason a flat global cap is unusable here: it would leave every project past it permanently unreachable on each rebuild (greptile P1). Profiles with a few projects keep the full 200, so the common case is unchanged. The ceiling is duplicated as a literal because api.models is imported below this cap in api/routes.py; a test pins the two equal, mirroring test_background_source_exclusion_literal_matches_the_constant.
This commit is contained in:
1 parent
b5ccf56c53
commit
76be369c54
2 files changed
+115
-1
No files matched your search
+34
-1
@@ -10494,12 +10494,23 @@ def _cli_visible_session_cap() -> int:
|
||||
# which no model-side cap applies to — so it is the only place that can actually
|
||||
# bound the assigned set (#6659 review finding 1).
|
||||
CLI_PROJECT_ASSIGNED_CAP = 200
|
||||
# Global ceiling on the assigned rows this cap will emit, mirroring
|
||||
# models.PROJECT_ASSIGNED_CLI_SCAN_CEILING so the route can never hand back more
|
||||
# assigned conversations than the model pass was allowed to recover. A flat
|
||||
# per-project bound does NOT bound the payload: 200 rows x N projects grows with
|
||||
# the project count, and 1,000 assigned rows spread over 5 projects still
|
||||
# returned all 1,000 — the exact reproduction from #6659 review finding 1.
|
||||
# Spelled as a literal because api.models is imported further down this module;
|
||||
# pinned equal to the model constant by
|
||||
# test_route_project_ceiling_matches_the_model_constant.
|
||||
CLI_PROJECT_ASSIGNED_SCAN_CEILING = 2000
|
||||
|
||||
|
||||
def _cap_recent_cli_sessions(
|
||||
sessions: list[dict],
|
||||
cli_cap: int | None = None,
|
||||
project_cap: int = CLI_PROJECT_ASSIGNED_CAP,
|
||||
project_ceiling: int = CLI_PROJECT_ASSIGNED_SCAN_CEILING,
|
||||
) -> list[dict]:
|
||||
"""Cap the default CLI list while retaining project-addressable rows.
|
||||
|
||||
@@ -10519,11 +10530,33 @@ def _cap_recent_cli_sessions(
|
||||
window is full. Past that bound they are dropped: keeping assigned rows past
|
||||
the *recent* cap is the fix, keeping them past *all* bounds just trades a
|
||||
vanishing session for a stalled sidebar.
|
||||
|
||||
That per-project budget is an EQUAL share of ``project_ceiling``
|
||||
(``ceiling // projects``, never below 1) once enough projects exist to exceed
|
||||
it, which is what makes the merged payload bounded no matter how many
|
||||
projects a profile has while keeping a project's allowance independent of how
|
||||
loud its neighbours are. Same shape as the model-side share of
|
||||
``PROJECT_ASSIGNED_CLI_SCAN_CEILING``, and for the same reason a flat global
|
||||
cap is not usable here: it would leave every project past it permanently
|
||||
unreachable on every rebuild (greptile P1 on #6659).
|
||||
"""
|
||||
if cli_cap is None:
|
||||
cli_cap = _cli_visible_session_cap()
|
||||
if cli_cap <= 0:
|
||||
return sessions
|
||||
# The share denominator has to be known before the first assigned row is
|
||||
# charged against it, so count the distinct assigned projects up front.
|
||||
assigned_projects = {
|
||||
project_id
|
||||
for session in sessions
|
||||
if _is_cli_session_for_settings(session)
|
||||
and (project_id := str(session.get("project_id") or "").strip())
|
||||
}
|
||||
effective_project_cap = project_cap
|
||||
if project_cap > 0 and project_ceiling > 0 and assigned_projects:
|
||||
effective_project_cap = min(
|
||||
project_cap, max(1, project_ceiling // len(assigned_projects))
|
||||
)
|
||||
kept = []
|
||||
recent_seen = 0
|
||||
unassigned_seen = 0
|
||||
@@ -10539,7 +10572,7 @@ def _cap_recent_cli_sessions(
|
||||
else:
|
||||
seen = assigned_seen.get(project_id, 0) + 1
|
||||
assigned_seen[project_id] = seen
|
||||
if project_cap > 0 and seen > project_cap:
|
||||
if effective_project_cap > 0 and seen > effective_project_cap:
|
||||
continue
|
||||
if recent_seen >= cli_cap:
|
||||
session = dict(session)
|
||||
|
||||
@@ -320,6 +320,87 @@ def test_route_cap_bounds_each_project_independently(fake_hermes_home):
|
||||
assert len(kept) > routes.CLI_PROJECT_ASSIGNED_CAP
|
||||
|
||||
|
||||
def test_route_cap_bounds_the_whole_merged_assigned_set(fake_hermes_home):
|
||||
"""A per-project bound alone is not a bound on the payload (finding 1).
|
||||
|
||||
``CLI_PROJECT_ASSIGNED_CAP`` rows times N projects grows with the project
|
||||
count: 1,000 assigned conversations spread over 5 projects reproduced the
|
||||
review's original "1,000 rows in, 1,000 rows out" exactly, because every
|
||||
project stayed under its own 200. The equal share of
|
||||
``CLI_PROJECT_ASSIGNED_SCAN_CEILING`` is what actually bounds the merged set.
|
||||
"""
|
||||
project_count = 40
|
||||
per_project = 200
|
||||
rows = []
|
||||
for project in range(project_count):
|
||||
rows.extend(
|
||||
{
|
||||
"session_id": f"project-{project:02d}-{index:04d}",
|
||||
"is_cli_session": True,
|
||||
"project_id": f"project-{project:02d}",
|
||||
}
|
||||
for index in range(per_project)
|
||||
)
|
||||
assert len(rows) > routes.CLI_PROJECT_ASSIGNED_SCAN_CEILING
|
||||
|
||||
kept = routes._cap_recent_cli_sessions(rows)
|
||||
|
||||
assert len(kept) <= routes.CLI_PROJECT_ASSIGNED_SCAN_CEILING
|
||||
# Bounded by an EQUAL share, not by dropping whole projects: every project
|
||||
# keeps a slice, or this would be the starvation greptile P1 rejected.
|
||||
counts: dict[str, int] = {}
|
||||
for row in kept:
|
||||
counts[row["project_id"]] = counts.get(row["project_id"], 0) + 1
|
||||
share = routes.CLI_PROJECT_ASSIGNED_SCAN_CEILING // project_count
|
||||
assert len(counts) == project_count
|
||||
assert set(counts.values()) == {share}
|
||||
# The share only bites once it is tighter than the per-project cap.
|
||||
assert share < routes.CLI_PROJECT_ASSIGNED_CAP
|
||||
|
||||
|
||||
def test_route_cap_share_does_not_shrink_below_the_per_project_cap(fake_hermes_home):
|
||||
"""Few projects must keep the full 200 — the ceiling is a ceiling, not a quota.
|
||||
|
||||
Negative control for the test above: with the ceiling comfortably wider than
|
||||
``project_count * CLI_PROJECT_ASSIGNED_CAP`` the share is inert, so the
|
||||
bounding above is attributable to the ceiling and not to a blanket shrink.
|
||||
"""
|
||||
rows = []
|
||||
for project in ("project-a", "project-b"):
|
||||
rows.extend(
|
||||
{
|
||||
"session_id": f"{project}-{index:04d}",
|
||||
"is_cli_session": True,
|
||||
"project_id": project,
|
||||
}
|
||||
for index in range(250)
|
||||
)
|
||||
|
||||
kept = routes._cap_recent_cli_sessions(rows)
|
||||
|
||||
counts: dict[str, int] = {}
|
||||
for row in kept:
|
||||
counts[row["project_id"]] = counts.get(row["project_id"], 0) + 1
|
||||
assert counts == {
|
||||
"project-a": routes.CLI_PROJECT_ASSIGNED_CAP,
|
||||
"project-b": routes.CLI_PROJECT_ASSIGNED_CAP,
|
||||
}
|
||||
|
||||
|
||||
def test_route_project_ceiling_matches_the_model_constant():
|
||||
"""The route literal must not drift from the model-side scan ceiling.
|
||||
|
||||
Same reasoning as ``test_background_source_exclusion_literal_matches_the_constant``:
|
||||
the value is duplicated because ``api.models`` is imported below this cap in
|
||||
``api/routes.py``, so only a test can keep the two honest. If they diverge the
|
||||
route could hand back more assigned rows than the model pass ever recovers.
|
||||
"""
|
||||
assert (
|
||||
routes.CLI_PROJECT_ASSIGNED_SCAN_CEILING
|
||||
== models.PROJECT_ASSIGNED_CLI_SCAN_CEILING
|
||||
)
|
||||
|
||||
|
||||
def test_more_than_two_hundred_assigned_conversations_survive_across_projects(
|
||||
fake_hermes_home, tmp_path
|
||||
):
|
||||
|
||||
Reference in new issue
Block a user