Skip to content
Closed
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
38 changes: 27 additions & 11 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -9323,17 +9323,33 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g
source, session_entry,
reason="hygiene-compression",
)

self.session_store.rewrite_transcript(
session_entry.session_id, _compressed
)
# Reset stored token count — transcript was rewritten
session_entry.last_prompt_tokens = 0
history = _compressed
_new_count = len(_compressed)
_new_tokens = estimate_messages_tokens_rough(
_compressed
)
# Write compressed messages to the NEW session
self.session_store.rewrite_transcript(
session_entry.session_id, _compressed
)
# Reset stored token count — transcript was rewritten
session_entry.last_prompt_tokens = 0
history = _compressed
_new_count = len(_compressed)
_new_tokens = estimate_messages_tokens_rough(
_compressed
)
else:
# Session rotation did not occur (e.g., _session_db unavailable).
# Do NOT rewrite the original session transcript — that would
# permanently delete the original messages. Instead, keep the
# compressed messages in memory for this API call only.
logger.warning(
"Session hygiene: compression triggered but session rotation "
"did not occur (_session_db unavailable?) — skipping transcript "
"rewrite to preserve original %s messages",
_msg_count,
)
history = _compressed
_new_count = len(_compressed)
_new_tokens = estimate_messages_tokens_rough(
_compressed
)

logger.info(
"Session hygiene: compressed %s → %s msgs, "
Expand Down
112 changes: 112 additions & 0 deletions tests/gateway/test_session_hygiene_rotation_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""Tests for issue #39704: Session Hygiene compression overwrites original messages.

When Gateway Session Hygiene triggers compression via _compress_context, the
original session's messages should be preserved when session rotation fails
(e.g., when _session_db is None). This test verifies the fix that guards
rewrite_transcript on successful session rotation.
"""
import logging
from datetime import datetime
from unittest.mock import MagicMock, patch
import pytest

logger = logging.getLogger(__name__)


def test_rewrite_transcript_only_after_successful_rotation():
"""rewrite_transcript should only be called if session rotation succeeded."""
# Simulate the problematic flow:
# 1. _hyg_agent has _session_db = None
# 2. compress_context() is called (skips rotation because _session_db is None)
# 3. rewrite_transcript should NOT be called (to preserve original messages)

# Original session ID remains unchanged when _session_db is None
original_session_id = "session_20260605_abc123"
new_session_id = original_session_id # Unchanged because rotation skipped

# This is the condition in the code:
if new_session_id != original_session_id:
# Session rotation succeeded - ONLY HERE should rewrite_transcript be called
call_rewrite_transcript = True
else:
# Session rotation failed/skipped - do NOT call rewrite_transcript
call_rewrite_transcript = False

# Verify the condition evaluates correctly
assert call_rewrite_transcript is False, \
"When session_id doesn't change, rewrite_transcript should not be called"


def test_rewrite_transcript_called_after_rotation():
"""rewrite_transcript should be called when session rotation succeeds."""
# When _session_db is available, compress_context creates a new session
original_session_id = "session_20260605_abc123"
new_session_id = "20260605_120000_def789" # Changed - rotation succeeded

# This is the condition in the code:
if new_session_id != original_session_id:
# Session rotation succeeded - call rewrite_transcript
call_rewrite_transcript = True
else:
# Session rotation failed/skipped - don't call rewrite_transcript
call_rewrite_transcript = False

# Verify the condition evaluates correctly
assert call_rewrite_transcript is True, \
"When session_id changes, rewrite_transcript should be called"


def test_session_rotation_skipped_when_session_db_none():
"""compress_context skips rotation when _session_db is None."""
# Simulate compress_context behavior
class FakeAgent:
def __init__(self, session_db_value=None):
self._session_db = session_db_value
self.session_id = "original_session"

# Case 1: _session_db is None (rotation should be skipped)
agent = FakeAgent(session_db_value=None)
original_id = agent.session_id

# This is what compress_context does (line 501):
if agent._session_db:
# This block is skipped when _session_db is None
agent.session_id = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_new"

# Verify session_id was NOT changed
assert agent.session_id == original_id, \
"Session ID should not change when _session_db is None"


def test_original_messages_preserved_when_rotation_fails():
"""Original session messages should be preserved when rotation fails."""
# Mock setup
session_entry = MagicMock()
session_entry.session_id = "original_session_20260605_abc123"

session_store = MagicMock()
compressed_messages = [MagicMock()] # Simulated ~7 compressed messages

# Simulate the hygiene code
hyg_new_sid = session_entry.session_id # Unchanged (rotation failed)

# Guard rewrite_transcript on successful rotation
if hyg_new_sid != session_entry.session_id:
# Rotation succeeded - update and rewrite
session_entry.session_id = hyg_new_sid
session_store.rewrite_transcript(session_entry.session_id, compressed_messages)
else:
# Rotation failed - preserve original messages
logger.warning(
"Hygiene compression: session rotation did not occur "
"(session_db unavailable?) — skipping transcript rewrite "
"to preserve original messages"
)

# Verify rewrite_transcript was NOT called
session_store.rewrite_transcript.assert_not_called(), \
"rewrite_transcript should not be called when rotation fails"


if __name__ == "__main__":
pytest.main([__file__, "-v"])