perf(state): add compact_rows to skip system_prompt blob in session list queries

list_sessions_rich and _get_session_rich_row previously used SELECT s.*,
pulling the system_prompt TEXT blob on every row even for dashboard and
picker callers that never display it. On large databases this blob routinely
runs to tens of kilobytes per session, causing unnecessary B-tree I/O.

Add compact_rows=False param to both functions. When True, an explicit
column list omitting system_prompt is substituted for s.* in both the
simple and the recursive-CTE (order_by_last_active) query paths.
Default is False so all existing callers are unaffected.

Update dashboard and session-picker callers in web_server.py and
tui_gateway/server.py to pass compact_rows=True.

Add seven regression tests covering: omission of system_prompt, presence
of all metadata fields, both query paths, _get_session_rich_row, and
backward-compat default.
This commit is contained in:
CodeForgeNet 2026-06-17 01:54:42 +05:30
parent 72cc32b508
commit c470cbd304
4 changed files with 99 additions and 9 deletions

View file

@ -1620,7 +1620,7 @@ async def get_status():
from hermes_state import SessionDB
db = SessionDB()
try:
sessions = db.list_sessions_rich(limit=50)
sessions = db.list_sessions_rich(limit=50, compact_rows=True)
now = time.time()
active_sessions = sum(
1 for s in sessions
@ -2607,6 +2607,7 @@ async def get_sessions(
include_archived=include_archived,
archived_only=archived_only,
order_by_last_active=order == "recent",
compact_rows=True,
)
total = db.session_count(
source=source or None,
@ -2718,6 +2719,7 @@ async def get_profiles_sessions(
include_archived=include_archived,
archived_only=archived_only,
order_by_last_active=order == "recent",
compact_rows=True,
)
profile_total = db.session_count(
source=source_filter,

View file

@ -1977,6 +1977,21 @@ class SessionDB:
current = row["id"]
return current
# All sessions columns except the large system_prompt blob, each prefixed
# with the "s" table alias used in list_sessions_rich/_get_session_rich_row
# queries. Used when compact_rows=True to avoid reading the blob for
# dashboard and picker callers that only need lightweight metadata.
_SESSION_COMPACT_COLS = (
"s.id, s.source, s.user_id, s.model, s.model_config, "
"s.parent_session_id, s.started_at, s.ended_at, s.end_reason, "
"s.message_count, s.tool_call_count, s.input_tokens, s.output_tokens, "
"s.cache_read_tokens, s.cache_write_tokens, s.reasoning_tokens, "
"s.cwd, s.billing_provider, s.billing_base_url, s.billing_mode, "
"s.estimated_cost_usd, s.actual_cost_usd, s.cost_status, s.cost_source, "
"s.pricing_version, s.title, s.api_call_count, s.handoff_state, "
"s.handoff_platform, s.handoff_error, s.rewind_count, s.archived"
)
def list_sessions_rich(
self,
source: str = None,
@ -1990,6 +2005,7 @@ class SessionDB:
include_archived: bool = False,
archived_only: bool = False,
id_query: str = None,
compact_rows: bool = False,
) -> List[Dict[str, Any]]:
"""List sessions with preview (first user message) and last active timestamp.
@ -2017,6 +2033,12 @@ class SessionDB:
surfaces in the correct slot. Ordering is computed at SQL level via
a recursive CTE that walks compression-continuation edges, so LIMIT
and OFFSET still apply efficiently.
Pass ``compact_rows=True`` for dashboard and picker callers that only
need lightweight metadata. This omits the ``system_prompt`` blob from
the SELECT so SQLite never copies it out of the B-tree page a
significant I/O saving on large databases where the blob routinely
runs to tens of kilobytes per row.
"""
where_clauses = []
params = []
@ -2099,6 +2121,7 @@ class SessionDB:
outer_where = (
f"{where_sql} AND {id_clause}" if where_sql else f"WHERE {id_clause}"
)
_sel = self._SESSION_COMPACT_COLS if compact_rows else "s.*"
query = f"""
WITH RECURSIVE chain(root_id, cur_id) AS (
SELECT s.id, s.id FROM sessions s {where_sql}
@ -2120,7 +2143,7 @@ class SessionDB:
FROM chain
GROUP BY root_id
)
SELECT s.*,
SELECT {_sel},
COALESCE(
(SELECT SUBSTR(REPLACE(REPLACE(m.content, X'0A', ' '), X'0D', ' '), 1, 63)
FROM messages m
@ -2143,8 +2166,9 @@ class SessionDB:
# only applies to the outer select.
params = params + params + id_params + [limit, offset]
else:
_sel = self._SESSION_COMPACT_COLS if compact_rows else "s.*"
query = f"""
SELECT s.*,
SELECT {_sel},
COALESCE(
(SELECT SUBSTR(REPLACE(REPLACE(m.content, X'0A', ' '), X'0D', ' '), 1, 63)
FROM messages m
@ -2281,13 +2305,17 @@ class SessionDB:
runs.append(s)
return runs
def _get_session_rich_row(self, session_id: str) -> Optional[Dict[str, Any]]:
def _get_session_rich_row(self, session_id: str, compact_rows: bool = False) -> Optional[Dict[str, Any]]:
"""Fetch a single session with the same enriched columns as
``list_sessions_rich`` (preview + last_active). Returns None if the
session doesn't exist.
Pass ``compact_rows=True`` to omit the ``system_prompt`` blob (see
``list_sessions_rich`` for details).
"""
query = """
SELECT s.*,
_sel = self._SESSION_COMPACT_COLS if compact_rows else "s.*"
query = f"""
SELECT {_sel},
COALESCE(
(SELECT SUBSTR(REPLACE(REPLACE(m.content, X'0A', ' '), X'0D', ' '), 1, 63)
FROM messages m

View file

@ -4141,3 +4141,63 @@ class TestListCronJobRuns:
detail = " ".join(row[-1] for row in plan)
assert "USING INDEX" in detail or "USING COVERING INDEX" in detail, detail
assert "idx_sessions_source" in detail, detail
# =========================================================================
# compact_rows — lightweight column projection (issue #47414)
# =========================================================================
class TestCompactRows:
"""list_sessions_rich and _get_session_rich_row with compact_rows=True
must omit system_prompt but return all other metadata fields."""
def _create(self, db, sid, *, system_prompt="big blob " * 500):
db.create_session(session_id=sid, source="cli", model="m")
db.update_system_prompt(sid, system_prompt)
return sid
def test_compact_rows_omits_system_prompt(self, db):
self._create(db, "s1")
rows = db.list_sessions_rich(compact_rows=True)
assert len(rows) == 1
assert "system_prompt" not in rows[0]
def test_full_rows_include_system_prompt(self, db):
self._create(db, "s1", system_prompt="keep me")
rows = db.list_sessions_rich(compact_rows=False)
assert rows[0]["system_prompt"] == "keep me"
def test_compact_rows_preserves_metadata_fields(self, db):
self._create(db, "s1")
rows = db.list_sessions_rich(compact_rows=True)
row = rows[0]
for field in ("id", "source", "model", "started_at", "message_count",
"input_tokens", "output_tokens", "title", "cwd",
"archived", "preview", "last_active"):
assert field in row, f"missing field: {field}"
def test_compact_rows_order_by_last_active(self, db):
"""compact_rows=True also works with the CTE / order_by_last_active path."""
self._create(db, "s1")
self._create(db, "s2")
rows = db.list_sessions_rich(compact_rows=True, order_by_last_active=True)
assert len(rows) == 2
assert all("system_prompt" not in r for r in rows)
def test_get_session_rich_row_compact_omits_system_prompt(self, db):
self._create(db, "s1", system_prompt="should be gone")
row = db._get_session_rich_row("s1", compact_rows=True)
assert row is not None
assert "system_prompt" not in row
assert row["id"] == "s1"
def test_get_session_rich_row_full_includes_system_prompt(self, db):
self._create(db, "s1", system_prompt="stay")
row = db._get_session_rich_row("s1", compact_rows=False)
assert row["system_prompt"] == "stay"
def test_compact_rows_default_is_false(self, db):
"""Default behaviour (compact_rows not passed) is unchanged — full rows."""
self._create(db, "s1", system_prompt="present")
rows = db.list_sessions_rich()
assert "system_prompt" in rows[0]

View file

@ -3987,7 +3987,7 @@ def _(rid, params: dict) -> dict:
fetch_limit = max(limit * 2, 200)
rows = [
s
for s in db.list_sessions_rich(source=None, limit=fetch_limit)
for s in db.list_sessions_rich(source=None, limit=fetch_limit, compact_rows=True)
if (s.get("source") or "").strip().lower() not in deny
][:limit]
return _ok(
@ -4034,7 +4034,7 @@ def _(rid, params: dict) -> dict:
# users (lots of recent ``tool`` rows) don't get a false
# "no eligible session" answer. ``session.list`` uses a
# similar over-fetch strategy.
rows = db.list_sessions_rich(source=None, limit=200)
rows = db.list_sessions_rich(source=None, limit=200, compact_rows=True)
for row in rows:
src = (row.get("source") or "").strip().lower()
if src in deny:
@ -9478,7 +9478,7 @@ def _(rid, params: dict) -> dict:
cutoff = time.time() - days * 86400
rows = [
s
for s in db.list_sessions_rich(limit=500)
for s in db.list_sessions_rich(limit=500, compact_rows=True)
if (s.get("started_at") or 0) >= cutoff
]
return _ok(