@@ -54,6 +54,7 @@ class DockerNestedConversationManager(ConversationManager):
54
54
docker_client : docker .DockerClient = field (default_factory = docker .from_env )
55
55
_conversation_store_class : type [ConversationStore ] | None = None
56
56
_starting_conversation_ids : set [str ] = field (default_factory = set )
57
+ _runtime_container_image : str | None = None
57
58
58
59
async def __aenter__ (self ):
59
60
# No action is required on startup for this implementation
@@ -155,6 +156,12 @@ async def _start_agent_loop(
155
156
try :
156
157
# Build the runtime container image if it is missing
157
158
await call_sync_from_async (runtime .maybe_build_runtime_container_image )
159
+ self ._runtime_container_image = runtime .runtime_container_image
160
+
161
+ # check that the container already exists...
162
+ if await self ._start_existing_container (runtime ):
163
+ self ._starting_conversation_ids .discard (sid )
164
+ return
158
165
159
166
# initialize the container but dont wait for it to start
160
167
await call_sync_from_async (runtime .init_container )
@@ -172,7 +179,7 @@ async def _start_agent_loop(
172
179
)
173
180
174
181
except Exception :
175
- self ._starting_conversation_ids .remove (sid )
182
+ self ._starting_conversation_ids .discard (sid )
176
183
raise
177
184
178
185
async def _start_conversation (
@@ -262,7 +269,7 @@ async def _start_conversation(
262
269
)
263
270
assert response .status_code == status .HTTP_200_OK
264
271
finally :
265
- self ._starting_conversation_ids .remove (sid )
272
+ self ._starting_conversation_ids .discard (sid )
266
273
267
274
async def send_to_event_stream (self , connection_id : str , data : dict ):
268
275
# Not supported - clients should connect directly to the nested server!
@@ -431,6 +438,7 @@ async def _create_runtime(self, sid: str, user_id: str | None, settings: Setting
431
438
# We need to be able to specify the nested conversation id within the nested runtime
432
439
env_vars ['ALLOW_SET_CONVERSATION_ID' ] = '1'
433
440
env_vars ['WORKSPACE_BASE' ] = f'/workspace'
441
+ env_vars ['SANDBOX_CLOSE_DELAY' ] = '0'
434
442
435
443
# Set up mounted volume for conversation directory within workspace
436
444
# TODO: Check if we are using the standard event store and file store
@@ -442,9 +450,11 @@ async def _create_runtime(self, sid: str, user_id: str | None, settings: Setting
442
450
conversation_dir = get_conversation_dir (sid , user_id )
443
451
444
452
volumes .append (
445
- f'{ config .file_store_path } /{ conversation_dir } :/root/openhands/file_store/{ conversation_dir } :rw'
453
+ f'{ config .file_store_path } /{ conversation_dir } :/root/. openhands/file_store/{ conversation_dir } :rw'
446
454
)
447
455
config .sandbox .volumes = ',' .join (volumes )
456
+ if not config .sandbox .runtime_container_image :
457
+ config .sandbox .runtime_container_image = self ._runtime_container_image
448
458
449
459
# Currently this eventstream is never used and only exists because one is required in order to create a docker runtime
450
460
event_stream = EventStream (sid , self .file_store , user_id )
@@ -464,6 +474,18 @@ async def _create_runtime(self, sid: str, user_id: str | None, settings: Setting
464
474
465
475
return runtime
466
476
477
+ async def _start_existing_container (self , runtime : DockerRuntime ) -> bool :
478
+ try :
479
+ container = self .docker_client .containers .get (runtime .container_name )
480
+ if container :
481
+ status = container .status
482
+ if status == 'exited' :
483
+ await call_sync_from_async (container .start ())
484
+ return True
485
+ return False
486
+ except docker .errors .NotFound as e :
487
+ return False
488
+
467
489
468
490
def _last_updated_at_key (conversation : ConversationMetadata ) -> float :
469
491
last_updated_at = conversation .last_updated_at
0 commit comments