From 76a17046e29e9738e5e90c7ae3d697af05694af3 Mon Sep 17 00:00:00 2001 From: Gille <4317663+helix4u@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:40:18 -0600 Subject: [PATCH] fix(gateway): preserve memory prompt during hygiene compression --- gateway/run.py | 47 +++++++++++++++++++++++++++ tests/gateway/test_session_hygiene.py | 18 ++++++++-- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index 37dfd9d92c..94e8f9e43a 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -85,6 +85,7 @@ _TELEGRAM_CONNECT_TIMEOUT_SECS_DEFAULT = 180.0 _ADAPTER_DISCONNECT_TIMEOUT_SECS_DEFAULT = 5.0 _GATEWAY_PROXY_SSE_BUFFER_MAX_CHARS = 16 * 1024 * 1024 _TELEGRAM_COMMAND_MENTION_RE = re.compile(r"(? bool: + """Keep gateway hygiene from rebuilding a live session's system prompt. + + The hygiene helper intentionally skips memory-provider initialization. + Compression is allowed to persist a system prompt, so letting that helper + rebuild one would strip external provider blocks from the live session. + Seed the exact persisted prompt instead. When no usable prompt can be + restored, seed an empty cache entry. Compression either preserves that + unusable value or rebuilds with the hygiene-only platform marker; the real + turn will rebuild either form with its fully initialized providers. + """ + stored_prompt = "" + if session_db is not None and session_id: + try: + session_row = session_db.get_session(session_id) + if isinstance(session_row, dict): + raw_prompt = session_row.get("system_prompt") + if isinstance(raw_prompt, str) and raw_prompt.strip(): + stored_prompt = raw_prompt + except Exception as exc: + logger.warning( + "Session hygiene could not restore the system prompt for " + "session %s: %s. Preserving an empty prompt so the live " + "turn rebuilds it with its configured providers.", + session_id, + exc, + exc_info=True, + ) + + agent._cached_system_prompt = stored_prompt + return bool(stored_prompt) + + def _is_transient_network_error(exc: BaseException) -> bool: """Return True for transient network errors safe to log + swallow. @@ -13644,6 +13682,15 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew session_id=session_entry.session_id, session_db=_hyg_session_db, ) + _seed_hygiene_system_prompt( + _hyg_agent, + _hyg_session_db, + session_entry.session_id, + ) + # If compression must rebuild instead of retaining + # the cached prompt, make the persisted result + # deliberately stale for every real gateway surface. + _hyg_agent.platform = _GATEWAY_HYGIENE_PLATFORM _hyg_cleanup_deferred = False try: # Gateway hygiene runs before the user turn diff --git a/tests/gateway/test_session_hygiene.py b/tests/gateway/test_session_hygiene.py index 9254d1b266..a586a26263 100644 --- a/tests/gateway/test_session_hygiene.py +++ b/tests/gateway/test_session_hygiene.py @@ -1094,15 +1094,26 @@ async def test_session_hygiene_forces_in_place_compaction_with_bound_session_db( fake_dotenv.load_dotenv = lambda *args, **kwargs: None monkeypatch.setitem(sys.modules, "dotenv", fake_dotenv) - fake_db = object() + stored_system_prompt = ( + "You are Hermes.\n\n" + "\n" + "Pinboard provider instructions\n" + "" + ) + fake_db = MagicMock() + fake_db.get_session.return_value = { + "system_prompt": stored_system_prompt, + } class FakeInPlaceCompressAgent: last_instance = None def __init__(self, **kwargs): self.model = kwargs.get("model") + self.platform = kwargs.get("platform") self.session_id = kwargs.get("session_id", "fake-session") self._session_db = kwargs.get("session_db") + self._cached_system_prompt = None self.compression_in_place = False self._last_compaction_in_place = False self.context_compressor = SimpleNamespace( @@ -1118,6 +1129,8 @@ async def test_session_hygiene_forces_in_place_compaction_with_bound_session_db( def _compress_context(self, messages, *_args, **_kwargs): assert self.compression_in_place is True assert self._session_db is fake_db + assert self.platform == "gateway_hygiene" + assert self._cached_system_prompt == stored_system_prompt self._last_compaction_in_place = True return ([{"role": "assistant", "content": "compressed in place"}], None) @@ -1190,6 +1203,7 @@ async def test_session_hygiene_forces_in_place_compaction_with_bound_session_db( assert result == "ok" agent = FakeInPlaceCompressAgent.last_instance assert agent is not None + fake_db.get_session.assert_called_once_with("sess-1") agent.context_compressor.bind_session_state.assert_called_once_with(fake_db, "sess-1") # In-place compaction already persisted via archive_and_compact() — # rewrite_transcript would replace_messages(active_only=False) and DELETE @@ -1429,7 +1443,7 @@ def _make_progress_runner(monkeypatch, tmp_path, agent_cls, cfg_text): monkeypatch.setitem(sys.modules, "run_agent", fake_run_agent) cfg_path = tmp_path / "config.yaml" - cfg_path.write_text(cfg_text) + cfg_path.write_text(cfg_text, encoding="utf-8") gateway_run = importlib.import_module("gateway.run") GatewayRunner = gateway_run.GatewayRunner