stage #4638 (maksym-mishchenko): cap sidebar lineage enrichment to top-N (#4654)

Fixes ~5s GET /api/sessions on power users (2400+ sessions) — _enrich_sidebar_lineage_metadata
probed state.db for every row's compression lineage. Caps to top-N (default 300, env
HERMES_WEBUI_LINEAGE_TOP_N, 0=disable). Display-safe: both call sites pass an already
pinned-first/newest-first sorted list, so sessions[:N] is exactly the paint-priority window.
Fail-open preserved. Added regression test (RED on master, GREEN here). Code byte-faithful
from PR head 0169785780.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: maksym-mishchenko <maksym-mishchenko@users.noreply.github.com>
This commit is contained in:
nesquena-hermes
2026-06-21 20:23:03 -07:00
committed by GitHub
parent 9969196e96
commit 145410d642
3 changed files with 116 additions and 2 deletions
+6
View File
@@ -3,6 +3,12 @@
## [Unreleased]
## [v0.51.571] — 2026-06-22 — Release UD (sidebar lineage enrichment cap)
### Fixed
- **Faster session sidebar for power users with thousands of sessions.** `GET /api/sessions` could spend ~5 seconds attaching compression-lineage metadata when the session count ran into the thousands. Lineage enrichment is now capped to the top-N most-recent (and pinned) sessions — the window the sidebar actually paints — which keeps the common case fast while older sessions enrich lazily. The cap defaults to 300 and is tunable via `HERMES_WEBUI_LINEAGE_TOP_N` (set to 0 to disable). Thanks @maksym-mishchenko. (#4638)
## [v0.51.570] — 2026-06-22 — Release UC (Windows restart console suppression)
### Fixed
+21 -2
View File
@@ -3200,11 +3200,30 @@ def _sidebar_title_is_generic_webui(title: str | None) -> bool:
def _enrich_sidebar_lineage_metadata(sessions: list[dict]) -> None:
"""Attach state.db compression lineage metadata used by sidebar collapse."""
"""Attach state.db compression lineage metadata used by sidebar collapse.
Cap the DB lookup to the top-N most recent sessions to bound wall-clock
on power users with thousands of sessions. The sidebar paints chronologically
newest first; older sessions almost never have visible lineage to collapse
(parents are themselves stale and rarely surface in the same render).
Lineage enrichment for those is loaded lazily when the user opens the
history panel. Issue #38914 / 2026-06-21 triage: /api/sessions was spending
4.9s on lineage_metadata across 2400+ rows.
"""
# 2026-06-21: configurable via env to ease A/B and rollback without a redeploy.
import os as _os
try:
_cap = int(_os.environ.get("HERMES_WEBUI_LINEAGE_TOP_N", "300"))
except (TypeError, ValueError):
_cap = 300
if _cap > 0 and len(sessions) > _cap:
candidates = sessions[:_cap]
else:
candidates = sessions
try:
metadata = read_session_lineage_metadata(
_active_state_db_path(),
{str(s.get('session_id')) for s in sessions if s.get('session_id')},
{str(s.get('session_id')) for s in candidates if s.get('session_id')},
)
except Exception:
return
+89
View File
@@ -0,0 +1,89 @@
"""Regression tests for #4638 — cap sidebar lineage enrichment to the top-N sessions.
On power users with thousands of sessions, GET /api/sessions spent ~5s in
_enrich_sidebar_lineage_metadata probing state.db for EVERY row's compression
lineage. The sidebar paints pinned-first then newest-first, and the caller passes
an already-sorted list, so enriching only the top-N (default 300) most-recent rows
covers the visible window while bounding wall-clock. The cap is env-configurable
(HERMES_WEBUI_LINEAGE_TOP_N) and fails open.
These tests pin: (1) only the top-N ids are probed when the list exceeds the cap,
(2) the env override is honored, (3) a non-positive / unparseable cap disables the
cap (enrich all), (4) lists at/under the cap probe everything.
"""
from __future__ import annotations
import api.models as models
def _capture_probed_ids(monkeypatch):
"""Patch read_session_lineage_metadata to record the id set it is asked for."""
seen = {}
def _fake_read(db_path, id_set):
seen["ids"] = set(id_set)
return {}
monkeypatch.setattr(models, "read_session_lineage_metadata", _fake_read)
monkeypatch.setattr(models, "_active_state_db_path", lambda: ":memory:")
return seen
def _sessions(n):
# Caller passes an already pinned-first/newest-first sorted list; index order
# therefore IS paint priority. id "s0" is the most-recent/visible-most.
return [{"session_id": f"s{i}"} for i in range(n)]
def test_caps_enrichment_to_top_n_default_300(monkeypatch):
seen = _capture_probed_ids(monkeypatch)
monkeypatch.delenv("HERMES_WEBUI_LINEAGE_TOP_N", raising=False)
models._enrich_sidebar_lineage_metadata(_sessions(1000))
assert seen["ids"] == {f"s{i}" for i in range(300)}, (
"Default cap must probe exactly the top-300 (paint-priority) sessions"
)
def test_env_override_changes_cap(monkeypatch):
seen = _capture_probed_ids(monkeypatch)
monkeypatch.setenv("HERMES_WEBUI_LINEAGE_TOP_N", "50")
models._enrich_sidebar_lineage_metadata(_sessions(1000))
assert seen["ids"] == {f"s{i}" for i in range(50)}, (
"HERMES_WEBUI_LINEAGE_TOP_N must bound the probed set"
)
def test_non_positive_cap_disables_capping(monkeypatch):
seen = _capture_probed_ids(monkeypatch)
monkeypatch.setenv("HERMES_WEBUI_LINEAGE_TOP_N", "0")
models._enrich_sidebar_lineage_metadata(_sessions(500))
assert len(seen["ids"]) == 500, "cap<=0 must enrich all sessions (cap disabled)"
def test_unparseable_cap_falls_back_to_default(monkeypatch):
seen = _capture_probed_ids(monkeypatch)
monkeypatch.setenv("HERMES_WEBUI_LINEAGE_TOP_N", "not-a-number")
models._enrich_sidebar_lineage_metadata(_sessions(1000))
assert seen["ids"] == {f"s{i}" for i in range(300)}, (
"An unparseable cap must fall back to the default 300, not crash"
)
def test_list_under_cap_probes_everything(monkeypatch):
seen = _capture_probed_ids(monkeypatch)
monkeypatch.delenv("HERMES_WEBUI_LINEAGE_TOP_N", raising=False)
models._enrich_sidebar_lineage_metadata(_sessions(120))
assert seen["ids"] == {f"s{i}" for i in range(120)}, (
"A list at/under the cap must enrich every session"
)
def test_enrichment_failure_is_swallowed(monkeypatch):
"""Lineage enrichment must fail open — a DB error never breaks /api/sessions."""
def _boom(db_path, id_set):
raise RuntimeError("db down")
monkeypatch.setattr(models, "read_session_lineage_metadata", _boom)
monkeypatch.setattr(models, "_active_state_db_path", lambda: ":memory:")
# Must not raise.
models._enrich_sidebar_lineage_metadata(_sessions(10))