mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-15 14:22:43 +00:00
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:
parent
72cc32b508
commit
c470cbd304
4 changed files with 99 additions and 9 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue