diff --git a/run_agent.py b/run_agent.py index 02803890a..793ddd675 100644 --- a/run_agent.py +++ b/run_agent.py @@ -66,7 +66,7 @@ from model_tools import ( handle_function_call, check_toolset_requirements, ) -from tools.terminal_tool import cleanup_vm, get_active_env +from tools.terminal_tool import cleanup_vm, get_active_env, is_persistent_env from tools.tool_result_storage import maybe_persist_tool_result, enforce_turn_budget from tools.interrupt import set_interrupt as _set_interrupt from tools.browser_tool import cleanup_browser @@ -1695,9 +1695,25 @@ class AIAgent: return None def _cleanup_task_resources(self, task_id: str) -> None: - """Clean up VM and browser resources for a given task.""" + """Clean up VM and browser resources for a given task. + + Skips ``cleanup_vm`` when the active terminal environment is marked + persistent (``persistent_filesystem=True``) so that long-lived sandbox + containers survive between turns. The idle reaper in + ``terminal_tool._cleanup_inactive_envs`` still tears them down once + ``terminal.lifetime_seconds`` is exceeded. Non-persistent backends are + torn down per-turn as before to prevent resource leakage (the original + intent of this hook for the Morph backend, see commit fbd3a2fd). + """ try: - cleanup_vm(task_id) + if is_persistent_env(task_id): + if self.verbose_logging: + logging.debug( + f"Skipping per-turn cleanup_vm for persistent env {task_id}; " + f"idle reaper will handle it." + ) + else: + cleanup_vm(task_id) except Exception as e: if self.verbose_logging: logging.warning(f"Failed to cleanup VM for task {task_id}: {e}") diff --git a/tools/terminal_tool.py b/tools/terminal_tool.py index 243127a29..183e89833 100644 --- a/tools/terminal_tool.py +++ b/tools/terminal_tool.py @@ -814,6 +814,23 @@ def get_active_env(task_id: str): return _active_environments.get(task_id) +def is_persistent_env(task_id: str) -> bool: + """Return True if the active environment for task_id is configured for + cross-turn persistence (``persistent_filesystem=True``). + + Used by the agent loop to skip per-turn teardown for backends whose whole + point is to survive between turns (docker with ``container_persistent``, + daytona, modal, etc.). Non-persistent backends (e.g. Morph) still get torn + down at end-of-turn to prevent leakage. The idle reaper + (``_cleanup_inactive_envs``) handles persistent envs once they exceed + ``terminal.lifetime_seconds``. + """ + env = get_active_env(task_id) + if env is None: + return False + return bool(getattr(env, "_persistent", False)) + + def get_active_environments_info() -> Dict[str, Any]: """Get information about currently active environments.""" info = {