From 510bf40705fbab44cd3e9dc55dc7bdc68c062838 Mon Sep 17 00:00:00 2001 From: srojk34 Date: Wed, 24 Jun 2026 20:21:28 +0530 Subject: [PATCH] fix(gateway): read compaction result flag not config flag in hygiene guard (#50098) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Salvage of #50098 by @srojk34, cherry-picked onto current main. The hygiene auto-compress guard and the /compress slash command both read compression_in_place (config flag — is in-place mode enabled?) instead of _last_compaction_in_place (result flag — did in-place compaction actually succeed?). Both agents are built without a session_db, so archive_and_compact always fails silently and _last_compaction_in_place stays False. Reading the config flag makes the guard think in-place succeeded, triggering rewrite_transcript() which replaces the original messages with only the compressed summary — permanent data loss. Co-authored-by: srojk34 --- gateway/run.py | 2 +- gateway/slash_commands.py | 2 +- tests/gateway/test_session_hygiene.py | 99 +++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index b1ee482b794..62f6874e3e1 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -9578,7 +9578,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew _hyg_new_sid = _hyg_agent.session_id _hyg_rotated = _hyg_new_sid != session_entry.session_id _hyg_in_place = bool( - getattr(_hyg_agent, "compression_in_place", False) + getattr(_hyg_agent, "_last_compaction_in_place", False) ) if _hyg_rotated: session_entry.session_id = _hyg_new_sid diff --git a/gateway/slash_commands.py b/gateway/slash_commands.py index e0b655c0a7d..24754336b36 100644 --- a/gateway/slash_commands.py +++ b/gateway/slash_commands.py @@ -2859,7 +2859,7 @@ class GatewaySlashCommandsMixin: # transcript replaced with the compacted set). new_session_id = tmp_agent.session_id rotated = new_session_id != session_entry.session_id - _in_place = bool(getattr(tmp_agent, "compression_in_place", False)) + _in_place = bool(getattr(tmp_agent, "_last_compaction_in_place", False)) if rotated: session_entry.session_id = new_session_id self.session_store._save() diff --git a/tests/gateway/test_session_hygiene.py b/tests/gateway/test_session_hygiene.py index e4bb9092db0..ccec24db53c 100644 --- a/tests/gateway/test_session_hygiene.py +++ b/tests/gateway/test_session_hygiene.py @@ -494,6 +494,105 @@ async def test_session_hygiene_preserves_transcript_when_no_rotation(monkeypatch runner.session_store.rewrite_transcript.assert_not_called() +@pytest.mark.asyncio +async def test_session_hygiene_preserves_transcript_when_in_place_configured_but_no_db(monkeypatch, tmp_path): + """Regression: when compression.in_place is True but the hygiene agent has + no session_db, archive_and_compact cannot run — _last_compaction_in_place + stays False. The guard must read the *result* flag, not the *config* flag, + otherwise the transcript is unconditionally rewritten with only the summary + (permanent data loss identical to #21301).""" + fake_dotenv = types.ModuleType("dotenv") + fake_dotenv.load_dotenv = lambda *args, **kwargs: None + monkeypatch.setitem(sys.modules, "dotenv", fake_dotenv) + + class InPlaceConfiguredAgent: + last_instance = None + + def __init__(self, **kwargs): + self.model = kwargs.get("model") + self.session_id = kwargs.get("session_id", "fake-session") + self.compression_in_place = True + self._last_compaction_in_place = False + self._print_fn = None + self.shutdown_memory_provider = MagicMock() + self.close = MagicMock() + type(self).last_instance = self + + def _compress_context(self, messages, *_args, **_kwargs): + return ([{"role": "assistant", "content": "summary only"}], None) + + fake_run_agent = types.ModuleType("run_agent") + fake_run_agent.AIAgent = InPlaceConfiguredAgent + monkeypatch.setitem(sys.modules, "run_agent", fake_run_agent) + + gateway_run = importlib.import_module("gateway.run") + GatewayRunner = gateway_run.GatewayRunner + + adapter = HygieneCaptureAdapter() + runner = object.__new__(GatewayRunner) + runner.config = GatewayConfig( + platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="fake-token")} + ) + runner.adapters = {Platform.TELEGRAM: adapter} + runner._voice_mode = {} + runner.hooks = SimpleNamespace(emit=AsyncMock(), loaded_hooks=False) + runner.session_store = MagicMock() + runner.session_store.get_or_create_session.return_value = SessionEntry( + session_key="agent:main:telegram:group:-1001:17585", + session_id="sess-1", + created_at=datetime.now(), + updated_at=datetime.now(), + platform=Platform.TELEGRAM, + chat_type="group", + ) + runner.session_store.load_transcript.return_value = _make_history(6, content_size=400) + runner.session_store.has_any_sessions.return_value = True + runner.session_store.rewrite_transcript = MagicMock() + runner.session_store.append_to_transcript = MagicMock() + runner._running_agents = {} + runner._pending_messages = {} + runner._pending_approvals = {} + runner._session_db = None + runner._is_user_authorized = lambda _source: True + runner._set_session_env = lambda _context: None + runner._run_agent = AsyncMock( + return_value={ + "final_response": "ok", + "messages": [], + "tools": [], + "history_offset": 0, + "last_prompt_tokens": 0, + } + ) + + monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path) + monkeypatch.setattr(gateway_run, "_resolve_runtime_agent_kwargs", lambda: {"api_key": "fake"}) + monkeypatch.setattr( + "agent.model_metadata.get_model_context_length", + lambda *_args, **_kwargs: 100, + ) + monkeypatch.setenv("TELEGRAM_HOME_CHANNEL", "795544298") + + event = MessageEvent( + text="hello", + source=SessionSource( + platform=Platform.TELEGRAM, + chat_id="-1001", + chat_type="group", + thread_id="17585", + user_id="12345", + ), + message_id="1", + ) + + result = await runner._handle_message(event) + + assert result == "ok" + # The config says in_place=True, but the DB write failed (no session_db) + # so _last_compaction_in_place is False. Transcript must NOT be rewritten. + runner.session_store.rewrite_transcript.assert_not_called() + + @pytest.mark.asyncio async def test_session_hygiene_warns_user_when_compression_aborts(monkeypatch, tmp_path): """When auxiliary compression's summary LLM call fails, the compressor