From 05a32542eaf78ace8847e5ef6fdffd0f533d1457 Mon Sep 17 00:00:00 2001 From: unsupportedpastels Date: Sun, 18 Oct 2026 23:56:22 +0000 Subject: [PATCH] fix(agent): agent builds no longer wrap the thread-routing stdio proxy _install_safe_stdio() only skipped a _SafeWriter, so after a background review installed the thread-routing proxy, the next agent build wrapped that proxy in a _SafeWriter. The next thread_scoped_silence() no longer saw a router on top and installed a new one over the wrapper, so every build/silence cycle added two links to sys.stdout and sys.stderr. After about 500 cycles, the line-buffering probe in setup_logging hit RecursionError and every gateway turn failed until a restart. The routing proxy already absorbs the OSError/ValueError the _SafeWriter guards against, so the build now leaves it alone. The chain stays at one link (two with startup redaction) however many agents are built. Fixes #139687 Co-authored-by: X9A81P4 <156729876+X9A81P4@users.noreply.github.com> Co-authored-by: salch-cred --- agent/process_bootstrap.py | 4 +++- tests/agent/test_thread_scoped_output.py | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/agent/process_bootstrap.py b/agent/process_bootstrap.py index 2fbaa1ad33..85dccfc737 100644 --- a/agent/process_bootstrap.py +++ b/agent/process_bootstrap.py @@ -18,6 +18,7 @@ from typing import Any, Optional from hermes_bootstrap import _happy_eyeballs_create_connection from utils import base_url_hostname, normalize_proxy_url from agent.proxy_bypass import first_proxy_env_value, should_bypass_proxy +from agent.thread_scoped_output import _ThreadRoutingStream _OPENAI_CLS_CACHE = None @@ -334,7 +335,8 @@ def _install_safe_stdio() -> None: """Wrap stdout/stderr so best-effort console output cannot crash the agent.""" for stream_name in ("stdout", "stderr"): stream = getattr(sys, stream_name, None) - if stream is not None and not isinstance(stream, _SafeWriter): + # The routing proxy already absorbs these errors; wrapping it stacks a new proxy per build (#139687). + if stream is not None and not isinstance(stream, (_SafeWriter, _ThreadRoutingStream)): setattr(sys, stream_name, _SafeWriter(stream)) diff --git a/tests/agent/test_thread_scoped_output.py b/tests/agent/test_thread_scoped_output.py index dc57064d13..7660f92621 100644 --- a/tests/agent/test_thread_scoped_output.py +++ b/tests/agent/test_thread_scoped_output.py @@ -130,6 +130,26 @@ def test_temporary_global_redirects_do_not_allocate_new_sinks(monkeypatch): sys.stdout, sys.stderr = original_stdout, original_stderr +def test_agent_builds_keep_the_installed_routing_proxy(monkeypatch): + """Agent builds between silences must not stack wrappers on sys.stdout/stderr (#139687).""" + from agent.process_bootstrap import _install_safe_stdio + + monkeypatch.setattr(thread_output, "_installed", {}) + monkeypatch.setattr(sys, "stdout", io.StringIO()) + monkeypatch.setattr(sys, "stderr", io.StringIO()) + _install_safe_stdio() + with thread_scoped_silence(): + pass + routers = (sys.stdout, sys.stderr) + + for _ in range(3): + _install_safe_stdio() + with thread_scoped_silence(): + pass + + assert (sys.stdout, sys.stderr) == routers + + def test_silence_survives_redirect_restoring_an_older_proxy(monkeypatch): """Silencing is stream-wide, even when a redirect swaps proxy generations.""" monkeypatch.setattr(thread_output, "_installed", {})