diff --git a/run_agent.py b/run_agent.py index bcacec3909b69..34b5e22133b5f 100644 --- a/run_agent.py +++ b/run_agent.py @@ -3601,6 +3601,14 @@ class AIAgent: except Exception: pass + try: + codex_session = getattr(self, "_codex_session", None) + if codex_session is not None: + self._codex_session = None + codex_session.close() + except Exception: + pass + # 6. Free conversation history. Mirrors _release_evicted_agent_soft's # soft-eviction clear — close() is the hard teardown for true session # boundaries (/new, /reset, session expiry), so the message list won't diff --git a/tests/run_agent/test_codex_app_server_lifecycle.py b/tests/run_agent/test_codex_app_server_lifecycle.py new file mode 100644 index 0000000000000..49ed592642938 --- /dev/null +++ b/tests/run_agent/test_codex_app_server_lifecycle.py @@ -0,0 +1,33 @@ +import threading + +from run_agent import AIAgent + + +class _FakeCodexSession: + def __init__(self): + self.close_calls = 0 + + def close(self): + self.close_calls += 1 + + +def test_agent_close_releases_codex_app_server_session(monkeypatch): + agent = AIAgent.__new__(AIAgent) + agent.session_id = "test-codex-lifecycle" + agent.client = None + agent._active_children_lock = threading.Lock() + agent._active_children = set() + agent._end_session_on_close = False + agent._session_messages = ["retained"] + codex_session = _FakeCodexSession() + agent._codex_session = codex_session + + monkeypatch.setattr("run_agent.cleanup_vm", lambda _task_id: None) + monkeypatch.setattr("run_agent.cleanup_browser", lambda _task_id: None) + + agent.close() + agent.close() + + assert codex_session.close_calls == 1 + assert agent._codex_session is None + assert agent._session_messages == []