diff --git a/nanobot/agent/tools/exec_session.py b/nanobot/agent/tools/exec_session.py index c00c2fd2..1813a04b 100644 --- a/nanobot/agent/tools/exec_session.py +++ b/nanobot/agent/tools/exec_session.py @@ -370,8 +370,9 @@ class ExecSessionManager: if now - session.last_access > self.idle_timeout ] for session_id in stale: - session = self._sessions.pop(session_id) + session = self._sessions[session_id] await session.kill() + self._sessions.pop(session_id, None) async def _spawn( self, diff --git a/tests/tools/test_exec_session_tools.py b/tests/tools/test_exec_session_tools.py index 148baf5b..b0fe2fee 100644 --- a/tests/tools/test_exec_session_tools.py +++ b/tests/tools/test_exec_session_tools.py @@ -702,6 +702,30 @@ def test_terminate_by_owner_retains_failed_sessions(): asyncio.run(run()) +def test_stale_cleanup_retains_session_when_kill_fails(): + async def run() -> None: + manager = ExecSessionManager(idle_timeout=1) + session = SimpleNamespace( + session_id="stale-failed", + owner_session_key="cli:a", + last_access=time.monotonic() - 10, + kill=AsyncMock(side_effect=OSError("termination failed")), + ) + manager._sessions[session.session_id] = session + + with pytest.raises(OSError, match="termination failed"): + await manager.list(owner_session_key="cli:a") + + assert manager._sessions == {session.session_id: session} + session.kill.assert_awaited_once() + + session.kill.side_effect = None + assert await manager.list(owner_session_key="cli:a") == [] + assert manager._sessions == {} + + asyncio.run(run()) + + def test_terminate_by_owner_skips_sessions_without_owner_key(tmp_path): async def run() -> None: manager = ExecSessionManager()