fix(gateway): end finalized expired sessions as reset

This commit is contained in:
saitsuki 2026-07-12 16:22:47 +08:00 committed by Teknium
parent 78b9d98d76
commit e701cdc86e
2 changed files with 80 additions and 0 deletions

View file

@ -1582,6 +1582,19 @@ class SessionStore:
"Session DB expiry_finalized write failed for %s: %s",
entry.session_id, exc,
)
try:
# Expiry finalization is a real conversation boundary. Without
# a durable ``session_reset`` end_reason, later agent cleanup can
# close the row as ``agent_close``; stale-route recovery treats
# that as resumable and resurrects the expired full history.
# Reopen first because SessionDB.end_session is first-writer-wins.
self._db.reopen_session(entry.session_id)
self._db.end_session(entry.session_id, "session_reset")
except Exception as exc:
logger.debug(
"Session DB end_session(session_reset) failed for %s: %s",
entry.session_id, exc,
)
def _is_session_expired(self, entry: SessionEntry) -> bool:
"""Check if a session has expired based on its reset policy.

View file

@ -0,0 +1,67 @@
"""Session expiry finalization closes sessions as session_reset.
Regression coverage for #61220: the expiry watcher marks a session expired,
then agent cleanup can close it as ``agent_close``. Stale routing recovery treats
``agent_close`` as recoverable, so expired sessions were reopened with full
history unless expiry finalization also persisted the real conversation boundary
as ``end_reason='session_reset'``.
"""
from datetime import datetime, timedelta
from unittest.mock import MagicMock, patch
from gateway.config import GatewayConfig, Platform, SessionResetPolicy
from gateway.session import SessionEntry, SessionStore
def _make_store_with_db(tmp_path, db_mock) -> SessionStore:
config = GatewayConfig(default_reset_policy=SessionResetPolicy(mode="daily"))
with patch("gateway.session.SessionStore._ensure_loaded"):
store = SessionStore(sessions_dir=tmp_path, config=config)
store._db = db_mock
store._loaded = True
return store
def _entry(session_id: str = "sid-expired") -> SessionEntry:
now = datetime.now()
return SessionEntry(
session_key="agent:main:telegram:dm:8494508720",
session_id=session_id,
created_at=now - timedelta(days=1),
updated_at=now - timedelta(days=1),
platform=Platform.TELEGRAM,
chat_type="dm",
model_override={"provider": "openrouter", "model": "test/model"},
)
def test_set_expiry_finalized_persists_session_reset_boundary(tmp_path):
db = MagicMock()
db.set_expiry_finalized.return_value = None
db.reopen_session.return_value = None
db.end_session.return_value = None
store = _make_store_with_db(tmp_path, db)
entry = _entry()
store.set_expiry_finalized(entry)
assert entry.expiry_finalized is True
assert entry.model_override is None
db.set_expiry_finalized.assert_called_once_with("sid-expired", True)
db.reopen_session.assert_called_once_with("sid-expired")
db.end_session.assert_called_once_with("sid-expired", "session_reset")
def test_set_expiry_finalized_still_sets_flag_if_end_session_fails(tmp_path):
db = MagicMock()
db.set_expiry_finalized.return_value = None
db.reopen_session.side_effect = RuntimeError("database locked")
store = _make_store_with_db(tmp_path, db)
entry = _entry()
store.set_expiry_finalized(entry)
assert entry.expiry_finalized is True
db.set_expiry_finalized.assert_called_once_with("sid-expired", True)
db.end_session.assert_not_called()