Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/agents/memory/openai_responses_compaction_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,15 +508,29 @@ 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
self._mutation_generation += 1
raise
self._compaction_candidate_items = []
self._session_items = []
self._response_id = None
self._deferred_response_id = None
self._last_unstored_response_id = None
Comment thread
sylvesterkaczmarek marked this conversation as resolved.
self._mutation_generation += 1

async def _ensure_compaction_candidates(
Expand Down
45 changes: 45 additions & 0 deletions tests/memory/test_openai_responses_compaction_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,51 @@ 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()

@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
Expand Down