fix(cli): expand oneshot cleanup to cover all process-global resources

The initial salvage from #43698 only shut down MCP servers and cached
auxiliary clients. The interactive CLI's _run_cleanup() also closes
terminal environments, browser sessions, and interrupts async
delegations — all of which can hold native-extension-backed resources
(aiohttp connectors, websocket clients) that SIGABRT during
Py_FinalizeEx.

Add the missing three sites to _cleanup_oneshot_runtime(), matching the
order in cli.py:_run_cleanup(). Update tests to cover the expanded
cleanup chain.

Credit: @konsisumer (#67768) identified the full cleanup surface.
This commit is contained in:
kshitijk4poor 2026-07-20 12:29:36 +05:30 committed by kshitij
parent 7462546a33
commit 2de60a3a7e
2 changed files with 59 additions and 7 deletions

View file

@ -111,10 +111,26 @@ def _exit_after_oneshot(rc: object) -> None:
def _cleanup_oneshot_runtime() -> None:
"""Best-effort process-global cleanup before one-shot hard exit.
``run_oneshot`` owns the agent-local cleanup. This mirrors the lightweight,
process-global pieces from the interactive CLI shutdown path that would
otherwise be skipped by ``os._exit``.
``run_oneshot`` owns the agent-local cleanup (memory provider, agent.close,
session_db.close all in ``_run_agent``'s finally block). This mirrors the
process-global pieces from ``cli.py:_run_cleanup()`` that would otherwise
be skipped by ``os._exit``.
"""
try:
from tools.terminal_tool import cleanup_all_environments
cleanup_all_environments()
except Exception:
pass
try:
from tools.async_delegation import interrupt_all
interrupt_all(reason="oneshot shutdown")
except Exception:
pass
try:
from tools.browser_tool import _emergency_cleanup_all_sessions
_emergency_cleanup_all_sessions()
except Exception:
pass
try:
from tools.mcp_tool import shutdown_mcp_servers
shutdown_mcp_servers()

View file

@ -857,20 +857,41 @@ def test_run_and_exit_oneshot_cleans_global_runtime_before_hard_exit(
):
events = []
def _mod(name, **attrs):
fake = types.ModuleType(name)
for key, value in attrs.items():
setattr(fake, key, value)
return fake
monkeypatch.setitem(
sys.modules,
"hermes_cli.oneshot",
types.SimpleNamespace(run_oneshot=lambda *_args, **_kwargs: events.append("run") or 0),
)
monkeypatch.setitem(
sys.modules,
"tools.terminal_tool",
_mod("tools.terminal_tool", cleanup_all_environments=lambda: events.append("terminal")),
)
monkeypatch.setitem(
sys.modules,
"tools.async_delegation",
_mod("tools.async_delegation", interrupt_all=lambda **kw: events.append("delegation")),
)
monkeypatch.setitem(
sys.modules,
"tools.browser_tool",
_mod("tools.browser_tool", _emergency_cleanup_all_sessions=lambda: events.append("browser")),
)
monkeypatch.setitem(
sys.modules,
"tools.mcp_tool",
types.SimpleNamespace(shutdown_mcp_servers=lambda: events.append("mcp")),
_mod("tools.mcp_tool", shutdown_mcp_servers=lambda: events.append("mcp")),
)
monkeypatch.setitem(
sys.modules,
"agent.auxiliary_client",
types.SimpleNamespace(shutdown_cached_clients=lambda: events.append("aux")),
_mod("agent.auxiliary_client", shutdown_cached_clients=lambda: events.append("aux")),
)
monkeypatch.setattr(
main_mod, "_exit_after_oneshot", lambda rc: events.append(f"exit:{rc}")
@ -878,7 +899,7 @@ def test_run_and_exit_oneshot_cleans_global_runtime_before_hard_exit(
main_mod._run_and_exit_oneshot("hello")
assert events == ["run", "mcp", "aux", "exit:0"]
assert events == ["run", "terminal", "delegation", "browser", "mcp", "aux", "exit:0"]
def test_run_and_exit_oneshot_still_exits_when_global_cleanup_raises(
@ -894,6 +915,21 @@ def test_run_and_exit_oneshot_still_exits_when_global_cleanup_raises(
"hermes_cli.oneshot",
types.SimpleNamespace(run_oneshot=lambda *_args, **_kwargs: 0),
)
monkeypatch.setitem(
sys.modules,
"tools.terminal_tool",
types.SimpleNamespace(cleanup_all_environments=lambda: events.append("terminal")),
)
monkeypatch.setitem(
sys.modules,
"tools.async_delegation",
types.SimpleNamespace(interrupt_all=lambda **kw: events.append("delegation")),
)
monkeypatch.setitem(
sys.modules,
"tools.browser_tool",
types.SimpleNamespace(_emergency_cleanup_all_sessions=lambda: events.append("browser")),
)
monkeypatch.setitem(
sys.modules,
"tools.mcp_tool",
@ -910,7 +946,7 @@ def test_run_and_exit_oneshot_still_exits_when_global_cleanup_raises(
main_mod._run_and_exit_oneshot("hello")
assert events == ["aux", "exit:0"]
assert events == ["terminal", "delegation", "browser", "aux", "exit:0"]
def test_run_and_exit_oneshot_hard_exits_when_cleanup_is_interrupted(