Release v0.51.256 — Release HX (stage-r4) (#3596)
## Release v0.51.256 — Release HX (stage-r4) Performance — bound WebUI memory growth & idle CPU on large installs. ### Fixed | Issue | Author | Fix | |-------|--------|-----| | #3506 | @nesquena-hermes (reported w/ profiling by @djenttleman) | On a large install (~615 sessions / 40k messages / 454 MB state.db) the WebUI process climbed ~100 MB → ~1.5 GB RSS over days and held high idle CPU. Three root causes fixed: (1) `session_lifecycle._sessions` grew unbounded → new `discard_session()` drops the entry at agent-eviction boundaries, only when no in-flight commit / no uncommitted memory work (retry invariant preserved); (2) cache caps now operator-tunable (`HERMES_WEBUI_AGENT_CACHE_MAX` default 50→25, `HERMES_WEBUI_SESSIONS_MAX`); (3) GatewayWatcher computes a cheap fingerprint before the expensive per-session `MAX(messages.timestamp)` projection and only re-projects on change. | ### Rebase + review notes - Rebased onto current master; the code diff was verified **byte-identical to the nesquena-APPROVED head** at rebase time (only CHANGELOG re-resolved). - The Codex regression gate then surfaced **two correctness gaps** the approval didn't catch, both fixed here with regression tests: 1. **Watcher fingerprint missed same-count transcript rewrites.** `/retry`,`/undo`,`/compress` (`SessionDB.replace_messages`) rewrite messages with new timestamps but can leave `message_count` unchanged → stale sidebar `last_activity`. Fixed with a **per-session** grouped message aggregate (`id, count, user_count, MAX(timestamp)`) over the same non-excluded sessions (a global MAX would miss a rewrite of an older, non-newest session); cron/webui stay excluded so idle churn still doesn't re-project. 2. **LRU agent-cache eviction could close a live worker's agent** (`popitem(last=False)`, liveness-blind — pre-existing, but the lower 50→25 cap made it more likely). Eviction now snapshots `ACTIVE_RUNS` session_ids (before the cache lock — no nested lock) and skips live sessions, deferring (temporarily exceeding cap) rather than closing a live agent. ### Gate - Full pytest suite: **7612 passed, 0 failed** (one boot-cascade flake re-run; clean on re-run) - ruff: CLEAN · Codex (regression): 4 rounds → both gaps + a stale test fixed → **SAFE TO SHIP** Co-authored-by: nesquena <nesquena@users.noreply.github.com>
This commit is contained in:
1 parent
8066d384ca
commit
3ca1188f64
9 files changed
+688
-14
No files matched your search
@@ -328,7 +328,12 @@ def test_lru_eviction_commits_outside_cache_lock():
|
||||
|
||||
assert "commit_session_memory" not in locked_section
|
||||
assert "_lifecycle_commit" not in locked_section
|
||||
assert "SESSION_AGENT_CACHE.popitem" in locked_section
|
||||
# Eviction now selects the oldest INACTIVE entry (active-run-aware) and pops
|
||||
# it by id under the lock, rather than a liveness-blind popitem(last=False).
|
||||
# The commit/close still happens outside the lock (asserted below).
|
||||
assert "SESSION_AGENT_CACHE.pop(_evictable_sid)" in locked_section
|
||||
assert "_sid not in _active_sids" in locked_section
|
||||
assert "SESSION_AGENT_CACHE.popitem(last=False)" not in locked_section
|
||||
assert "_close_evicted_agent_at_session_boundary" in outside_section
|
||||
helper_start = src.index("def _close_evicted_agent_at_session_boundary")
|
||||
helper_end = src.index("\ndef _refresh_cached_agent_runtime", helper_start)
|
||||
|
||||
Reference in new issue
Block a user