diff --git a/nanobot/session/manager.py b/nanobot/session/manager.py index bb78e65f..a7919674 100644 --- a/nanobot/session/manager.py +++ b/nanobot/session/manager.py @@ -986,6 +986,8 @@ class SessionManager: "path": str(path), } ) + except FileNotFoundError: + continue except _SESSION_DATA_ERRORS: repaired = self._repair(fallback_key, path=path) if repaired is not None: diff --git a/tests/agent/test_auto_compact.py b/tests/agent/test_auto_compact.py index 35d96186..ce0f2205 100644 --- a/tests/agent/test_auto_compact.py +++ b/tests/agent/test_auto_compact.py @@ -781,6 +781,40 @@ class TestProactiveAutoCompact: ) await _drain_background_tasks(loop) + @pytest.mark.asyncio + async def test_idle_tick_survives_session_removed_during_listing( + self, + tmp_path, + monkeypatch, + ): + loop = _make_loop(tmp_path, session_ttl_minutes=15) + removed = loop.sessions.get_or_create("cli:removed") + removed.add_message("user", "remove me") + loop.sessions.save(removed) + healthy = loop.sessions.get_or_create("cli:healthy") + healthy.add_message("user", "keep me") + loop.sessions.save(healthy) + + removed_path = loop.sessions._get_session_path(removed.key) + original_open = open + + def remove_before_open(path, *args, **kwargs): + if Path(path) == removed_path: + removed_path.unlink() + return original_open(path, *args, **kwargs) + + monkeypatch.setattr("builtins.open", remove_before_open) + + async def idle_once(): + loop._running = False + raise asyncio.TimeoutError + + monkeypatch.setattr(loop.bus, "consume_inbound", idle_once) + + await loop.run() + + assert [row["key"] for row in loop.sessions.list_sessions()] == ["cli:healthy"] + @pytest.mark.asyncio async def test_no_check_when_ttl_disabled(self, tmp_path): """check_expired should be a no-op when TTL is 0."""