diff --git a/nanobot/session/manager.py b/nanobot/session/manager.py index 1d6ae7c0..c4d39446 100644 --- a/nanobot/session/manager.py +++ b/nanobot/session/manager.py @@ -539,9 +539,10 @@ class SessionManager: logger.info("Recovered session {} from corrupt file ({} messages)", key, len(repaired.messages)) return repaired - def _repair(self, key: str) -> Session | None: + def _repair(self, key: str, *, path: Path | None = None) -> Session | None: """Attempt to recover a session from a corrupt JSONL file.""" - path = self._get_session_path(key) + if path is None: + path = self._get_session_path(key) if not path.exists(): return None @@ -893,7 +894,7 @@ class SessionManager: } ) except Exception: - repaired = self._repair(fallback_key) + repaired = self._repair(fallback_key, path=path) if repaired is not None: sessions.append( { diff --git a/tests/session/test_session_list_repair_legacy.py b/tests/session/test_session_list_repair_legacy.py new file mode 100644 index 00000000..cfd937ec --- /dev/null +++ b/tests/session/test_session_list_repair_legacy.py @@ -0,0 +1,39 @@ +"""Reproduction test: list_sessions drops corrupt legacy-stem sessions during repair.""" +import json +from datetime import datetime +from pathlib import Path +from nanobot.session.manager import SessionManager + + +def test_list_sessions_repairs_corrupt_legacy_stem(tmp_path: Path, monkeypatch) -> None: + monkeypatch.setattr( + "nanobot.session.manager.get_legacy_sessions_dir", + lambda: tmp_path / "legacy_sessions", + ) + manager = SessionManager(tmp_path / "workspace") + + # Simulate a legacy lossy-path filename (telegram_12345.jsonl) with a corrupt + # first line that triggers the repair branch in list_sessions. + legacy_stem = "telegram_12345" + corrupt_path = manager.sessions_dir / f"{legacy_stem}.jsonl" + corrupt_path.parent.mkdir(parents=True, exist_ok=True) + metadata = json.dumps({ + "_type": "metadata", + "key": "telegram:12345", + "created_at": datetime(2025, 1, 1).isoformat(), + "updated_at": datetime(2025, 1, 1).isoformat(), + }) + # Corrupt line followed by valid message + corrupt_path.write_text( + metadata + "\n{INVALID JSON LINE\n" + + json.dumps({"role": "user", "content": "recoverable message"}) + "\n", + encoding="utf-8", + ) + + sessions = manager.list_sessions() + + # BUG: repair fails because _repair re-encodes the fallback_key via + # _get_session_path, producing a base64 stem that doesn't match the + # actual legacy filename. The session is silently dropped. + assert len(sessions) == 1, f"Expected 1 session, got {len(sessions)}" + assert sessions[0]["key"] == "telegram:12345"