NousResearch/hermes-agent

Gateway compression and hygiene transcript rotation/persistence issues

Cluster 21301 · gateway. Data as of

1 open item. Fix merged in #52628 on 25 Jun 2026. Check it covers your case.

The short answer

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

Most members concern gateway session hygiene auto-compress and manual /compress, particularly temporary compression agents lacking a session_db, session rotation not happening, and transcripts being rewritten or overwritten. They include bug reports and proposed fixes (passing session_db, guarding rewrites, in-place compression), though the exact outcomes are not established here.

Open issues0

No open issues.

Open PRs1

  • PR#78401fix(gateway): hygiene compression must force-shrink over-threshold sessions

Fixes on record

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

  • #48584 fix(compression): preserve messages across session rotation (flush-before-rotate + /compress rewrite guard)

    merged to main

    files: agent/conversation_compression.py, cli.py, gateway/slash_commands.py

    Show the diff
    • agent/conversation_compression.py
      @@ -512,6 +512,16 @@ def _release_lock() -> None:
                   old_title = agent._session_db.get_session_title(agent.session_id)
                   # Trigger memory extraction on the old session before it rotates.
                   agent.commit_memory_session(messages)
      +            # Flush any un-persisted messages from the current turn to the
      +            # old session *before* rotating.  compress_context() can be
      +            # called mid-turn (auto-compress when context exceeds threshold)
      +            # at a point when _flush_messages_to_session_db() has not yet
      +            # run.  Without this, messages generated during the current turn
      +            # are silently lost on session rotation (#47202).
      +            try:
      +                agent._flush_messages_to_session_db(messages)
      +            except Exception:
      +                pass  # best-effort — don't block compression on a flush error
                   agent._session_db.end_session(agent.session_id, "compression")
                   old_session_id = agent.session_id
                   agent.session_id = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}"
    • cli.py
      @@ -5957,6 +5957,18 @@ def new_session(self, silent=False, title=None):
       
               old_session_id = self.session_id
               if self._session_db and old_session_id:
      +            # Flush any un-persisted messages from the current turn to the
      +            # old session *before* rotating.  /new can be called mid-turn
      +            # when _flush_messages_to_session_db() has not yet run — without
      +            # this, messages generated during the current turn are silently
      +            # lost on session rotation (#47202).
      +            if self.agent:
      +                try:
      +                    self.agent._flush_messages_to_session_db(
      +                        self.conversation_history
      +                    )
      +                except Exception:
      +                    pass  # best-effort
                   try:
                       self._session_db.end_session(old_session_id, "new_session")
                   except Exception:
    • gateway/slash_commands.py
      @@ -2588,14 +2588,29 @@ async def _handle_compress_command(self, event: MessageEvent) -> str:
                       # session_id for the continuation.  Write the compressed messages
                       # into the NEW session so the original history stays searchable.
                       new_session_id = tmp_agent.session_id
      -                if new_session_id != session_entry.session_id:
      +                rotated = new_session_id != session_entry.session_id
      +                if rotated:
                           session_entry.session_id = new_session_id
                           self.session_store._save()
                           self._sync_telegram_topic_binding(
                               source, session_entry, reason="compress-command",
                           )
       
      -                self.session_store.rewrite_transcript(new_session_id, compressed)
      +                # Only rewrite the transcript when rotation actually produced a
      +                # NEW session id. If _compress_context could not rotate (e.g.
      +                # _session_db unavailable, or the DB split raised), session_id
      +                # is unchanged and rewrite_transcript() would DELETE the
      +                # original messages and replace them with only the compressed
      +                # summary — permanent data loss (#44794, #39704). In that case
      +                # leave the original transcript intact.
      +                if rotated:
      +                    self.session_store.rewrite_transcript(new_session_id, compressed)
      +                else:
      +                    logger.warning(
      +                        "Manual /compress: session rotation did not occur "
      +                        "(session_id unchanged) — preserving original transcript "
      +                        "instead of overwriting it (#44794)."
      +                    )
                       # Reset stored token count — transcript changed, old value is stale
                       self.session_store.update_session(
                           session_entry.session_key, last_prompt_tokens=0
  • #49925 fix(gateway): preserve transcript when hygiene auto-compress can't rotate (#21301)

    merged to main

    files: gateway/run.py, tests/gateway/test_session_hygiene.py

  • #52628 fix(gateway): pass session_db to compress temp agents so persistence works

    merged to main

    files: gateway/run.py, gateway/slash_commands.py, tests/gateway/test_compress_command.py, tests/gateway/test_compress_focus.py, tests/gateway/test_compress_plugin_engine.py

    Show the diff
    • gateway/run.py
      @@ -9550,14 +9550,15 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g
                                           skip_memory=True,
                                           enabled_toolsets=["memory"],
                                           session_id=session_entry.session_id,
      +                                    session_db=self._session_db,
                                       )
                                       try:
                                           # The hygiene agent rotates the session
                                           # forward to a continuation id that becomes
                                           # the gateway session's live row. It must
      -                                    # never finalize on close() (today it has no
      -                                    # session_db so close() no-ops, but this
      -                                    # guards a future where one is wired in).
      +                                    # never finalize on close() — close() would
      +                                    # end the newly rotated session the gateway
      +                                    # entry now points at.
                                           _hyg_agent._end_session_on_close = False
                                           _hyg_agent._print_fn = lambda *a, **kw: None
       
    • gateway/slash_commands.py
      @@ -2818,9 +2818,14 @@ async def _handle_compress_command(self, event: MessageEvent) -> str:
                       skip_memory=True,
                       enabled_toolsets=["memory"],
                       session_id=session_entry.session_id,
      +                session_db=self._session_db,
                   )
                   try:
                       tmp_agent._print_fn = lambda *a, **kw: None
      +                # Prevent close() from ending the newly rotated session —
      +                # the gateway session entry now points at the new id and
      +                # must remain open for the next user turn.
      +                tmp_agent._end_session_on_close = False
       
                       # Estimate with system prompt + tool schemas included so the
                       # figure reflects real request pressure, not a transcript-only
    • tests/gateway/test_compress_command.py
      @@ -54,6 +54,7 @@ def _make_runner(history: list[dict[str, str]]):
           runner.session_store.rewrite_transcript = MagicMock()
           runner.session_store.update_session = MagicMock()
           runner.session_store._save = MagicMock()
      +    runner._session_db = None
           return runner
       
       
      @@ -247,3 +248,60 @@ def _estimate(messages, **_kwargs):
           assert "intact" in result
           agent_instance.shutdown_memory_provider.assert_called_once()
           agent_instance.close.assert_called_once()
      +
      +
      [redacted:email]
      +async def test_compress_command_passes_session_db_and_persists_rotated_session():
      +    """session_db must be wired into the /compress temp agent so that
      +    _compress_context can actually rotate the session and persist the
      +    compressed transcript — without it compression is a silent no-op."""
      +    history = _make_history()
      +    compressed = [
      +        history[0],
      +        {"role": "assistant", "content": "compressed summary"},
      +        history[-1],
      +    ]
      +    runner = _make_runner(history)
      +    runner._session_db = object()
      +    agent_instance = MagicMock()
      +    agent_instance.shutdown_memory_provider = MagicMock()
      +    agent_instance.close = MagicMock()
      +    agent_instance._cached_system_prompt = ""
      +    agent_instance.tools = None
      +    agent_instance.context_compressor.has_content_to_compress.return_value = True
      +    agent_instance.compression_in_place = False
      +    agent_instance.session_id = "sess-1"
      +
      +    def _compress(messages, *_args, **_kwargs):
      +        agent_instance.session_id = "sess-2"
      +        return compressed, ""
      +
      +    agent_instance._compress_context.side_effect = _compress
      +
      +    def _estimate(messages, **_kwargs):
      +        if messages == history:
      +            return 100
      +        if messages == compressed:
      +            return 60
      +        raise AssertionError(f"unexpected transcript: {messages!r}")
      +
      +    with (
      +        patch("gateway.run._resolve_runtime_agent_kwargs", return_value={"api_key": "***"}),
      +        patch("gateway.run._resolve_gateway_model", return_value="test-model"),
      +        patch("run_agent.AIAgent", return_value=agent_instance) as mock_agent_cls,
      +        patch("agent.model_metadata.estimate_request_tokens_rough", side_effect=_estimate),
      +    ):
      +        result = await runner._handle_compress_command(_make_event())
      +
      +    assert "Compressed:" in result
      +    mock_agent_cls.assert_called_once()
      +    assert mock_agent_cls.call_args.kwargs["session_db"] is runner._session_db
      +    runner.session_store._save.assert_called_once()
      +    runner.session_store.rewrite_transcript.assert_called_once_with(
      +        "sess-2", compressed
      +    )
      +    runner.session_store.update_session.assert_called_once_with(
      +        build_session_key(_make_source()), last_prompt_tokens=0
      +    )
      +    agent_instance.shutdown_memory_provider.assert_called_once()
      +    agent_instance.close.assert_called_once()
    • tests/gateway/test_compress_focus.py
      @@ -54,6 +54,7 @@ def _make_runner(history: list[dict[str, str]]):
           runner.session_store.rewrite_transcript = MagicMock()
           runner.session_store.update_session = MagicMock()
           runner.session_store._save = MagicMock()
      +    runner._session_db = None
           return runner
       
       
    • tests/gateway/test_compress_plugin_engine.py
      @@ -101,6 +101,7 @@ def _make_runner(history: list[dict[str, str]]):
           runner.session_store.rewrite_transcript = MagicMock()
           runner.session_store.update_session = MagicMock()
           runner.session_store._save = MagicMock()
      +    runner._session_db = None
           return runner
       
       
  • #52658 feat(compression): flip in_place default to True + guard fix (#38763) [salvage #51959]

    merged to main

    files: gateway/run.py, gateway/slash_commands.py, hermes_cli/config.py, scripts/release.py, tests/agent/test_compression_concurrent_fork.py, tests/agent/test_compression_logging_session_context.py, tests/agent/test_compression_rotation_state.py, tests/gateway/test_compression_concurrent_sessions.py, tests/gateway/test_session_hygiene.py, tests/run_agent/test_compression_boundary_hook.py, tests/run_agent/test_in_place_compaction.py

  • #60981 fix(gateway): compact hygiene transcripts in place (#60947)

    merged to main

    files: gateway/run.py, scripts/release.py, tests/gateway/test_session_hygiene.py

13 PRs closed without merging

From the close comments: done elsewhere 5 · merged via another change 5 · duplicate 2 · superseded by another PR 1

  • #21313 fix(gateway): preserve transcript on /compress and hygiene auto-compress · merged via another change → #49925
  • #39731 fix(gateway): prevent session hygiene compression from overwriting original transcript · done elsewhere → #52658, #50098, #38763
  • #39745 fix(gateway): pass session_db to hygiene agent for proper session rotation (#39704) · merged via another change → #49925
  • #40112 fix: guard compress rewrite_transcript on session rotation (#39704) · done elsewhere → 4c349e8
  • #41000 Fix #39704: Guard session transcript rewrite on successful rotation · done elsewhere → 4c349e8
Show 8 more
  • #44867 fix(gateway): only rewrite transcript when session rotation happened (#44794) · duplicate → #35905, #39704, #40112, #41000, #44794
  • #50935 fix(gateway): pass session_db to hygiene and manual compress agents for proper session rotation · merged via another change → #52628, #49925
  • #51624 fix(gateway): preserve session rotation for manual /compress · merged via another change → #52628
  • #60956 fix(gateway): compact hygiene transcripts in place · merged via another change → 4d611ba, #60981, #60947
  • #60964 fix(gateway): set compression_in_place on session hygiene agent · duplicate → #60956, #60947, #60328, #60963, #60964, #60965
  • #71940 fix(gateway): compact hygiene transcripts in place · done elsewhere → d6a275b
  • #78389 fix(gateway): hygiene compression must force-shrink over-threshold sessions · superseded by another PR
  • #80669 fix: wire gateway SessionDB into the hygiene compression agent (#21301) · done elsewhere → 1a38a8f, 73c8d5a, d6a275b, #21301
Closed issues · 6
  • Closed#21301[bug] /compress and gateway hygiene compression destroy original transcript — tmp_agent missing session_db kwarg
  • Closed#39704Bug: Session Hygiene compression overwrites original messages when _session_db is None
  • Closed#44794/compress deletes original messages from state.db when session rotation fails
  • Closed#51469Manual gateway /compress succeeds but no-ops because temporary agent never rotates session without session_db
  • Closed#58452Hygiene compression never persists: summary discarded when hygiene agent has no session_db
  • Closed#60947Gateway session hygiene can no-op on oversized Telegram transcripts, leaving users stuck on spinning replies
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.08: 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
#48584, #49925, #52628, #52658, #60981
Closed issues
6
Attempts
13 PRs closed without merging
Fix link checked
8 Jul 2026

For your agent

The same facts, typed.