Gateway session reset and resume_pending bug fixes

NousResearch/hermes-agent · cluster 46934 · gateway

Data as of

4 open items. Fix merged in #56264. Check it covers your case.

The short answer

2open issues
2open PRs
13items grouped
Known?
Yes. 13 related items grouped here, 2 issues already closed.
Fix event
A fix was merged to main in PR #56264. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #56264. That is separate from the fix event.
Still open
2 issues and 2 PRs.

This cluster centers on bugs and fixes related to gateway session recovery, where resume_pending markers, idle reset policies, and DB state restoration interact incorrectly — causing stale sessions, policy bypass, misleading notices, and context bleed. Multiple fixes address reset boundary enforcement, freshness gating, and user notification during resume_pending expiry.

Open issues2

  • Issue#61052gateway: resume_pending_expired resets despite session_reset.mode=none and shows misleading 'inactive for 24h' notice
  • Issue#66255Gateway: DB session recovery bypasses the reset policy and re-arms the idle clock — sessions become immortal across restarts

Open PRs2

  • PR#58935fix(gateway): notify user and write correct end_reason for resume_pending_expired resets
  • PR#61154fix(gateway): honor session_reset.mode none in resume_pending freshness gate

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #56264 fix(gateway): freshness-gate resume_pending against per-message zombies

    merged to main

    files: gateway/run.py, gateway/session.py, tests/gateway/test_clean_shutdown_marker.py

    Show the diff
    • gateway/run.py
      @@ -618,20 +618,19 @@ def _coerce_gateway_timestamp(value: Any) -> Optional[float]:
       def _auto_continue_freshness_window() -> float:
           """Return the configured auto-continue freshness window in seconds.
       
      -    Reads ``HERMES_AUTO_CONTINUE_FRESHNESS`` (bridged from
      -    ``config.yaml`` ``agent.gateway_auto_continue_freshness`` at gateway
      -    startup, same pattern as ``HERMES_AGENT_TIMEOUT``).  Falls back to the
      -    module default when unset or malformed.  Non-positive values disable
      -    the freshness gate (restores the pre-fix "always fresh" behaviour for
      -    users who want to opt out).
      +    Thin wrapper that delegates to the canonical implementation in
      +    ``gateway.session`` (the single source of truth shared with the
      +    routing-time zombie gate in ``get_or_create_session``).  Reads
      +    ``HERMES_AUTO_CONTINUE_FRESHNESS`` (bridged from ``config.yaml``
      +    ``agent.gateway_auto_continue_freshness`` at gateway startup, same
      +    pattern as ``HERMES_AGENT_TIMEOUT``).  Falls back to the module default
      +    when unset or malformed.  Non-positive values disable the freshness gate
      +    (restores the pre-fix "always fresh" behaviour for users who want to opt
      +    out).  Kept here so existing call sites and test patches importing it
      +    from ``gateway.run`` continue to work.
           """
      -    raw = os.environ.get("HERMES_AUTO_CONTINUE_FRESHNESS")
      -    if raw is None or raw == "":
      -        return float(_AUTO_CONTINUE_FRESHNESS_SECS_DEFAULT)
      -    try:
      -        return float(raw)
      -    except (TypeError, ValueError):
      -        return float(_AUTO_CONTINUE_FRESHNESS_SECS_DEFAULT)
      +    from gateway.session import auto_continue_freshness_window
      +    return auto_continue_freshness_window()
       
       
       def _float_env(name: str, default: float) -> float:
    • gateway/session.py
      @@ -27,6 +27,35 @@ def _now() -> datetime:
           return datetime.now()
       
       
      +# Default auto-continue freshness window in seconds (1 hour).  A session
      +# interrupted by a restart is only auto-resumed — and only returned by
      +# ``get_or_create_session`` — while it stays within this window of when
      +# ``resume_pending`` was marked.  ``gateway/run.py`` bridges
      +# ``config.yaml`` ``agent.gateway_auto_continue_freshness`` into
      +# ``HERMES_AUTO_CONTINUE_FRESHNESS`` at startup.
      +_AUTO_CONTINUE_FRESHNESS_SECS_DEFAULT = 60 * 60
      +
      +
      +def auto_continue_freshness_window() -> float:
      +    """Return the configured auto-continue freshness window in seconds.
      +
      +    Single source of truth for both the resume scheduler (``gateway/run.py``)
      +    and the routing-time zombie gate in ``get_or_create_session``.  Reads
      +    ``HERMES_AUTO_CONTINUE_FRESHNESS`` (bridged from ``config.yaml``
      +    ``agent.gateway_auto_continue_freshness`` at gateway startup) and falls
      +    back to the module default when unset or malformed.  A non-positive value
      +    disables the freshness gate (restores the pre-fix "always fresh" behaviour
      +    for users who want to opt out).
      +    """
      +    raw = os.environ.get("HERMES_AUTO_CONTINUE_FRESHNESS")
      +    if raw is None or raw == "":
      +        return float(_AUTO_CONTINUE_FRESHNESS_SECS_DEFAULT)
      +    try:
      +        return float(raw)
      +    except (TypeError, ValueError):
      +        return float(_AUTO_CONTINUE_FRESHNESS_SECS_DEFAULT)
      +
      +
       # ---------------------------------------------------------------------------
       # PII redaction helpers
       # ---------------------------------------------------------------------------
      @@ -1325,11 +1354,27 @@ def get_or_create_session(
                               # Restart-interrupted session: preserve the session_id
                               # and return the existing entry so the transcript reloads
                               # intact, but still honour normal daily/idle reset policy.
      +                        #
      +                        # Freshness gate (#46934): the idle/daily policy checks
      +                        # ``updated_at``, which is bumped to ``now`` on every
      +                        # message — so a zombie session that keeps receiving
      +                        # messages never trips it and would resume stale context
      +                        # forever.  ``last_resume_marked_at`` is set once when
      +                        # resume was marked and never bumped per-message, so it
      +                        # correctly measures how long resume has been pending.
      +                        # If that exceeds the auto-continue freshness window, the
      +                        # recovery turn either never ran or failed — treat the
      +                        # session as a zombie and fall through to auto-reset.
                               reset_reason = self._should_reset(entry, source)
                               if not reset_reason:
      -                            entry.updated_at = now
      -                            self._save()
      -                            return entry
      +                            _fw = auto_continue_freshness_window()
      +                            _ref_time = entry.last_resume_marked_at or entry.updated_at
      +                            if _fw > 0 and (now - _ref_time).total_seconds() > _fw:
      +                                reset_reason = "resume_pending_expired"
      +                            else:
      +                                entry.updated_at = now
      +                                self._save()
      +                                return entry
                           else:
                               reset_reason = self._should_reset(entry, source)
                           if not reset_reason:
    • tests/gateway/test_clean_shutdown_marker.py
      @@ -244,3 +244,76 @@ async def _run():
       
               assert agent._end_session_on_close is False
               agent.close.assert_called_once()
      +
      +
      +# ---------------------------------------------------------------------------
      +# resume_pending freshness gate (#46934)
      +# ---------------------------------------------------------------------------
      +
      +class TestResumePendingFreshnessGate:
      +    """A resume_pending session is only returned while it is still fresh.
      +
      +    ``get_or_create_session`` returns a ``resume_pending`` session so its
      +    transcript reloads intact after a restart.  But the idle/daily reset
      +    policy keys on ``updated_at``, which is bumped to ``now`` on every
      +    message — so a zombie session that keeps receiving messages never trips
      +    it and would resume stale context forever.  The freshness gate keys on
      +    ``last_resume_marked_at`` (set once at resume-mark, never bumped) so it
      +    catches that case.
      +    """
      +
      +    def _mark_resume_pending(self, store, source):
      +        """Put the session into resume_pending and return the entry."""
      +        store.get_or_create_session(source)
      +        count = store.suspend_recently_active()
      +        assert count == 1
      +        with store._lock:
      +            entry = store._entries[store._generate_session_key(source)]
      +        assert entry.resume_pending
      +        assert entry.last_resume_marked_at is not None
      +        return entry
      +
      +    def test_fresh_resume_pending_returns_same_session(self, tmp_path):
      +        store = _make_store(tmp_path)
      +        source = _make_source()
      +        entry = self._mark_resume_pending(store, source)
      +
      +        # Within the freshness window (marked just now) → same session back.
      +        refreshed = store.get_or_create_session(source)
      +        assert refreshed.session_id == entry.session_id
      +        assert refreshed.resume_pending
      +
      +    def test_stale_resume_pending_falls_through_to_reset(self, tmp_path, monkeypatch):
      +        monkeypatch.setenv("HERMES_AUTO_CONTINUE_FRESHNESS", "3600")
      +        store = _make_store(tmp_path)
      +        source = _make_source()
      +        entry = self._mark_resume_pending(store, source)
      +
      +        # Backdate the resume mark past the freshness window. Keep updated_at
      +        # fresh (as a per-message zombie would have) so the idle/daily policy
      +        # would NOT fire — only the freshness gate should catch this.
      +        with store._lock:
      +            entry.last_resume_marked_at = datetime.now() - timedelta(seconds=7200)
      +            entry.updated_at = datetime.now()
      +            store._save()
      +
      +        fresh = store.get_or_create_session(source)
      +        # Zombie detected → brand-new session, not the stale transcript.
      +        assert fresh.session_id != entry.session_id
      +        assert not fresh.resume_pending
      +
      +    def test_freshness_gate_disabled_returns_stale_session(self, tmp_path, monkeypatch):
      +        # Opt-out: window <= 0 restores the pre-fix "always fresh" behaviour.
      +        monkeypatch.setenv("HERMES_AUTO_CONTINUE_FRESHNESS", "0")
      +        store = _make_store(tmp_path)
      +        source = _make_source()
      +        entry = self._mark_resume_pending(store, source)
      +
      +        with store._lock:
      +            entry.last_resume_marked_at = datetime.now() - timedelta(seconds=999999)
      +            entry.updated_at = datetime.now()
      +            store._save()
      +
      +        refreshed = store.get_or_create_session(source)
      +        assert refreshed.session_id == entry.session_id
      +        assert refreshed.resume_pending

6 PRs closed without merging

From the close comments: closed without comment 2 · partial fix 2 · done elsewhere 1 · other 1

  • #46963 fix(gateway): add freshness gate to resume_pending sessions · partial fix → a1f62f4, #56264, #54442
  • #52637 fix(gateway): clear stale resume_pending markers in scheduler · closed without comment
  • #52818 fix(gateway): clear stale resume_pending markers in scheduler · closed without comment
  • #53031 fix(gateway): clear stale resume_pending markers in scheduler · other → #52818, #53031
  • #61092 fix(gateway): respect session_reset.mode and fix misleading reset notice · done elsewhere → #61071
Show 1 more
  • #66264 fix(gateway): honor the reset policy when recovering a session from state.db (#66255) · partial fix → 4ddd2d8, #61242, #61489, #62672, #63068, #65783, #66264, #71530, #68617, #68795, #61220, #66255, #68539, #374151
Closed issues · 2
  • Closed#46934Bug: stale resume_pending sessions bypass idle reset, causing context bleed after gateway restart
  • Closed#58933[Bug]: resume_pending_expired silently creates new session without notifying user or properly closing the old one
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.13: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
gateway
Merged PRs
#56264
Closed issues
2
Attempts
6 PRs closed without merging
Fix link checked
1 Jul 2026

For your agent

The same facts, typed.