Skip to content

fix(honcho): bound local session cache growth - #71463

Open
alex107ivanov wants to merge 2 commits into
NousResearch:mainfrom
alex107ivanov:fix/honcho-session-cache-unbounded-growth
Open

fix(honcho): bound local session cache growth#71463
alex107ivanov wants to merge 2 commits into
NousResearch:mainfrom
alex107ivanov:fix/honcho-session-cache-unbounded-growth

Conversation

@alex107ivanov

Copy link
Copy Markdown
Contributor

What does this PR do?

Bounds the two unbounded-growth sources in the Honcho local session cache described in #71461: HonchoSession.messages (never trimmed) and HonchoSessionManager's four caches (_cache, _peers_cache untouched/_sessions_cache/_context_cache, no eviction path except an explicit /new). Together these cause monotonic RSS growth on any gateway with a long-lived channel that's never manually reset — distinct from the _agent_cache leak already fixed for #48287.

This is scoped to plugins/memory/honcho/session.py only — no changes to gateway/run.py or its watcher loop, since get_or_create() already re-fetches from Honcho (the durable source of truth) on a cache miss, so the fix can be entirely opportunistic/self-contained rather than needing new background-task wiring.

Related Issue

Fixes #71461

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • plugins/memory/honcho/session.py:
    • Added _SESSION_MESSAGE_RETENTION / _SESSION_IDLE_TTL_SECONDS / _SESSION_SWEEP_INTERVAL_SECONDS constants.
    • HonchoSessionManager._trim_synced_messages() (new, static): drops already-synced messages beyond the retention cap after a successful flush; never touches unsynced messages, even ones stuck mid-list from a failed sync.
    • HonchoSessionManager._sweep_idle_sessions_locked() / _maybe_sweep_idle_sessions() (new): idle-TTL eviction across _cache/_sessions_cache/_context_cache, rate-limited and triggered opportunistically from get_or_create() — no new background task needed. _peers_cache is deliberately left alone: it's keyed by distinct peer/user id, not by session, so its cardinality is bounded by user count rather than by uptime — it isn't part of this leak.
    • _flush_session() now calls _trim_synced_messages() right after a successful sync.
    • get_or_create() now calls _maybe_sweep_idle_sessions() on entry.
  • tests/test_honcho_session_cache_bounds.py (new): 7 tests covering trim-caps-length, trim-never-drops-unsynced (including the stuck-mid-list case), trim-is-noop-under-cap, sweep-evicts-stale-across-all-caches, sweep-keeps-fresh, sweep-is-rate-limited, and a threading test guarding against the RLock reentrancy this design depends on (sweep is triggered from inside a method that also holds the lock).

How to Test

  1. pytest tests/test_honcho_session_cache_bounds.py -v — all 7 new tests pass.
  2. pytest tests/test_honcho_session_context.py tests/test_honcho_client_concurrency.py tests/test_honcho_startup_fail_open.py tests/test_honcho_client_config.py -v — confirms no regression in the existing 29 Honcho tests.
  3. Manual repro of the original bug: create a HonchoSession, call add_message in a loop past _SESSION_MESSAGE_RETENTION, call _flush_sessionlen(session.messages) stays bounded instead of growing forever. Similarly, age a cached session's updated_at past _SESSION_IDLE_TTL_SECONDS and call _maybe_sweep_idle_sessions() — it's evicted.

Checklist

Code

Documentation & Housekeeping

  • N/A — no config keys added, no architecture/workflow changes, no tool schema changes.

Screenshots / Logs

$ pytest tests/test_honcho_session_cache_bounds.py -v
...
7 passed in 0.17s

$ pytest tests/test_honcho_session_context.py tests/test_honcho_client_concurrency.py \
    tests/test_honcho_startup_fail_open.py tests/test_honcho_client_config.py -q
29 passed in 5.40s

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/plugins Plugin system and bundled plugins tool/memory Memory tool and memory providers area/memory Memory subsystem: store, providers, sync, background reviews sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 25, 2026
HonchoSession.messages grew forever -- add_message() appended every
turn and _flush_session() only marked entries synced, never trimmed
them. HonchoSessionManager's four caches (_cache, _peers_cache,
_sessions_cache, _context_cache) had no eviction path besides an
explicit /new reset. A long-lived channel that is never manually
reset accumulates both for the gateway's entire uptime.

Honcho is the durable source of truth (get_or_create() already
re-fetches history from Honcho on a cache miss), so both bounds are
safe: evicting an idle local entry only costs one extra Honcho
round-trip next time that key is used.

- Trim already-synced messages beyond a retention window right after
  a successful flush; unsynced messages are never touched.
- Add a rate-limited idle-TTL sweep triggered opportunistically from
  get_or_create(), so no new background task/watcher wiring is
  needed.

Fixes NousResearch#71461
@alex107ivanov
alex107ivanov force-pushed the fix/honcho-session-cache-unbounded-growth branch from 48a207a to d967329 Compare July 26, 2026 01:31
@alex107ivanov

Copy link
Copy Markdown
Contributor Author

Rebased this PR onto upstream/main.

  • upstream base: ba159d6fa9a12f29f85aaefab9a98020d9ca683a
  • head: 48a207a3f8a1 -> d967329308f3
  • validation: PYTHONPATH=$PWD /workspace/repos/hermes-agent/.venv/bin/python -m pytest tests/test_honcho_session_cache_bounds.py -q -> exit 0; ....... [100%] | 7 passed in 3.91s; git diff --check upstream/main...HEAD -> exit 0
  • GitHub checks for new head: 0 workflow runs, 0 check runs observed immediately after push

@alex107ivanov

Copy link
Copy Markdown
Contributor Author

Rebased this PR onto upstream/main.

  • upstream base: 6cf572c9e5d88a33e0e51cfc5bcfe47f02682731
  • head: d967329308f3 -> 5be5377155aa
  • validation: PYTHONPATH=$PWD /workspace/repos/hermes-agent/.venv/bin/python -m pytest tests/test_honcho_session_cache_bounds.py -q -> exit 0; ....... [100%] | 7 passed in 1.33s; git diff --check upstream/main...HEAD -> exit 0
  • GitHub checks for new head: 0 workflow runs, 0 check runs observed immediately after push

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing a real current-main retention problem: add_message() still appends indefinitely and _flush_session() only marks entries synced (plugins/memory/honcho/session.py:45-54,421-458).

Problems

  • The new cap is not a hard bound. _trim_synced_messages() stops at the first unsynced entry (plugins/memory/honcho/session.py:497 in this PR); the added test intentionally retains entries 10–299 after placing an unsynced message at index 10 (tests/test_honcho_session_cache_bounds.py:64-77). A persistent failed sync can therefore still grow the local list without limit.
  • The sweep pops _context_cache under _cache_lock (plugins/memory/honcho/session.py:401 in this PR), but normal context-cache writes/pops are guarded by _prefetch_cache_lock on main (plugins/memory/honcho/session.py:708-718). Please use that lock for sweep removal and cover the concurrent prefetch case.

Suggested changes

  • Specify a bounded failure-mode policy for unsynced messages, or narrow the claimed guarantee to successfully persisted history and test that contract.
  • Synchronize context-cache eviction with the existing prefetch-cache lock.

Automated hermes-sweeper review.

continue
del self._cache[key]
self._sessions_cache.pop(session.honcho_session_id, None)
self._context_cache.pop(key, None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_context_cache is otherwise accessed under _prefetch_cache_lock (set_context_result() and pop_context_result() on main). Please acquire that lock for this removal as well; a background prefetch can otherwise race an idle sweep and repopulate stale context after eviction.

negligible in practice.
"""
excess = len(session.messages) - _SESSION_MESSAGE_RETENTION
while excess > 0 and session.messages and session.messages[0].get("_synced"):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This stops at the first unsynced item, so it does not enforce the advertised retention bound: the added test's unsynced item at index 10 leaves 290 entries. Please define and test the intended persistent-sync-failure policy, or narrow the bound claim to histories that have successfully flushed.

@teknium1 teknium1 added sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform area/sessions Session lifecycle, resume, persistence, history labels Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/memory Memory subsystem: store, providers, sync, background reviews area/sessions Session lifecycle, resume, persistence, history comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/memory Memory tool and memory providers type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Honcho local session cache grows unbounded — no eviction path except explicit /new

3 participants