diff --git a/agent/conversation_compression.py b/agent/conversation_compression.py index 1b6cca41a9..c25a0c2d4e 100644 --- a/agent/conversation_compression.py +++ b/agent/conversation_compression.py @@ -3143,6 +3143,17 @@ def _fold_todo_snapshot(agent: Any, compressed: list) -> None: def _rebuild_system_prompt_at_boundary(agent: Any, system_message: str) -> str: """Refresh tool schemas and rebuild the system prompt at the commit boundary.""" + if getattr(agent, "_retain_seeded_system_prompt", False) is True: + # Gateway hygiene / gateway /compress run a detached agent with a reduced toolset and no live + # surface: its builder output drops the skills index, external provider blocks and tool guidance, + # and the commit below would persist that over the live session's snapshot (restored verbatim by + # the next fresh agent). Keep the seeded bytes; the live agent's own compaction propagates updates. + seeded_system_prompt = agent._cached_system_prompt or "" + agent._cached_system_prompt = seeded_system_prompt + if seeded_system_prompt: + from agent.system_prompt import reconstruct_static_prefix + reconstruct_static_prefix(agent, system_message=system_message, log_label="compression seeded-prompt") + return seeded_system_prompt cached_system_prompt = agent._cached_system_prompt agent._invalidate_system_prompt() diff --git a/gateway/run.py b/gateway/run.py index d38fbd0978..78c0de2ec5 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -509,6 +509,8 @@ def _seed_hygiene_system_prompt(agent: Any, session_row: Optional[Dict[str, Any] stored_prompt = raw_prompt agent._cached_system_prompt = stored_prompt + # Compaction otherwise rebuilds the prompt at the commit boundary from this agent's reduced toolset. + agent._retain_seeded_system_prompt = True return bool(stored_prompt) diff --git a/tests/gateway/test_hygiene_compaction_keeps_seeded_prompt.py b/tests/gateway/test_hygiene_compaction_keeps_seeded_prompt.py new file mode 100644 index 0000000000..78ce8a33e9 --- /dev/null +++ b/tests/gateway/test_hygiene_compaction_keeps_seeded_prompt.py @@ -0,0 +1,64 @@ +"""Gateway hygiene / gateway /compress must not persist a reduced-toolset prompt over a live session. + +Both paths compact with a detached ``AIAgent(enabled_toolsets=["memory"])`` seeded with the session's +stored prompt (``_seed_hygiene_system_prompt``). Compaction rebuilds the prompt at the commit boundary +(#98426) and the commit persists the result, so without the seed being honoured the session row lost the +skills index / provider blocks and every later fresh agent restored those degraded bytes verbatim. +""" +from types import SimpleNamespace +from unittest.mock import patch + +from agent import conversation_compression as cc +from gateway.run import _seed_hygiene_system_prompt + +STORED = "You are Hermes.\n\n## Skills (mandatory)\n\n- a: b\n" +DEGRADED = "You are Hermes." + + +def _agent(**over): + built = [] + + def _build(system_message=None): + built.append(system_message) + return DEGRADED + + def _invalidate(): + agent._cached_system_prompt = None + + fields = dict(_cached_system_prompt=None, session_id="sess-1", + _build_system_prompt=_build, _invalidate_system_prompt=_invalidate) + fields.update(over) + agent = SimpleNamespace(**fields) + return agent, built + + +def _rebuild(agent): + with patch.object(cc, "_refresh_agent_tool_definitions", return_value=False), \ + patch("agent.system_prompt.reconstruct_static_prefix"): + return cc._rebuild_system_prompt_at_boundary(agent, "") + + +def test_seeded_hygiene_agent_keeps_the_stored_prompt_at_the_commit_boundary(): + agent, built = _agent() + assert _seed_hygiene_system_prompt(agent, {"system_prompt": STORED}) is True + + result = _rebuild(agent) + + assert result is STORED + assert agent._cached_system_prompt is STORED + assert built == [] + + +def test_seed_without_a_stored_prompt_persists_empty_not_the_reduced_build(): + agent, built = _agent() + assert _seed_hygiene_system_prompt(agent, None) is False + + assert _rebuild(agent) == "" + assert built == [] + + +def test_live_agent_compaction_still_rebuilds(): + agent, built = _agent(_cached_system_prompt=STORED) + + assert _rebuild(agent) == DEGRADED + assert built == [""]