fix: cap CLI session sidebar state scans
This commit is contained in:
1 parent
6c60925a54
commit
6e64068f0f
3 files changed
+127
-11
No files matched your search
@@ -3,6 +3,9 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Limit the CLI/agent session sidebar bridge to a recent candidate window before joining message rows, so `show_cli_sessions` no longer aggregates every historical `state.db` message before applying the visible sidebar cap. Closes #2628.
|
||||
|
||||
## [v0.51.96] — 2026-05-20 — Release BT (stage-389 — 8-PR batch — IPv6 dashboard link normalization + configured title-generation provider routing + sidebar pinned-session 3-cap + external-refresh sidecar count preference + Hermes overview docs relocation + legacy dedup timestamp granularity + custom provider /models endpoint error surfacing + RuntimeAdapter Slice 4c harness gate RFC)
|
||||
|
||||
|
||||
+43
-11
@@ -401,7 +401,7 @@ def read_importable_agent_session_rows(
|
||||
)
|
||||
|
||||
where_clauses = ["s.source IS NOT NULL"]
|
||||
params: list[str] = []
|
||||
params: list[object] = []
|
||||
if exclude_sources:
|
||||
excluded = tuple(str(source) for source in exclude_sources if source)
|
||||
if excluded:
|
||||
@@ -409,8 +409,7 @@ def read_importable_agent_session_rows(
|
||||
where_clauses.append(f"s.source NOT IN ({placeholders})")
|
||||
params.extend(excluded)
|
||||
|
||||
cur.execute(
|
||||
f"""
|
||||
select_sql = f"""
|
||||
SELECT s.id, s.title, s.model, s.message_count,
|
||||
s.started_at, s.source,
|
||||
{session_source_expr},
|
||||
@@ -428,14 +427,47 @@ def read_importable_agent_session_rows(
|
||||
COUNT(m.id) AS actual_message_count,
|
||||
{user_message_count_expr} AS actual_user_message_count,
|
||||
MAX(m.timestamp) AS last_activity
|
||||
FROM sessions s
|
||||
LEFT JOIN messages m ON m.session_id = s.id
|
||||
WHERE {' AND '.join(where_clauses)}
|
||||
GROUP BY s.id
|
||||
ORDER BY COALESCE(MAX(m.timestamp), s.started_at) DESC
|
||||
""",
|
||||
params,
|
||||
)
|
||||
"""
|
||||
if limit is not None:
|
||||
result_limit = max(0, int(limit))
|
||||
if result_limit == 0:
|
||||
return []
|
||||
# The sidebar only needs a small visible window. Bound the expensive
|
||||
# messages join to a recent-session candidate set instead of
|
||||
# aggregating every historical Hermes state.db session before
|
||||
# slicing in Python. Oversampling preserves room for hidden
|
||||
# compression segments or other rows filtered after projection.
|
||||
candidate_limit = max(result_limit * 8, result_limit)
|
||||
cur.execute(
|
||||
f"""
|
||||
WITH candidates AS (
|
||||
SELECT s.id
|
||||
FROM sessions s
|
||||
WHERE {' AND '.join(where_clauses)}
|
||||
ORDER BY s.started_at DESC
|
||||
LIMIT ?
|
||||
)
|
||||
{select_sql}
|
||||
FROM sessions s
|
||||
JOIN candidates c ON c.id = s.id
|
||||
LEFT JOIN messages m ON m.session_id = s.id
|
||||
GROUP BY s.id
|
||||
ORDER BY COALESCE(MAX(m.timestamp), s.started_at) DESC
|
||||
""",
|
||||
[*params, candidate_limit],
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
f"""
|
||||
{select_sql}
|
||||
FROM sessions s
|
||||
LEFT JOIN messages m ON m.session_id = s.id
|
||||
WHERE {' AND '.join(where_clauses)}
|
||||
GROUP BY s.id
|
||||
ORDER BY COALESCE(MAX(m.timestamp), s.started_at) DESC
|
||||
""",
|
||||
params,
|
||||
)
|
||||
projected = _project_agent_session_rows([dict(row) for row in cur.fetchall()])
|
||||
projected = [_with_normalized_source(row) for row in projected]
|
||||
projected = [row for row in projected if is_cli_session_row_visible(row)]
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Regression coverage for capped CLI/agent session sidebar scans (#2628)."""
|
||||
|
||||
import pathlib
|
||||
import sqlite3
|
||||
import time
|
||||
|
||||
import api.agent_sessions as agent_sessions
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _make_state_db(path, *, sessions=80, messages_per_session=3):
|
||||
conn = sqlite3.connect(str(path))
|
||||
conn.executescript(
|
||||
"""
|
||||
CREATE TABLE sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
source TEXT,
|
||||
session_source TEXT,
|
||||
title TEXT,
|
||||
model TEXT,
|
||||
started_at REAL NOT NULL,
|
||||
message_count INTEGER DEFAULT 0,
|
||||
parent_session_id TEXT,
|
||||
ended_at REAL,
|
||||
end_reason TEXT
|
||||
);
|
||||
CREATE INDEX idx_sessions_started ON sessions(started_at);
|
||||
CREATE TABLE messages (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT,
|
||||
role TEXT,
|
||||
content TEXT,
|
||||
timestamp REAL
|
||||
);
|
||||
CREATE INDEX idx_messages_session ON messages(session_id, timestamp);
|
||||
"""
|
||||
)
|
||||
base = time.time() - sessions
|
||||
for i in range(sessions):
|
||||
sid = f"cli_perf_{i:04d}"
|
||||
started = base + i
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO sessions
|
||||
(id, source, session_source, title, model, started_at, message_count, parent_session_id, ended_at, end_reason)
|
||||
VALUES (?, 'cli', 'cli', ?, 'openai/gpt-5', ?, ?, NULL, NULL, NULL)
|
||||
""",
|
||||
(sid, sid, started, messages_per_session),
|
||||
)
|
||||
for j in range(messages_per_session):
|
||||
conn.execute(
|
||||
"INSERT INTO messages (id, session_id, role, content, timestamp) VALUES (?, ?, ?, 'hello', ?)",
|
||||
(f"msg_{i:04d}_{j:02d}", sid, "user" if j == 0 else "assistant", started + j / 10),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_importable_agent_rows_push_sidebar_limit_into_sql(tmp_path):
|
||||
"""A capped sidebar scan should not aggregate the entire state.db first."""
|
||||
db = tmp_path / "state.db"
|
||||
_make_state_db(db, sessions=120, messages_per_session=5)
|
||||
|
||||
rows = agent_sessions.read_importable_agent_session_rows(db, limit=20, exclude_sources=("webui",))
|
||||
|
||||
assert len(rows) == 20
|
||||
assert [row["id"] for row in rows][:3] == ["cli_perf_0119", "cli_perf_0118", "cli_perf_0117"]
|
||||
assert {row["actual_message_count"] for row in rows} == {5}
|
||||
|
||||
src = (REPO_ROOT / "api" / "agent_sessions.py").read_text()
|
||||
assert "WITH candidates AS" in src
|
||||
assert "JOIN candidates c ON c.id = s.id" in src
|
||||
assert "candidate_limit = max(result_limit * 8, result_limit)" in src
|
||||
|
||||
|
||||
def test_importable_agent_rows_zero_limit_skips_query_work(tmp_path):
|
||||
db = tmp_path / "state.db"
|
||||
_make_state_db(db, sessions=5, messages_per_session=1)
|
||||
|
||||
assert agent_sessions.read_importable_agent_session_rows(db, limit=0, exclude_sources=("webui",)) == []
|
||||
Reference in new issue
Block a user