Release p1-batch: session cache cap 300→100 (#6362) + virtualization comment (#6318) (#6375)

* perf(transcript): enable DOM virtualization by default (re-enable #4346 fix) (#6151) (#6155)

* fix: revert DOM virtualization default to opt-in, fix gate RED (#6155)

* fix(#6351): lower default session cache cap

* Release p1-batch: session cache cap 300->100 (#6362) + virtualization comment (#6318)

---------

Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
This commit is contained in:
authored and GitHub committed 2026-07-21 02:53:43 +00:00
1 parent 302843c89f
commit a725a0f71c
5 files changed
+44 -12

No files matched your search

+4
View File
@@ -11,12 +11,16 @@
- **Internal: removed a dead `rowIndex` parameter from the settled-scene row projector.** `attachLiveStream`'s `pushRow` helper carried an unused positional index left over from before final-segment eligibility moved to a row-identity `WeakSet`; dropping it is a pure no-op refactor (differential execution confirms byte-identical settled scenes — ordering, dedupe, final-prefix suppression, and sequence assignment all unchanged). Thanks @webtecnica. (#6258)
- **Internal: clarified the transcript-virtualization boot comment (opt-in default retained).** The comment above the boot-time `_virtualizeTranscript` apply now records that the #4346 Phase B footer-jitter fix resolved the original scroll-up flicker root cause, while virtualization stays opt-in (default OFF) until battle-tested further. Zero behavior change — the apply predicate, settings-load fallback, checkbox, and server default all remain opt-in/OFF and in agreement. Thanks @webtecnica. (#6318, #6155)
- **Docs: reconciled the assistant-reply lifecycle RFCs/contracts with shipped behavior.** The Stable Assistant Turn Anchors and Live-to-Final RFCs, `docs/CONTRACTS.md`, and the Phase 0 architecture inventory were updated from "proposed/scaffold" framing to "accepted/implemented" to match what actually ships (single assistant-turn owner projecting one `activity_scene_v1` into Compact Worklog / Transparent Stream / Final answer). Documentation-only — no runtime behavior or CI-gate change; the one code touch is a corrected comment header in `static/assistant_turn_anchors.js`. Remaining hardening stays tracked under #3400. Thanks @franksong2702. (#6144)
- **The busy-time send behavior is now called "Default message mode," and new installs default to Steer.** The Settings → Preferences control formerly labeled "Busy input mode" is renamed to "Default message mode," and a fresh install now defaults to **Steer** (inject a mid-turn correction without interrupting) instead of Queue. Your existing choice is preserved — if you ever saved settings, your current mode (Queue/Interrupt/Steer) is migrated as-is and unchanged; only never-configured installs pick up the new Steer default. The saved preference still survives a reload or a brief server outage (the localStorage mirror from the previous release is intact). Thanks @rodboev. (#5162, #5145)
### Fixed
- **Long-running installs use less memory: the default resident-session cache cap is lowered from 300 to 100.** The in-memory `SESSIONS` LRU (compact session objects) is a dominant contributor to WebUI RSS climbing over hours of continuous use; the shipped no-override default is now tuned for the common single-user install. Larger deployments can still raise it via `webui.sessions_cache_max` in `config.yaml` (preferred) or the `HERMES_WEBUI_SESSIONS_MAX` env fallback. Eviction safety is unchanged — only clean, persisted, non-active sessions are ever dropped, and an evicted session lazily reloads from its JSON sidecar on next access (no data loss). Thanks @rodboev. (#6362, #6351)
- **Completed image tool-results are compacted from persisted history, cutting long-session memory and save/load cost.** After a turn finishes, base64 image parts in persisted *tool* results are replaced with a small `[screenshot]` text placeholder, so JSON sidecar saves/loads and session API responses no longer scale with image bytes (the active model still gets the full image while the tool call is running). The transform preserves every non-image part in place (text, documents, provider-specific parts) without flattening or reordering, only touches `role: tool` messages (user attachments are untouched), runs on all live and settled persistence writebacks, and is idempotent. Thanks @sjungwon03. (#6315, #1217)
- **Large embedded images no longer slow down API response redaction.** Native raster image data URIs (`data:image/png|jpeg|gif|webp|bmp;base64,…`) are skipped by the credential-redaction pass instead of being scanned byte-by-byte on every response — a multi-megabyte image is opaque binary, not text a credential regex should rewrite. The fast path only applies after the *entire* base64 payload is validated (canonical round-trip + structural walk), so a string that merely starts with an image header but carries trailing or appended text still goes through full redaction — a credential can never ride along in an image's tail. Non-image strings, and sibling secrets in the same payload, are unaffected. Thanks @inch772. (#6311)
+1 -1
View File
@@ -373,7 +373,7 @@ Full list of environment variables:
| `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_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` | `300` | 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 |
| `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 |
Extension deployments can inspect sanitized, authenticated diagnostics at `GET /api/extensions/status`; see [WebUI Extensions](docs/EXTENSIONS.md#diagnostics).
+4 -2
View File
@@ -8502,16 +8502,18 @@ _INDEX_HTML_PATH = get_index_html_path()
# ── Thread synchronisation ───────────────────────────────────────────────────
LOCK = threading.Lock()
# Max compact Session objects held in the in-memory LRU (issue #3506, #4765).
# Max compact Session objects held in the in-memory LRU (issue #3506, #4765, #6351).
# Lighter than the agent cache (no live agent runtime), but still bounded so a
# long-running self-hosted install cannot accumulate every session it ever
# touched in RAM and eventually segfault (the #4765/#2233/#4633 crash cluster).
# The shipped default is tuned for the common single-user install; larger
# deployments can keep raising it through config.yaml or the legacy env fallback.
#
# Precedence for the effective cap is resolved by get_sessions_cache_max():
# 1. config.yaml webui.sessions_cache_max (preferred, no new env var)
# 2. HERMES_WEBUI_SESSIONS_MAX env var (legacy operator override)
# 3. DEFAULT_SESSIONS_CACHE_MAX (sane bounded default)
DEFAULT_SESSIONS_CACHE_MAX = 300
DEFAULT_SESSIONS_CACHE_MAX = 100
SESSIONS_MAX = _env_int("HERMES_WEBUI_SESSIONS_MAX", DEFAULT_SESSIONS_CACHE_MAX)
+4 -2
View File
@@ -3225,8 +3225,10 @@ window._mirrorSpeechSettingsFromServer=_mirrorSpeechSettingsFromServer;
window._hideEmptyStateSuggestions=s.hide_empty_state_suggestions===true;
applyEmptyStateSuggestionPref();
// #4343: transcript virtualization is EXPERIMENTAL/opt-IN (default OFF).
// It caused scroll-up flicker on long sessions, so it's off for everyone
// unless explicitly opted in; long transcripts render in full by default.
// #4346 Phase B (footer-jitter suppression during virtual-scroll
// measurement re-renders) resolved the scroll-up flicker root cause,
// but virtualization remains opt-in until battle-tested further.
// Users can explicitly enable it via Settings → virtualize_transcript.
window._virtualizeTranscript=s.virtualize_transcript===true;
window._showTps=!!s.show_tps;
window._fadeTextEffect=!!s.fade_text_effect;
+31 -7
View File
@@ -111,19 +111,43 @@ def _insert(sid_session):
# ─────────────────────────── config knob ────────────────────────────────────
def test_default_sessions_cache_cap_is_100():
"""The shipped no-override session cache default is 100 (#6351)."""
from api import config as _cfg
assert _cfg.DEFAULT_SESSIONS_CACHE_MAX == 100
def test_cache_cap_reads_config_yaml_key():
"""The cap is configurable via config.yaml webui.sessions_cache_max (#4765)."""
from api import config as _cfg
old_sessions_max = _cfg.SESSIONS_MAX
try:
_cfg.SESSIONS_MAX = 222
assert _cfg.get_sessions_cache_max({"webui": {"sessions_cache_max": 42}}) == 42
# Invalid / missing values must fall back, never disable the bound.
fallback = _cfg.get_sessions_cache_max({"webui": {"sessions_cache_max": "nope"}})
assert isinstance(fallback, int) and fallback >= 1
assert _cfg.get_sessions_cache_max({"webui": {}}) >= 1
assert _cfg.get_sessions_cache_max({}) >= 1
# A zero/negative typo must not disable the cap.
assert _cfg.get_sessions_cache_max({"webui": {"sessions_cache_max": 0}}) >= 1
assert _cfg.get_sessions_cache_max({"webui": {"sessions_cache_max": -5}}) >= 1
assert _cfg.get_sessions_cache_max({"webui": {"sessions_cache_max": "nope"}}) == 222
assert _cfg.get_sessions_cache_max({"webui": {"sessions_cache_max": 0}}) == 222
assert _cfg.get_sessions_cache_max({"webui": {"sessions_cache_max": -5}}) == 222
finally:
_cfg.SESSIONS_MAX = old_sessions_max
def test_cache_cap_preserves_environment_fallback():
"""The parsed env fallback still wins when config is absent or invalid (#6351)."""
from api import config as _cfg
old_sessions_max = _cfg.SESSIONS_MAX
try:
_cfg.SESSIONS_MAX = 222
assert _cfg.get_sessions_cache_max({"webui": {}}) == 222
assert _cfg.get_sessions_cache_max({}) == 222
_cfg.SESSIONS_MAX = _cfg.DEFAULT_SESSIONS_CACHE_MAX
assert _cfg.get_sessions_cache_max({"webui": {}}) == _cfg.DEFAULT_SESSIONS_CACHE_MAX
assert _cfg.get_sessions_cache_max({}) == _cfg.DEFAULT_SESSIONS_CACHE_MAX
finally:
_cfg.SESSIONS_MAX = old_sessions_max
# ─────────────────────────── invariant 1: eviction ──────────────────────────