hermes - ✅(Solved) Fix [Bug]: The on_session_finalize hook is not being fired when gateway sessions expire due to configured idle time. [3 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#14981Fetched 2026-04-24 10:43:50
View on GitHub
Comments
1
Participants
2
Timeline
5
Reactions
0
Timeline (top)
labeled ×4commented ×1

Error Message

Additional Logs / Traceback (optional)

Root Cause

Evidence of the Bug The expiry watcher code has no invocation of hermes_cli.plugins.invoke_hook for on_session_finalize Tests in tests/gateway/test_session_boundary_hooks.py verify the hook fires for /new commands and shutdown test_session_boundary_hooks.py:76-156 , but there are no tests for idle timeout scenarios The CLI implementation properly fires the hook via _notify_session_boundary cli.py:4505-4533 Root Cause The session expiry watcher was designed to proactively flush memories before the next user message arrives, but the hook firing mechanism was overlooked. This is inconsistent with the documented behavior and with how other session teardown scenarios handle the hook.

Fix Action

Fixed

PR fix notes

PR #15132: fix(gateway): fire on_session_finalize on idle expiry (salvage #13756)

Description (problem / solution / changelog)

Salvage of #13756 by @dimitrovi onto current main with a regression test added.

Closes #14981.

What this PR does

When _session_expiry_watcher sweeps a session that has aged past its reset policy (idle timeout, scheduled reset), it now fires on_session_finalize — matching the contract already honored by /new, /reset, CLI shutdown, and gateway stop. Before this PR, the expiry path flushed memories and evicted the agent silently, so plugins using on_session_finalize never saw their final-pass extraction opportunity for idle-expired sessions.

How

In _session_expiry_watcher, right after _async_flush_memories completes for an expired session, invoke hermes_cli.plugins.invoke_hook("on_session_finalize", session_id=..., platform=...). Platform is parsed from the session key (agent:main:<platform>:<chat_type>:<chat_id>). The invocation is wrapped in try/except so a misbehaving plugin can't break the expiry sweep.

Changes

  • @dimitrovi (#13756 commit): 11 lines in gateway/run.py
  • Follow-up: AUTHOR_MAP entry for [email protected] → @dimitrovi
  • Regression test: test_idle_expiry_fires_finalize_hook in tests/gateway/test_session_boundary_hooks.py

Validation

BeforeAfter
/new fires on_session_finalize
/reset fires hook
CLI shutdown fires hook
Gateway stop() fires hook for each active agent
Idle expiry fires hook✗ (silent)

Test verified as a real regression guard: fails cleanly on pre-fix code ("on_session_finalize was not fired during idle expiry"), passes with the fix applied.

  • tests/gateway/test_session_boundary_hooks.py — 6/6 pass (1 new regression test)

Note on parallel PR #11410

#11410 fires MemoryProvider.on_session_end() (a different lifecycle hook on the memory-manager layer) on expiry. That's a separate gap — distinct from the plugin-hook fix here. Leaving #11410 open for separate review.

Co-authored-by: @dimitrovi

Changed files

  • gateway/run.py (modified, +11/-0)
  • scripts/release.py (modified, +1/-0)
  • tests/gateway/test_session_boundary_hooks.py (modified, +77/-0)

PR #13756: Invoke session finalize hooks on expiry flush

Description (problem / solution / changelog)

Summary

  • invoke the on_session_finalize plugin hook after successful expiry-driven memory flushes
  • pass through the expired session id and detected platform
  • keep hook failures non-fatal so expiry cleanup still completes

Why

DIM memory automation relies on session finalization side effects after idle expiry. Without this hook, Hermes flushes provider memory but does not trigger the profile-local capture path.

Changed files

  • gateway/run.py (modified, +11/-0)

PR #15481: fix(gateway): pass session messages to shutdown_memory_provider (#15165)

Description (problem / solution / changelog)

Summary

  • Fixes #15165 Part A_cleanup_agent_resources previously invoked agent.shutdown_memory_provider() with no arguments, so every memory provider's on_session_end hook received an empty list. Providers with an early-return guard on empty input (Holographic, Hindsight, etc.) never extracted facts from the conversation.
  • Forward agent._session_messages — the transcript AIAgent maintains and refreshes every turn via _persist_session — so providers see the actual conversation instead of [].
  • Falls back to the legacy no-arg call whenever _session_messages is absent or not a list (test stubs built via object.__new__ or MagicMock) to keep every existing suite green.

The bug

Per #15165:

...any provider that tries to extract facts from the session's conversation gets an empty list. Providers like Holographic (on_session_end([])) have an early return guard... Every gateway restart wipes the session's conversational memory...

Users saw "抱歉,找不到相關的對話記錄" (roughly: "sorry, cannot find the corresponding conversation record") on the first turn after any gateway restart / idle expiry / session reset because no facts had ever been persisted.

The fix

AIAgent.shutdown_memory_provider already accepts messages: list = None (run_agent.py:4126), so this is a pure caller-side change in gateway/run.py:

session_messages = getattr(agent, "_session_messages", None)
if isinstance(session_messages, list):
    agent.shutdown_memory_provider(session_messages)
else:
    agent.shutdown_memory_provider()
  • _session_messages is set on AIAgent.__init__ (run_agent.py:1518) and refreshed at the end of every run_conversation turn (_persist_session, line 3264). By the time _cleanup_agent_resources fires, it holds the real transcript.
  • isinstance(..., list) discrimination is deliberate — it protects against MagicMock agents (whose attribute access auto-synthesises a child mock, not None) falling through and passing a bogus object to the provider's List[Dict] hook.
  • The try/except Exception: pass wrap is inherited; providers that raise never prevent close() from running.
  • Paths using skip_memory=True temporary agents (pre-reset memory flush, session hygiene auto-compress, /compress) are no-ops inside shutdown_memory_provider because self._memory_manager is None — so this change has no behaviour effect on them.

Scope

Part A only. Part B of #15165 (adding on_session_end to the Hindsight plugin) is a separate concern that benefits from this fix landing first — without Part A, a Hindsight on_session_end hook would still receive [].

Test plan

  • New regression suite: tests/gateway/test_shutdown_memory_provider_messages.py — 7 cases:
    • Populated _session_messages list is forwarded byte-for-byte
    • Empty list is still explicitly forwarded (matches pre-fix observable behaviour)
    • Agent without _session_messages falls back to no-arg call (stub compatibility)
    • MagicMock agent (non-list attribute) falls back to no-arg call
    • Provider exception is swallowed — close() still runs afterward
    • None agent is a no-op (idle-sweep race tolerance)
    • Agent without shutdown_memory_provider method still gets close()
  • Regression guard verified: reverted the fix → test_populated_messages_forwarded fails with Expected: mock([...]) / Actual: mock(); restored the fix → all 7 pass.
  • Full tests/gateway/ suite: 3703 passed, 9 pre-existing failures (dingtalk, matrix with missing mautrix, one approve-deny test — all fail identically on main, unrelated to this change).
  • Related suites pass clean (100/100): test_agent_cache.py, test_compress_command.py, test_background_command.py, test_flush_memory_stale_guard.py, test_session_boundary_hooks.py, test_compress_plugin_engine.py, test_clean_shutdown_marker.py.

Related

  • Fixes #15165 (Part A)
  • #7759 (closed) — /new / /reset memory commit, different code path
  • #14981 — on_session_finalize not fired on idle timeout (orthogonal)
  • #15073 — Hindsight sync races interpreter shutdown (orthogonal)

🤖 Generated with Claude Code

Changed files

  • gateway/run.py (modified, +15/-1)
  • tests/gateway/test_shutdown_memory_provider_messages.py (added, +148/-0)

Code Example

LOG_FILE=/tmp/plugin.log
def on_session_finalize(session_id=None, **kwargs):
    entry = {
        "timestamp": datetime.now().isoformat(),
        "session_id": session_id,
        "kwargs": kwargs,
    }
    with open(LOG_FILE, "a") as f:
        f.write(json.dumps(entry) + "\n")
        f.flush()
def register(ctx):
    ctx.register_hook("on_session_finalize", on_session_finalize)
    with open(LOG_FILE, "a") as f:
        f.write(json.dumps({"register": "on_session_finalize registered"}) + "\n")
        f.flush()

---

no logs as nothing happened

---
RAW_BUFFERClick to expand / collapse

Bug Description

Summary The background session expiry watcher in gateway/run.py cleans up expired sessions but does not fire the on_session_finalize hook, despite the documentation stating this hook should fire when "CLI/gateway tears down an active session" hooks.md:613-634 .

Detailed Analysis Expected Behavior The on_session_finalize hook is documented to fire when:

CLI exits with an active agent Gateway tears down an active session (including GC/cleanup scenarios) User runs /new or /reset commands Actual Behavior in Idle Timeout When a session expires due to idle time, the _session_expiry_watcher function in gateway/run.py run.py:2247-2394 performs these actions:

Flushes memories via _async_flush_memories Cleans up agent resources via _cleanup_agent_resources Evicts the agent from cache via _evict_cached_agent Marks the session as flushed However, it does not call the on_session_finalize hook.

Evidence of the Bug The expiry watcher code has no invocation of hermes_cli.plugins.invoke_hook for on_session_finalize Tests in tests/gateway/test_session_boundary_hooks.py verify the hook fires for /new commands and shutdown test_session_boundary_hooks.py:76-156 , but there are no tests for idle timeout scenarios The CLI implementation properly fires the hook via _notify_session_boundary cli.py:4505-4533 Root Cause The session expiry watcher was designed to proactively flush memories before the next user message arrives, but the hook firing mechanism was overlooked. This is inconsistent with the documented behavior and with how other session teardown scenarios handle the hook.

Steps to Reproduce

Write plugin with on_session_finalize

LOG_FILE=/tmp/plugin.log
def on_session_finalize(session_id=None, **kwargs):
    entry = {
        "timestamp": datetime.now().isoformat(),
        "session_id": session_id,
        "kwargs": kwargs,
    }
    with open(LOG_FILE, "a") as f:
        f.write(json.dumps(entry) + "\n")
        f.flush()
def register(ctx):
    ctx.register_hook("on_session_finalize", on_session_finalize)
    with open(LOG_FILE, "a") as f:
        f.write(json.dumps({"register": "on_session_finalize registered"}) + "\n")
        f.flush()

Open/close cli session -> notice new entry in plugin.log Start gateway session Wait for idle timeout ->notice no new log entry

Expected Behavior

Should have fired

Actual Behavior

didn't

Affected Component

Other

Messaging Platform (if gateway-related)

No response

Debug Report

no logs as nothing happened

Operating System

debian 13

Python Version

No response

Hermes Version

No response

Additional Logs / Traceback (optional)

Root Cause Analysis (optional)

No response

Proposed Fix (optional)

No response

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

extent analysis

TL;DR

The on_session_finalize hook is not being fired when a session expires due to idle timeout, and a fix is needed to invoke this hook in the _session_expiry_watcher function.

Guidance

  • Review the _session_expiry_watcher function in gateway/run.py to understand how sessions are cleaned up when they expire due to idle timeout.
  • Modify the _session_expiry_watcher function to invoke the on_session_finalize hook after cleaning up the session resources.
  • Add tests to tests/gateway/test_session_boundary_hooks.py to cover the idle timeout scenario and verify that the on_session_finalize hook is fired correctly.
  • Verify that the on_session_finalize hook is being fired by checking the plugin log file for new entries after the session has expired due to idle timeout.

Example

# in gateway/run.py
def _session_expiry_watcher(session):
    # ... (existing cleanup code)
    hermes_cli.plugins.invoke_hook("on_session_finalize", session_id=session.id)

Notes

The exact implementation details may vary depending on the specific requirements of the on_session_finalize hook and the session cleanup process.

Recommendation

Apply workaround: Modify the _session_expiry_watcher function to invoke the on_session_finalize hook, as this will ensure that the hook is fired when a session expires due to idle timeout, consistent with the documented behavior.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix [Bug]: The on_session_finalize hook is not being fired when gateway sessions expire due to configured idle time. [3 pull requests, 1 comments, 2 participants]