diff --git a/run_agent.py b/run_agent.py index bca815da3c6..1ded29f3679 100644 --- a/run_agent.py +++ b/run_agent.py @@ -692,6 +692,24 @@ class AIAgent: reset_engine=True, ) + # Reset-only session switches (/new, /resume, /branch) update + # agent.session_id before calling reset_session_state(). The built-in + # compressor keeps durable cooldown state keyed by its bound session, + # so rebind it when the active session changed but no full start hook ran. + engine = getattr(self, "context_compressor", None) + target_session_id = getattr(self, "session_id", "") or "" + bound_session_id = getattr(engine, "_session_id", "") if engine is not None else "" + if ( + engine is not None + and hasattr(engine, "bind_session_state") + and target_session_id + and target_session_id != bound_session_id + ): + try: + engine.bind_session_state(getattr(self, "_session_db", None), target_session_id) + except Exception as exc: + logger.debug("context engine bind_session_state during reset: %s", exc) + def _ensure_lmstudio_runtime_loaded(self, config_context_length: Optional[int] = None) -> None: """ Preload the LM Studio model with at least Hermes' minimum context. diff --git a/tests/agent/test_context_engine_host_contract.py b/tests/agent/test_context_engine_host_contract.py index bb6fb4c4108..9bcde04cc90 100644 --- a/tests/agent/test_context_engine_host_contract.py +++ b/tests/agent/test_context_engine_host_contract.py @@ -28,7 +28,8 @@ from __future__ import annotations from unittest.mock import MagicMock - +from agent.context_compressor import ContextCompressor +from hermes_state import SessionDB from run_agent import AIAgent @@ -158,6 +159,43 @@ def test_reset_session_state_default_call_only_resets(): assert not engine.on_session_start.called +def test_reset_session_state_rebinds_builtin_compressor_after_session_switch(tmp_path, monkeypatch): + """Reset-only session switches must rebind durable cooldown state to the new session.""" + db = SessionDB(db_path=tmp_path / "state.db") + db.create_session("old-sid", source="cli") + db.create_session("new-sid", source="cli") + db.record_compression_failure_cooldown("old-sid", 4_000_000_000.0, "old-timeout") + + monkeypatch.setattr( + "agent.context_compressor.get_model_context_length", + lambda *_a, **_k: 100_000, + ) + compressor = ContextCompressor( + model="fake-model", + threshold_percent=0.85, + protect_first_n=2, + protect_last_n=2, + quiet_mode=True, + ) + compressor.bind_session_state(db, "old-sid") + + agent = _bare_agent() + agent._session_db = db + agent.context_compressor = compressor + agent.session_id = "new-sid" + + agent.reset_session_state() + + assert compressor._session_id == "new-sid" + assert compressor.get_active_compression_failure_cooldown() is None + assert db.get_compression_failure_cooldown("old-sid") is not None + + compressor._record_compression_failure_cooldown(30.0, "new-timeout") + + assert db.get_compression_failure_cooldown("new-sid") is not None + assert db.get_compression_failure_cooldown("old-sid")["error"] == "old-timeout" + + def test_update_from_response_forwards_canonical_cache_buckets(): """conversation_loop passes cache_read/write/reasoning tokens to engine.""" # Test the contract directly: a usage_dict built from CanonicalUsage must