Skip to content

fix(agent): replace socket_options transport with httpx pool-level keepalive expiry (#54550 salvage) - #58766

Merged
teknium1 merged 2 commits into
mainfrom
salvage/httpx-pool-keepalive
Jul 5, 2026
Merged

teknium1 merged 2 commits into
mainfrom
salvage/httpx-pool-keepalive

Conversation

@teknium1

@teknium1 teknium1 commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Connection lifecycle moves from the socket layer to the HTTP pool layer: the custom httpx.HTTPTransport(socket_options=[SO_KEEPALIVE, ...]) is replaced with httpx.Limits(keepalive_expiry=20.0), fixing streaming breakage / TLS handshake stalls behind reverse proxies (Cloudflare, OpenResty) for ALL providers and deleting the per-domain bypass list instead of growing it.

Salvages #54550 by @DavidMetcalfe onto current main, authorship preserved, widened to the sibling builder in agent/process_bootstrap.py (aux clients: compression, vision, web_extract, titles) which the original missed. Retires the whole bug class behind #58392 / #36623 / #12953 (per-domain bypass PRs).

Why

The socket_options transport was added to stop CLOSE-WAIT accumulation (#10324), but it conflicts with reverse-proxy chunked transfer handling (#54049, #12952) and stripped TCP_NODELAY, stalling TLS handshakes — which is why the copilot hardcoded bypass existed and per-domain bypass PRs kept arriving. keepalive_expiry=20.0 reaps idle pooled connections before a proxy's typical 30-60s timeout drops them — same CLOSE-WAIT protection, right layer, no socket meddling.

Changes

  • run_agent.py: _build_keepalive_http_client → pool limits + timeouts (read=None for SSE), plain no-proxy mounts to preserve NO_PROXY resolution, verify= kept on client and mounts, copilot hardcode removed
  • agent/process_bootstrap.py: same conversion for the aux-client builder (sync + async), copilot hardcode removed
  • tests/run_agent/test_create_openai_client_proxy_env.py: pinning test updated — proxy mount still asserted, socket_options asserted ABSENT

Validation (live, before/after A/B)

Leg OLD socket_options NEW pool-keepalive
non-stream chat (OpenRouter/Cloudflare) 200, 563ms 200, 502ms
SSE stream (chunks / TTFB) 4 chunks, 611ms 4 chunks, 455ms
pool reuse (2nd request) 200 200

Also verified live: keepalive_expiry=20.0 present on the pool and idle connection actually reaped after 22s idle; verify=False honored + verify=True rejects self-signed (badssl.com); HTTPS_PROXY routed (dead-proxy fail proves routing) + NO_PROXY loopback bypass preserved; full hermes chat -q E2E through the worktree completed a real OpenRouter conversation. Targeted suites: 333 tests green (proxy env, auxiliary client, stream timeout floor, attribution headers).

Closes #54550. Closes #58392. Closes #36623. Closes #12953.

Infographic

pool-level-keepalive

DavidMetcalfe and others added 2 commits July 5, 2026 02:32
…evel keepalive expiry

The custom ``httpx.HTTPTransport(socket_options=[SO_KEEPALIVE, ...])``
in ``_build_keepalive_http_client()`` was introduced to fix CLOSE-WAIT
socket accumulation on long-lived connections (#10324).

That approach broke streaming for providers behind reverse proxies
(OpenResty, Cloudflare, etc.) because the custom socket options
conflict with the proxy's chunked-transfer handling (#54049, #12952).
It also stripped TCP_NODELAY, stalling TLS handshakes and SSE encoding.
Narrow per-provider bypasses were added for Copilot (#50298), Codex
(#36623, #12953), but the root cause remained.

The fix moves connection lifecycle management from the socket layer to
the HTTP pool layer:

- ``httpx.Limits(keepalive_expiry=20.0)`` tells httpx to close idle
  pooled connections at 20 s, before a reverse proxy's typical 30-60 s
  timeout drops them and causes CLOSE-WAIT accumulation.
- The default httpx transport preserves OS TCP defaults (including
  TCP_NODELAY), so TLS handshakes and SSE chunked encoding work
  correctly.
- ``trust_env=False`` prevents httpx from double-dipping on env vars
  (we handle proxy detection ourselves via ``_get_proxy_for_base_url``
  which respects NO_PROXY).
- The Copilot host bypass (line 3632) is no longer needed since all
  providers now use the same standard httpx.Client.

Closes #54049.  Supersedes #12010, #36623, #12953, #50298.
…ng builder

The salvaged #54550 converted AIAgent._build_keepalive_http_client but the
near-identical build_keepalive_http_client in agent/process_bootstrap.py
(used by auxiliary clients: compression, vision, web_extract, titles) kept
the socket_options transport and the api.githubcopilot.com bypass. Same
conversion: httpx.Limits(keepalive_expiry=20) + pool timeouts, verify
forwarded on client and no-proxy mounts, copilot hardcode removed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants