961f7481a7
The native Relay pipeline binds its Futures to the event loop that entered run_in_session_async. While a managed tool callback executes, that loop is blocked until the callback returns — so a nested managed relay call made from inside the callback (vision_analyze's auxiliary LLM call on a worker-thread loop) awaits a Future that can never complete: 'RuntimeError: Future attached to a different loop', or a deadlock, plus 'Event loop is closed' at shutdown when the orphaned future completes late. (#77244) Fix: managed_callback_guard, a ContextVar depth marker set around every Hermes callback the relay adapters hand to the native pipeline (relay_tools.execute invoke, relay_llm execute/execute_async invoke, ManagedLlmStream run_callback). resolve_execution_context returns the no-relay triple while the marker is set, so nested calls run unmanaged. The marker propagates through contextvars.copy_context() into the worker threads tools use for their internal async work. Top-level turn LLM calls and tool wraps stay fully managed — verified live: vision_analyze works under active shared metrics while the main turn still records managed llm.execute events. Alternative fixes considered and rejected: removing retain_managed_execution (kills the shared-metrics managed pipeline) and gating on main-thread identity (managed tool wraps legitimately run on the run_agent thread, so that gate disables relay everywhere).