From c470cbd3042d95a62708cb5572118efd964f7699 Mon Sep 17 00:00:00 2001 From: CodeForgeNet Date: Wed, 17 Jun 2026 01:54:42 +0530 Subject: [PATCH] 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. --- hermes_cli/web_server.py | 4 ++- hermes_state.py | 38 ++++++++++++++++++++---- tests/test_hermes_state.py | 60 ++++++++++++++++++++++++++++++++++++++ tui_gateway/server.py | 6 ++-- 4 files changed, 99 insertions(+), 9 deletions(-) diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 0e77b3d7a25..14c5a4febe6 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -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, diff --git a/hermes_state.py b/hermes_state.py index 8ffe8c25f68..04233578fe3 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -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 diff --git a/tests/test_hermes_state.py b/tests/test_hermes_state.py index f4258f2b915..926e81c0ae2 100644 --- a/tests/test_hermes_state.py +++ b/tests/test_hermes_state.py @@ -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] diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 4d12a1a417b..b1611b6636c 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -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(