docs+test: document HERMES_WEBUI_VISIBLE_SESSION_LIMIT; probe it in a subprocess

- README env table + .env.example gain the new setting (default 20, fallback
  semantics), pinned by test_setting_is_documented.
- tests/test_visible_session_limit_env.py no longer importlib.reload()s
  api.models in the shared pytest process (which recreated its module-level
  locks/caches/classes behind other modules' backs); each case now imports
  api.models in a fresh interpreter with a scrubbed env, matching
  test_issue3283_profiles_config_import_order.
This commit is contained in:
carlotestor committed 2026-09-20 11:19:41 +00:00
1 parent 63d024dc45
commit 899b89e1d3
3 files changed
+60 -18

No files matched your search

+5
View File
@@ -132,6 +132,11 @@
# Extra sources for the notes drawer (see docs for the expected format).
# HERMES_WEBUI_EXTERNAL_NOTES_SOURCES=
# Sidebar recency window: how many recent sessions are listed (default: 20).
# Also caps how many delegated subagent children can nest at once; raise it
# for wide fan-outs. Invalid or non-positive values fall back to 20.
# HERMES_WEBUI_VISIBLE_SESSION_LIMIT=20
# Directory scanned for WebUI plugins (default: ~/.hermes/plugins).
# HERMES_WEBUI_PLUGINS_DIR=~/.hermes/plugins
+1
View File
@@ -373,6 +373,7 @@ Full list of environment variables:
| `HERMES_HOME` | Windows: `%LOCALAPPDATA%\hermes`; POSIX: `~/.hermes` | Base directory for Hermes state (affects all paths) |
| `HERMES_CONFIG_PATH` | `$HERMES_HOME/config.yaml` | Path to Hermes config file |
| `HERMES_WEBUI_SERVER_CWD` | *(unset)* | Working directory for the server process. Defaults to the agent dir; point it at a writable workspace when the agent dir is read-only so fallback relative writes land somewhere writable |
| `HERMES_WEBUI_VISIBLE_SESSION_LIMIT` | `20` | Size of the sidebar's interactive recency window (how many recent non-cron/webhook sessions are listed). Also bounds how many delegated subagent children can nest at once, since a child only renders when its row wins a slot in the window — raise it for wide fan-outs. Non-integer or non-positive values fall back to the default |
| `HERMES_WEBUI_AGENT_CACHE_MAX` | `25` | Max live agent instances kept warm in the in-memory LRU. Each pins a full conversation transcript, so this is the dominant lever on resident memory — lower it on installs with many long sessions to cap RAM (at the cost of more cold reloads) |
| `HERMES_WEBUI_SESSIONS_MAX` | `100` | Legacy operator override for the max compact `Session` objects held in the in-memory LRU. Prefer the `webui.sessions_cache_max` key in `config.yaml` (which takes precedence); this env var remains a fallback. Bounds resident memory so long-running installs cannot accumulate every session ever touched and eventually crash (#4765/#2233/#4633). Eviction only ever drops clean, persisted, non-active sessions; an evicted session lazily reloads from its JSON sidecar on next access |
+54 -18
View File
@@ -3,33 +3,69 @@
The sidebar recency window also bounds how many delegated subagent children can
render at once, since a child only nests when its row wins a slot in the same
payload. Operators running wide fan-outs need to raise it without editing code.
The constant is bound once at import time, so each case imports ``api.models``
in a fresh subprocess (same pattern as test_issue3283_profiles_config_import_order)
rather than ``importlib.reload``-ing it in the shared test process — reloading
would recreate the module's locks, caches, and classes while other modules keep
references to the old objects.
"""
import importlib
import os
import subprocess
import sys
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[1]
_PROBE = """
import api.models
print(api.models.CLI_VISIBLE_SESSION_LIMIT)
"""
def _reload_limit(monkeypatch, value):
if value is None:
monkeypatch.delenv("HERMES_WEBUI_VISIBLE_SESSION_LIMIT", raising=False)
else:
monkeypatch.setenv("HERMES_WEBUI_VISIBLE_SESSION_LIMIT", value)
import api.models as models
return importlib.reload(models).CLI_VISIBLE_SESSION_LIMIT
def _limit_in_fresh_interpreter(tmp_path, value):
home = tmp_path / "home"
home.mkdir()
env = os.environ.copy()
for key in list(env):
if key.startswith("HERMES_WEBUI_") or key in ("HERMES_HOME", "HERMES_BASE_HOME"):
env.pop(key)
env["HOME"] = str(home)
env["PYTHONPATH"] = str(REPO_ROOT)
if value is not None:
env["HERMES_WEBUI_VISIBLE_SESSION_LIMIT"] = value
proc = subprocess.run(
[sys.executable, "-c", _PROBE],
cwd=REPO_ROOT,
env=env,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=60,
check=False,
)
assert proc.returncode == 0, proc.stderr
return int(proc.stdout.strip())
def test_defaults_to_20_when_unset(monkeypatch):
assert _reload_limit(monkeypatch, None) == 20
def test_defaults_to_20_when_unset(tmp_path):
assert _limit_in_fresh_interpreter(tmp_path, None) == 20
def test_env_override_raises_the_window(monkeypatch):
assert _reload_limit(monkeypatch, "64") == 64
def test_env_override_raises_the_window(tmp_path):
assert _limit_in_fresh_interpreter(tmp_path, "64") == 64
def test_invalid_value_falls_back_to_default(monkeypatch):
assert _reload_limit(monkeypatch, "bogus") == 20
@pytest.mark.parametrize("value", ["bogus", "0", "-5"])
def test_invalid_or_nonpositive_falls_back_to_default(tmp_path, value):
assert _limit_in_fresh_interpreter(tmp_path, value) == 20
def test_zero_and_negative_fall_back_to_default(monkeypatch):
assert _reload_limit(monkeypatch, "0") == 20
assert _reload_limit(monkeypatch, "-5") == 20
def test_setting_is_documented():
readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8")
env_example = (REPO_ROOT / ".env.example").read_text(encoding="utf-8")
assert "| `HERMES_WEBUI_VISIBLE_SESSION_LIMIT` | `20` |" in readme
assert "# HERMES_WEBUI_VISIBLE_SESSION_LIMIT=20" in env_example