fix(terminal): persistent sandbox envs survive between turns

`_cleanup_task_resources` was unconditionally calling `cleanup_vm()` at
the end of every `run_conversation` (i.e. every user turn), tearing down
the docker/daytona/modal sandbox container regardless of its
`persistent_filesystem` setting. This contradicted the documented intent
of `terminal.lifetime_seconds` (idle reaper) and `container_persistent`,
and caused per-turn loss of `/workspace`, `~/.config`, agent CLI auth
state, and any other content living inside the sandbox.

The unconditional teardown was introduced in fbd3a2fd ("prevent leakage
of morph instances between tasks", 2025-11-04) to plug a Morph backend
leak, two days after `lifetime_seconds` shipped in faecbddd. It was
later refactored into `_cleanup_task_resources` in 70dd3a16 without
changing semantics. Code and docs have disagreed since.

Fix: introduce `terminal_tool.is_persistent_env(task_id)` and skip the
per-turn `cleanup_vm` when the active env is persistent. The idle reaper
(`_cleanup_inactive_envs`) still tears persistent envs down once
`terminal.lifetime_seconds` is exceeded. Non-persistent backends (Morph)
are unchanged — still torn down per turn, preserving the original
leak-prevention intent.
This commit is contained in:
angelos 2026-04-09 02:12:26 +00:00 committed by Teknium
parent 54db7cbbe1
commit e7d3e9d767
2 changed files with 36 additions and 3 deletions

View file

@ -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 = {