diff --git a/openhands/runtime/impl/docker/docker_runtime.py b/openhands/runtime/impl/docker/docker_runtime.py index 1176b19972..74ac002096 100644 --- a/openhands/runtime/impl/docker/docker_runtime.py +++ b/openhands/runtime/impl/docker/docker_runtime.py @@ -374,28 +374,11 @@ class DockerRuntime(ActionExecutionClient): ) self.log('debug', f'Container started. Server url: {self.api_url}') self.send_status_message('STATUS$CONTAINER_STARTED') - except docker.errors.APIError as e: - if '409' in str(e): - self.log( - 'warning', - f'Container {self.container_name} already exists. Removing...', - ) - stop_all_containers(self.container_name) - return self.init_container() - - else: - self.log( - 'error', - f'Error: Instance {self.container_name} FAILED to start container!\n', - ) - self.log('error', str(e)) - raise e except Exception as e: self.log( 'error', f'Error: Instance {self.container_name} FAILED to start container!\n', ) - self.log('error', str(e)) self.close() raise e diff --git a/openhands/server/conversation_manager/docker_nested_conversation_manager.py b/openhands/server/conversation_manager/docker_nested_conversation_manager.py index db6e071389..6349edd7d9 100644 --- a/openhands/server/conversation_manager/docker_nested_conversation_manager.py +++ b/openhands/server/conversation_manager/docker_nested_conversation_manager.py @@ -54,6 +54,7 @@ class DockerNestedConversationManager(ConversationManager): docker_client: docker.DockerClient = field(default_factory=docker.from_env) _conversation_store_class: type[ConversationStore] | None = None _starting_conversation_ids: set[str] = field(default_factory=set) + _runtime_container_image: str | None = None async def __aenter__(self): # No action is required on startup for this implementation @@ -155,6 +156,12 @@ class DockerNestedConversationManager(ConversationManager): try: # Build the runtime container image if it is missing await call_sync_from_async(runtime.maybe_build_runtime_container_image) + self._runtime_container_image = runtime.runtime_container_image + + # check that the container already exists... + if await self._start_existing_container(runtime): + self._starting_conversation_ids.discard(sid) + return # initialize the container but dont wait for it to start await call_sync_from_async(runtime.init_container) @@ -172,7 +179,7 @@ class DockerNestedConversationManager(ConversationManager): ) except Exception: - self._starting_conversation_ids.remove(sid) + self._starting_conversation_ids.discard(sid) raise async def _start_conversation( @@ -262,7 +269,7 @@ class DockerNestedConversationManager(ConversationManager): ) assert response.status_code == status.HTTP_200_OK finally: - self._starting_conversation_ids.remove(sid) + self._starting_conversation_ids.discard(sid) async def send_to_event_stream(self, connection_id: str, data: dict): # Not supported - clients should connect directly to the nested server! @@ -431,6 +438,7 @@ class DockerNestedConversationManager(ConversationManager): # We need to be able to specify the nested conversation id within the nested runtime env_vars['ALLOW_SET_CONVERSATION_ID'] = '1' env_vars['WORKSPACE_BASE'] = f'/workspace' + env_vars['SANDBOX_CLOSE_DELAY'] = '0' # Set up mounted volume for conversation directory within workspace # TODO: Check if we are using the standard event store and file store @@ -442,9 +450,11 @@ class DockerNestedConversationManager(ConversationManager): conversation_dir = get_conversation_dir(sid, user_id) volumes.append( - f'{config.file_store_path}/{conversation_dir}:/root/openhands/file_store/{conversation_dir}:rw' + f'{config.file_store_path}/{conversation_dir}:/root/.openhands/file_store/{conversation_dir}:rw' ) config.sandbox.volumes = ','.join(volumes) + if not config.sandbox.runtime_container_image: + config.sandbox.runtime_container_image = self._runtime_container_image # Currently this eventstream is never used and only exists because one is required in order to create a docker runtime event_stream = EventStream(sid, self.file_store, user_id) @@ -464,6 +474,18 @@ class DockerNestedConversationManager(ConversationManager): return runtime + async def _start_existing_container(self, runtime: DockerRuntime) -> bool: + try: + container = self.docker_client.containers.get(runtime.container_name) + if container: + status = container.status + if status == 'exited': + await call_sync_from_async(container.start()) + return True + return False + except docker.errors.NotFound as e: + return False + def _last_updated_at_key(conversation: ConversationMetadata) -> float: last_updated_at = conversation.last_updated_at diff --git a/openhands/server/conversation_manager/standalone_conversation_manager.py b/openhands/server/conversation_manager/standalone_conversation_manager.py index 09649c9125..e547f8e5c8 100644 --- a/openhands/server/conversation_manager/standalone_conversation_manager.py +++ b/openhands/server/conversation_manager/standalone_conversation_manager.py @@ -154,6 +154,10 @@ class StandaloneConversationManager(ConversationManager): await conversation.disconnect() self._detached_conversations.pop(sid, None) + # Implies disconnected sandboxes stay open indefinitely + if not self.config.sandbox.close_delay: + return + close_threshold = time.time() - self.config.sandbox.close_delay running_loops = list(self._local_agent_loops_by_sid.items()) running_loops.sort(key=lambda item: item[1].last_active_ts) diff --git a/openhands/server/utils.py b/openhands/server/utils.py index f977649806..0bdb94050a 100644 --- a/openhands/server/utils.py +++ b/openhands/server/utils.py @@ -1,7 +1,7 @@ from fastapi import Depends, Request from openhands.server.shared import ConversationStoreImpl, config, conversation_manager -from openhands.server.user_auth import get_user_auth, get_user_id +from openhands.server.user_auth import get_user_id from openhands.storage.conversation.conversation_store import ConversationStore @@ -11,8 +11,7 @@ async def get_conversation_store(request: Request) -> ConversationStore | None: ) if conversation_store: return conversation_store - user_auth = await get_user_auth(request) - user_id = await user_auth.get_user_id() + user_id = get_user_id(request) conversation_store = await ConversationStoreImpl.get_instance(config, user_id) request.state.conversation_store = conversation_store return conversation_store