From be216bd766634ffd2579d2178971f4529eb3380a Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Fri, 4 Sep 2026 19:20:44 +0100 Subject: [PATCH 1/2] fix(sessions): reset compaction state on clear --- .../openai_responses_compaction_session.py | 2 ++ ...est_openai_responses_compaction_session.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/agents/memory/openai_responses_compaction_session.py b/src/agents/memory/openai_responses_compaction_session.py index fed2d89ff4..b113280527 100644 --- a/src/agents/memory/openai_responses_compaction_session.py +++ b/src/agents/memory/openai_responses_compaction_session.py @@ -516,7 +516,9 @@ async def clear_session(self) -> None: raise self._compaction_candidate_items = [] self._session_items = [] + self._response_id = None self._deferred_response_id = None + self._last_unstored_response_id = None self._mutation_generation += 1 async def _ensure_compaction_candidates( diff --git a/tests/memory/test_openai_responses_compaction_session.py b/tests/memory/test_openai_responses_compaction_session.py index 7f8895a31c..c940e3a86e 100644 --- a/tests/memory/test_openai_responses_compaction_session.py +++ b/tests/memory/test_openai_responses_compaction_session.py @@ -100,6 +100,25 @@ def test_excludes_easy_user_messages_without_type(self) -> None: class TestOpenAIResponsesCompactionSession: + @pytest.mark.asyncio + async def test_clear_session_forgets_response_chain_state(self) -> None: + mock_session = self.create_mock_session() + mock_client = MagicMock() + mock_client.responses.compact = AsyncMock() + session = OpenAIResponsesCompactionSession( + session_id="test", + underlying_session=mock_session, + client=mock_client, + should_trigger_compaction=lambda _context: False, + ) + + await session.run_compaction({"response_id": "resp-old", "store": True}) + await session.clear_session() + + with pytest.raises(ValueError, match="requires a response_id"): + await session.run_compaction({"force": True, "compaction_mode": "previous_response_id"}) + mock_client.responses.compact.assert_not_called() + def test_client_preserves_falsy_default_client(self) -> None: mock_client = MagicMock() mock_client.__bool__.return_value = False From 2c2ecaadf5921a774829e9d8d8207458d7e51c9f Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Tue, 8 Sep 2026 18:23:39 +0100 Subject: [PATCH 2/2] fix(sessions): reset chain state on cancelled clear --- .../openai_responses_compaction_session.py | 14 +++++++++- ...est_openai_responses_compaction_session.py | 26 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/agents/memory/openai_responses_compaction_session.py b/src/agents/memory/openai_responses_compaction_session.py index b113280527..f716b5be10 100644 --- a/src/agents/memory/openai_responses_compaction_session.py +++ b/src/agents/memory/openai_responses_compaction_session.py @@ -508,7 +508,19 @@ async def clear_session(self) -> None: async with self._mutation_lock: try: await self.underlying_session.clear_session() - except (Exception, asyncio.CancelledError): + except asyncio.CancelledError: + # Some session backends shield mutations so a cancellation can be + # observed only after the clear has committed. Treat cancellation as + # potentially destructive and forget the response chain as well as + # cached history, so cleared history cannot be reconstructed later. + self._compaction_candidate_items = None + self._session_items = None + self._response_id = None + self._deferred_response_id = None + self._last_unstored_response_id = None + self._mutation_generation += 1 + raise + except Exception: self._compaction_candidate_items = None self._session_items = None self._deferred_response_id = None diff --git a/tests/memory/test_openai_responses_compaction_session.py b/tests/memory/test_openai_responses_compaction_session.py index c940e3a86e..cf13aad57c 100644 --- a/tests/memory/test_openai_responses_compaction_session.py +++ b/tests/memory/test_openai_responses_compaction_session.py @@ -119,6 +119,32 @@ async def test_clear_session_forgets_response_chain_state(self) -> None: await session.run_compaction({"force": True, "compaction_mode": "previous_response_id"}) mock_client.responses.compact.assert_not_called() + @pytest.mark.asyncio + async def test_cancelled_clear_forgets_response_chain_state(self) -> None: + class CancelAfterCommittedClearSession(SimpleListSession): + async def clear_session(self) -> None: + await super().clear_session() + raise asyncio.CancelledError() + + underlying = CancelAfterCommittedClearSession() + mock_client = MagicMock() + mock_client.responses.compact = AsyncMock() + session = OpenAIResponsesCompactionSession( + session_id="test", + underlying_session=underlying, + client=mock_client, + should_trigger_compaction=lambda _context: False, + ) + + await session.run_compaction({"response_id": "resp-old", "store": True}) + with pytest.raises(asyncio.CancelledError): + await session.clear_session() + + assert await underlying.get_items() == [] + with pytest.raises(ValueError, match="requires a response_id"): + await session.run_compaction({"force": True, "compaction_mode": "previous_response_id"}) + mock_client.responses.compact.assert_not_called() + def test_client_preserves_falsy_default_client(self) -> None: mock_client = MagicMock() mock_client.__bool__.return_value = False