diff --git a/agent/conversation_compression.py b/agent/conversation_compression.py index 06257ffd2e7..164b494fc57 100644 --- a/agent/conversation_compression.py +++ b/agent/conversation_compression.py @@ -513,6 +513,21 @@ def compress_context( set_current_session_id(agent.session_id) except Exception: os.environ["HERMES_SESSION_ID"] = agent.session_id + # The gateway/tools session context (ContextVar + env) and the + # logging session context are SEPARATE mechanisms. The call above + # moves the former; the ``[session_id]`` tag on log lines comes + # from ``hermes_logging._session_context`` (set once per turn in + # conversation_loop.py). Without this, post-rotation log lines in + # the same turn keep the STALE old id while the message/DB/gateway + # state carry the new one — breaking log correlation exactly at the + # compaction boundary (see #34089). Guarded separately so a logging + # failure can never regress the routing update above. + try: + from hermes_logging import set_session_context + + set_session_context(agent.session_id) + except Exception: + pass agent._session_db_created = False agent._session_db.create_session( session_id=agent.session_id, diff --git a/tests/agent/test_compression_logging_session_context.py b/tests/agent/test_compression_logging_session_context.py new file mode 100644 index 00000000000..c67ffc1fde2 --- /dev/null +++ b/tests/agent/test_compression_logging_session_context.py @@ -0,0 +1,80 @@ +"""Regression: compaction must move the LOGGING session context with the id. + +When ``compress_context`` rotates ``agent.session_id`` it updates the +gateway/tools session context (``gateway.session_context.set_current_session_id``, +which moves ``HERMES_SESSION_ID`` env + ContextVar). The ``[session_id]`` tag on +log lines comes from a SEPARATE mechanism — ``hermes_logging._session_context`` +(a threading.local read by the global LogRecord factory), set once per turn in +``conversation_loop.py``. Before the fix, the rotation block never updated it, so +log lines emitted after a mid-turn compaction carried the STALE old id while the +message body / session DB / gateway state carried the new one (see #34089). This +asserts the logging context follows the rotation. +""" + +from __future__ import annotations + +import os +from pathlib import Path +from unittest.mock import MagicMock, patch + +import hermes_logging +from hermes_state import SessionDB + + +def _build_agent_with_db(db: SessionDB, session_id: str): + """Mirror tests/agent/test_compression_concurrent_fork.py's harness.""" + with patch.dict(os.environ, {"OPENROUTER_API_KEY": "test-key"}): + from run_agent import AIAgent + + agent = AIAgent( + api_key="test-key", + base_url="https://openrouter.ai/api/v1", + model="test/model", + quiet_mode=True, + session_db=db, + session_id=session_id, + skip_context_files=True, + skip_memory=True, + ) + + compressor = MagicMock() + compressor.compress.return_value = [ + {"role": "user", "content": "[CONTEXT COMPACTION] summary"}, + {"role": "user", "content": "tail"}, + ] + compressor.compression_count = 1 + compressor.last_prompt_tokens = 0 + compressor.last_completion_tokens = 0 + compressor._last_summary_error = None + compressor._last_compress_aborted = False + compressor._last_aux_model_failure_model = None + compressor._last_aux_model_failure_error = None + agent.context_compressor = compressor + return agent + + +def test_logging_session_context_follows_compression_rotation(tmp_path: Path) -> None: + db = SessionDB(db_path=tmp_path / "state.db") + parent_sid = "PARENT_LOGCTX_SESSION" + db.create_session(parent_sid, source="cli") + + agent = _build_agent_with_db(db, parent_sid) + + # conversation_loop.py pins the logging tag to the ORIGINAL id at turn start. + hermes_logging.set_session_context(parent_sid) + try: + messages = [{"role": "user", "content": f"m{i}"} for i in range(20)] + agent._compress_context(messages, "sys", approx_tokens=120_000) + + # The id actually rotated (sanity — otherwise the assertion is vacuous). + assert agent.session_id != parent_sid + + # The logging context must now match the NEW id, not the stale one. + current = getattr(hermes_logging._session_context, "session_id", None) + assert current == agent.session_id, ( + "Logging session context did not follow the compaction rotation: " + f"log tag still {current!r}, agent.session_id is {agent.session_id!r} " + "(see #34089)." + ) + finally: + hermes_logging.clear_session_context()