diff --git a/tests/tools/test_browser_orphan_reaper.py b/tests/tools/test_browser_orphan_reaper.py index 4ca34d3dea..2d8bd9c907 100644 --- a/tests/tools/test_browser_orphan_reaper.py +++ b/tests/tools/test_browser_orphan_reaper.py @@ -58,9 +58,26 @@ class TestReapOrphanedBrowserSessions: from tools.browser_tool import _reap_orphaned_browser_sessions d = _make_socket_dir(fake_tmpdir, "h_abc1234567") assert d.exists() - _reap_orphaned_browser_sessions() + with patch( + "tools.browser_tool._socket_dir_idle_seconds", + return_value=10_000, + ): + _reap_orphaned_browser_sessions() assert not d.exists() + def test_fresh_dir_without_pid_file_survives_creator_race(self, fake_tmpdir): + """A concurrent reaper must not delete a session still starting.""" + from tools.browser_tool import _reap_orphaned_browser_sessions + + d = _make_socket_dir(fake_tmpdir, "h_starting1234") + with patch( + "tools.browser_tool._socket_dir_idle_seconds", + return_value=0.0, + ): + _reap_orphaned_browser_sessions() + + assert d.exists() + def test_alive_legacy_daemon_is_reaped(self, fake_tmpdir): """Alive, untracked, legacy (no owner_pid) daemon is reaped. diff --git a/tools/browser_tool.py b/tools/browser_tool.py index 438e7a64ce..87337b0c19 100644 --- a/tools/browser_tool.py +++ b/tools/browser_tool.py @@ -2646,7 +2646,14 @@ def _reap_orphaned_browser_sessions(): # owner_alive is False (dead owner) OR legacy daemon not tracked here. pid_file = os.path.join(socket_dir, f"{session_name}.pid") if not os.path.isfile(pid_file): - # No daemon PID file — just a stale dir, remove it + # A newly-created session directory exists briefly before + # agent-browser writes its PID/owner files. Another Hermes process + # may run this global reaper during that window. Treat a pidless + # directory as stale only after the orphan grace period; deleting + # it immediately races the creator's first stdout/stderr open. + idle_s = _socket_dir_idle_seconds(socket_dir) + if idle_s is None or idle_s < BROWSER_ORPHAN_GRACE_SECONDS: + continue shutil.rmtree(socket_dir, ignore_errors=True) continue