mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-13 03:00:25 +00:00
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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user