fix(background-review): silence memory provider teardown output leak

Background review fork redirected stdout/stderr around run_conversation()
so its iteration messages stay silent.  But the memory-provider teardown
(shutdown_memory_provider() and review_agent.close()) fired in the outer
finally block AFTER the redirect_stdout context exited — so provider
teardown prints (Honcho disconnect, Hindsight sync, etc.) leaked into
the parent terminal at end of every turn.

Moves the teardown inside the redirect_stdout scope on the success path
(and nulls review_agent so the finally safety-net skips double-shutdown).
The finally block is rewritten as an exception-path safety net that
re-opens a devnull redirect, since the original 'with' context has
already exited by the time finally runs.

Salvage of #25342 by @ayushere (manually re-applied + merged conflict
with current main's set_thread_tool_whitelist wiring).
This commit is contained in:
ayushere 2026-05-13 23:17:14 -07:00 committed by Teknium
parent 7becb19ea0
commit 55ba02befb

View file

@ -4373,6 +4373,20 @@ class AIAgent:
finally: finally:
clear_thread_tool_whitelist() clear_thread_tool_whitelist()
# Tear down memory providers while stdout is still
# redirected so background thread teardown (Honcho flush,
# Hindsight sync, etc.) stays silent. The finally block
# below is a safety net for the exception path.
try:
review_agent.shutdown_memory_provider()
except Exception:
pass
try:
review_agent.close()
except Exception:
pass
review_agent = None
# Scan the review agent's messages for successful tool actions # Scan the review agent's messages for successful tool actions
# and surface a compact summary to the user. Tool messages # and surface a compact summary to the user. Tool messages
# already present in messages_snapshot must be skipped, since # already present in messages_snapshot must be skipped, since
@ -4402,21 +4416,24 @@ class AIAgent:
logger.warning("Background memory/skill review failed: %s", e) logger.warning("Background memory/skill review failed: %s", e)
self._emit_auxiliary_failure("background review", e) self._emit_auxiliary_failure("background review", e)
finally: finally:
# Background review agents can initialize memory providers # Safety-net cleanup for the exception path. Normal
# (for example Hindsight) that own their own network clients. # completion already shut down inside redirect_stdout above.
# Explicitly stop those providers before closing the agent so # Re-open devnull here so any teardown output (Honcho flush,
# their aiohttp sessions do not leak until GC/process exit. # Hindsight sync, background thread joins) stays silent even
# Then close all remaining resources (httpx client, # on the exception path where redirect_stdout already exited.
# subprocesses, etc.) so GC doesn't try to clean them up on a
# dead asyncio event loop (which produces "Event loop is
# closed" errors).
if review_agent is not None: if review_agent is not None:
try: try:
review_agent.shutdown_memory_provider() with open(os.devnull, "w", encoding="utf-8") as _fn, \
except Exception: contextlib.redirect_stdout(_fn), \
pass contextlib.redirect_stderr(_fn):
try: try:
review_agent.close() review_agent.shutdown_memory_provider()
except Exception:
pass
try:
review_agent.close()
except Exception:
pass
except Exception: except Exception:
pass pass
# Clear the approval callback on this bg-review thread so a # Clear the approval callback on this bg-review thread so a