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 <salch-cred@users.noreply.github.com>
This commit is contained in:
authored and Mark S. committed 2026-10-18 22:47:12 -04:00
1 parent 67481b6f37
commit 05a32542ea
2 files changed
+23 -1

No files matched your search

+3 -1
View File
@@ -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))
+20
View File
@@ -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", {})