From 0728b994d8cc421edf9aa554af32b35f389f143b Mon Sep 17 00:00:00 2001 From: RaresKeY <158580472+RaresKeY@users.noreply.github.com> Date: Sun, 16 Aug 2026 23:34:27 +0100 Subject: [PATCH] fix: discover sessions from persisted messages (#5938) * fix(session): discover sessions from persisted messages Use indexed chat-row existence instead of stale derived message_count metadata during startup discovery, then repair the bounded in-memory counts so lazy hydration remains correct. Keep truly empty sessions excluded and cover stale-low and stale-high counts with real SQLite. * test(session): isolate discovery database * test(session): use manager database metadata --- core/session_manager.py | 18 +++++- tests/test_session_discovery_message_count.py | 62 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 tests/test_session_discovery_message_count.py diff --git a/core/session_manager.py b/core/session_manager.py index 6e65226d3..eeb9c2a16 100644 --- a/core/session_manager.py +++ b/core/session_manager.py @@ -14,6 +14,8 @@ import logging from datetime import datetime, timezone, timedelta from typing import Dict, Optional +from sqlalchemy import func + from .database import Session as DbSession, ChatMessage as DbChatMessage, Document as DbDocument, SessionLocal, utcnow_naive from .models import Session, ChatMessage from src.attachment_refs import persistable_message_content @@ -92,14 +94,28 @@ class SessionManager: try: db_sessions = db.query(DbSession).filter( DbSession.archived == False, - DbSession.message_count > 0, + DbSession.messages.any(), ).order_by(DbSession.last_accessed.desc()).limit(100).all() + # message_count is derived metadata and can drift after interrupted + # or legacy writes. Count only the bounded discovery set so startup + # remains metadata-only while lazy hydration sees an authoritative + # positive count for every discovered non-empty session. + message_counts = {} + if db_sessions: + message_counts = dict( + db.query(DbChatMessage.session_id, func.count(DbChatMessage.id)) + .filter(DbChatMessage.session_id.in_([row.id for row in db_sessions])) + .group_by(DbChatMessage.session_id) + .all() + ) + loaded_count = 0 for db_session in db_sessions: try: session = self._db_to_session_meta(db_session) if session is not None: + session.message_count = message_counts[db_session.id] self.sessions[db_session.id] = session loaded_count += 1 except Exception as e: diff --git a/tests/test_session_discovery_message_count.py b/tests/test_session_discovery_message_count.py new file mode 100644 index 000000000..bc121360f --- /dev/null +++ b/tests/test_session_discovery_message_count.py @@ -0,0 +1,62 @@ +"""Real-SQLite regressions for session discovery with stale derived counts.""" + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import NullPool + +import core.session_manager as session_manager + + +def _make_manager(db_path, monkeypatch): + engine = create_engine( + f"sqlite:///{db_path}", + connect_args={"check_same_thread": False}, + poolclass=NullPool, + ) + session_manager.DbSession.metadata.create_all(bind=engine) + session_local = sessionmaker( + bind=engine, + autoflush=False, + autocommit=False, + ) + monkeypatch.setattr(session_manager, "SessionLocal", session_local) + return session_manager.SessionManager(), session_manager + + +def test_discovery_uses_persisted_rows_and_repairs_cached_count(tmp_path, monkeypatch): + from core.models import ChatMessage + + manager, session_manager = _make_manager( + tmp_path / "session-discovery.db", monkeypatch + ) + for session_id in ("stale-low", "empty", "stale-high"): + manager.create_session( + session_id=session_id, + name=session_id, + endpoint_url="http://example.invalid", + model="test-model", + rag=False, + owner="tester", + ) + manager.add_message("stale-low", ChatMessage("user", "persisted message")) + + db = session_manager.SessionLocal() + try: + db.query(session_manager.DbSession).filter( + session_manager.DbSession.id == "stale-low" + ).update({"message_count": 0}) + db.query(session_manager.DbSession).filter( + session_manager.DbSession.id == "stale-high" + ).update({"message_count": 7}) + db.commit() + finally: + db.close() + + restarted = session_manager.SessionManager() + + assert set(restarted.sessions) == {"stale-low"} + assert restarted.sessions["stale-low"].history == [] + assert restarted.sessions["stale-low"].message_count == 1 + + hydrated = restarted.get_session("stale-low") + assert [message.content for message in hydrated.history] == ["persisted message"]