{"repo":"NousResearch/hermes-agent","public_id":"6369","display_label":"Terminal sandbox persistence across conversation turns","summary":"These issues and PRs concern persistent terminal sandboxes (docker/daytona/modal) being torn down between turns despite configuration indicating they should survive. A fix is proposed to make sandbox environments persist across turns as intended.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":6412,"merged_at":"2026-04-09T04:31:58+00:00"},"resolution":{"state":"resolved","by":"pr-6412"},"freshness":{"state":"stale","last_verified_at":"2026-04-09T04:31:58+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":6369,"pr_number":6412,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-04-09T04:31:58+00:00"}],"members":[{"kind":"issue","number":6369,"title":"Persistent terminal sandboxes (docker/daytona/modal) are torn down every turn, contradicting `terminal.lifetime_seconds`","url":null,"state":"closed"},{"kind":"pr","number":6370,"title":"fix(terminal): persistent sandbox envs survive between turns","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#6412","number":6412}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":6412,"title":"fix(terminal): persistent sandbox envs survive between turns","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["run_agent.py","tools/terminal_tool.py"],"fix_hunks":[{"file":"run_agent.py","hunks":["@@ -66,7 +66,7 @@\n     handle_function_call,\n     check_toolset_requirements,\n )\n-from tools.terminal_tool import cleanup_vm, get_active_env\n+from tools.terminal_tool import cleanup_vm, get_active_env, is_persistent_env\n from tools.tool_result_storage import maybe_persist_tool_result, enforce_turn_budget\n from tools.interrupt import set_interrupt as _set_interrupt\n from tools.browser_tool import cleanup_browser","@@ -1687,9 +1687,25 @@ def _extract_reasoning(self, assistant_message) -> Optional[str]:\n         return None\n \n     def _cleanup_task_resources(self, task_id: str) -> None:\n-        \"\"\"Clean up VM and browser resources for a given task.\"\"\"\n+        \"\"\"Clean up VM and browser resources for a given task.\n+\n+        Skips ``cleanup_vm`` when the active terminal environment is marked\n+        persistent (``persistent_filesystem=True``) so that long-lived sandbox\n+        containers survive between turns. The idle reaper in\n+        ``terminal_tool._cleanup_inactive_envs`` still tears them down once\n+        ``terminal.lifetime_seconds`` is exceeded. Non-persistent backends are\n+        torn down per-turn as before to prevent resource leakage (the original\n+        intent of this hook for the Morph backend, see commit fbd3a2fd).\n+        \"\"\"\n         try:\n-            cleanup_vm(task_id)\n+            if is_persistent_env(task_id):\n+                if self.verbose_logging:\n+                    logging.debug(\n+                        f\"Skipping per-turn cleanup_vm for persistent env {task_id}; \"\n+                        f\"idle reaper will handle it.\"\n+                    )\n+            else:\n+                cleanup_vm(task_id)\n         except Exception as e:\n             if self.verbose_logging:\n                 logging.warning(f\"Failed to cleanup VM for task {task_id}: {e}\")"]},{"file":"tools/terminal_tool.py","hunks":["@@ -814,6 +814,23 @@ def get_active_env(task_id: str):\n         return _active_environments.get(task_id)\n \n \n+def is_persistent_env(task_id: str) -> bool:\n+    \"\"\"Return True if the active environment for task_id is configured for\n+    cross-turn persistence (``persistent_filesystem=True``).\n+\n+    Used by the agent loop to skip per-turn teardown for backends whose whole\n+    point is to survive between turns (docker with ``container_persistent``,\n+    daytona, modal, etc.). Non-persistent backends (e.g. Morph) still get torn\n+    down at end-of-turn to prevent leakage. The idle reaper\n+    (``_cleanup_inactive_envs``) handles persistent envs once they exceed\n+    ``terminal.lifetime_seconds``.\n+    \"\"\"\n+    env = get_active_env(task_id)\n+    if env is None:\n+        return False\n+    return bool(getattr(env, \"_persistent\", False))\n+\n+\n def get_active_environments_info() -> Dict[str, Any]:\n     \"\"\"Get information about currently active environments.\"\"\"\n     info = {\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}