Skip to content

Commit 888bc8a

Browse files
committed
Merge remote-tracking branch 'upstream/main' into test/upstream-catchup-20260711
2 parents 94e3b26 + 5ecc079 commit 888bc8a

48 files changed

Lines changed: 2905 additions & 290 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

agent/agent_runtime_helpers.py

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,7 +1208,42 @@ def restore_primary_runtime(agent) -> bool:
12081208
api_mode=rt.get("compressor_api_mode", ""),
12091209
)
12101210

1211-
# ── Re-select from the credential pool if one is available ──
1211+
# ── Rebind and re-select the primary credential pool ──
1212+
# A cross-provider fallback attaches the fallback provider's pool. The
1213+
# runtime fields above restore the primary, but leaving that pool in
1214+
# place makes the next primary 401/429 hit the provider-mismatch guard
1215+
# and disables credential rotation. Reload the primary pool first; if
1216+
# auth storage is temporarily unreadable, clear the mismatched pool.
1217+
primary_provider = str(rt.get("provider") or "").strip().lower()
1218+
pool = getattr(agent, "_credential_pool", None)
1219+
pool_provider = str(getattr(pool, "provider", "") or "").strip().lower()
1220+
pool_matches_primary = pool_provider == primary_provider
1221+
if (
1222+
primary_provider == "custom"
1223+
and pool_provider.startswith("custom:")
1224+
):
1225+
try:
1226+
from agent.credential_pool import get_custom_provider_pool_key
1227+
1228+
primary_key = (
1229+
get_custom_provider_pool_key(str(rt.get("base_url") or "")) or ""
1230+
).strip().lower()
1231+
pool_matches_primary = bool(primary_key) and primary_key == pool_provider
1232+
except Exception:
1233+
pool_matches_primary = False
1234+
if pool is not None and pool_provider and not pool_matches_primary:
1235+
agent._credential_pool = None
1236+
try:
1237+
from agent.credential_pool import load_pool
1238+
1239+
agent._credential_pool = load_pool(primary_provider)
1240+
except Exception as exc:
1241+
logger.warning(
1242+
"Restore could not reload primary credential pool for %s: %s",
1243+
primary_provider,
1244+
exc,
1245+
)
1246+
12121247
# The snapshot's api_key was captured at construction time. Across
12131248
# turns the pool may have rotated (token revocation, billing/rate-limit
12141249
# exhaustion, cooldown), leaving the snapshot key stale. Restoring it
@@ -1222,7 +1257,6 @@ def restore_primary_runtime(agent) -> bool:
12221257
entry = pool.select()
12231258
if entry is not None:
12241259
entry_provider = str(getattr(entry, "provider", "") or "").strip().lower()
1225-
primary_provider = str(rt.get("provider") or "").strip().lower()
12261260
entry_matches_primary = entry_provider == primary_provider
12271261
# Custom endpoints all carry the generic ``custom`` provider on
12281262
# the agent while the pool entry is keyed ``custom:<name>`` (see
@@ -1858,6 +1892,9 @@ def switch_model(agent, new_model, new_provider, api_key='', base_url='', api_mo
18581892
old_norm = (old_provider or "").strip().lower()
18591893
new_norm = (new_provider or "").strip().lower()
18601894
if old_norm != new_norm or getattr(agent, "_credential_pool", None) is None:
1895+
# A pool bound to the old provider is worse than no pool: the
1896+
# recovery guard rejects it and every later 401/429 skips rotation.
1897+
agent._credential_pool = None
18611898
try:
18621899
from agent.credential_pool import load_pool
18631900
agent._credential_pool = load_pool(new_provider)
@@ -3161,20 +3198,21 @@ def apply_pending_steer_to_tool_results(agent, messages: list, num_tool_msgs: in
31613198

31623199

31633200

3164-
def force_close_tcp_sockets(client: Any, *, release_fds: bool = False) -> int:
3165-
"""Abort in-flight TCP I/O by shutting down pool sockets.
3201+
def force_close_tcp_sockets(client: Any) -> int:
3202+
"""Abort in-flight TCP I/O by shutting down sockets WITHOUT closing FDs.
31663203
31673204
When a provider drops a connection mid-stream — or the user issues an
31683205
interrupt — we want to unblock httpx's reader/writer immediately rather
31693206
than waiting for the kernel's per-connection timeout. ``shutdown(SHUT_RDWR)``
31703207
achieves that: it sends FIN, breaks any pending ``recv``/``send`` with EOF
3171-
or ``EPIPE``.
3208+
or ``EPIPE``, but does NOT release the file descriptor.
31723209
3173-
By default (``release_fds=False``) this helper does **not** call
3174-
``socket.close()`` / release the FD. That default is load-bearing for
3175-
cross-thread abort paths (#29507):
3210+
Historically this helper also called ``socket.close()`` so the FD got
3211+
released immediately, but that's unsafe when (as is the case for both the
3212+
interrupt-abort path and stale-call kill path) the helper runs on a
3213+
different thread than the one driving the request:
31763214
3177-
* The Python ``socket.socket`` we close is the SAME object held by
3215+
* The Python ``socket.socket`` we close here is the SAME object held by
31783216
httpx's pool, so closing it via Python sets its ``_fd`` to -1 and
31793217
future operations on that Python object fail safely.
31803218
* BUT the SSL wrapper (``ssl.SSLSocket``'s underlying OpenSSL ``BIO``)
@@ -3186,20 +3224,15 @@ def force_close_tcp_sockets(client: Any, *, release_fds: bool = False) -> int:
31863224
wrong file (issue #29507: 24-byte TLS application-data record
31873225
clobbering SQLite header bytes 5..28).
31883226
3189-
``shutdown()`` from any thread is FD-safe; ``close()`` is not when a
3190-
stranger thread still has the BIO holding the raw FD.
3227+
The fix is to let the owning thread own the close. ``shutdown()`` from any
3228+
thread is FD-safe; ``close()`` is not. The httpx connection's own close
3229+
path — which runs from the worker thread when it unwinds — will release
3230+
the FD via the same ``socket.socket`` object, and because Python's socket
3231+
close atomically swaps ``_fd`` to -1 *before* issuing ``os.close``, there
3232+
is no FD-aliasing window when only one thread closes.
31913233
3192-
When the **owning** thread is disposing of a client that is no longer
3193-
shared (``_close_openai_client`` after replace / request-complete), pass
3194-
``release_fds=True``. httpx's own ``client.close()`` does not reliably
3195-
``os.close()`` sockets that were already ``shutdown()``'d, so without an
3196-
explicit ``sock.close()`` those FDs stay in kernel CLOSED state forever
3197-
and accumulate under long-lived gateways (issue #61979 — ~1 CLOSED fd
3198-
per ~6 minutes through a local proxy path).
3199-
3200-
Returns the number of sockets shut down (and optionally closed). Field
3201-
kept as ``tcp_force_closed=N`` in log lines for backwards-compatible
3202-
parsing.
3234+
Returns the number of sockets shut down. (Field kept as
3235+
``tcp_force_closed=N`` in the log line for backwards-compatible parsing.)
32033236
"""
32043237
import socket as _socket
32053238

@@ -3211,13 +3244,7 @@ def force_close_tcp_sockets(client: Any, *, release_fds: bool = False) -> int:
32113244
except OSError:
32123245
# Already shut down / not connected / FD invalid — all benign.
32133246
pass
3214-
# IMPORTANT (#29507): never release FDs from stranger-thread
3215-
# abort paths. Only the owning-thread close path may opt in.
3216-
if release_fds:
3217-
try:
3218-
sock.close()
3219-
except OSError:
3220-
pass
3247+
# IMPORTANT (#29507): do NOT call sock.close() here. See docstring.
32213248
shutdown_count += 1
32223249
except Exception as exc:
32233250
_ra().logger.debug("Force-close TCP sockets sweep error: %s", exc)

agent/auxiliary_client.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,7 @@ def create(self, **kwargs) -> Any:
872872
# `function_call_output` items with a valid call_id, so every
873873
# Responses path normalizes tool history identically and cannot drift.
874874
from agent.codex_responses_adapter import _chat_messages_to_responses_input
875+
from utils import base_url_host_matches
875876

876877
instructions = "You are a helpful assistant."
877878
replay_messages: List[Dict[str, Any]] = []
@@ -883,7 +884,18 @@ def create(self, **kwargs) -> Any:
883884
else:
884885
replay_messages.append(msg)
885886

886-
input_items = _chat_messages_to_responses_input(replay_messages)
887+
# Copilot (githubcopilot.com) binds replayed codex_message_items ids
888+
# to a backend "connection" that doesn't survive credential
889+
# rotation/gateway restarts — replaying one gets HTTP 401 "input
890+
# item ID does not belong to this connection" (#32716). Auxiliary
891+
# calls (context compression, flush_memories, MoA aggregation) go
892+
# through this adapter instead of agent/transports/codex.py's
893+
# build_kwargs, so they need the same guard applied independently.
894+
_host_for_input = str(getattr(self._client, "base_url", "") or "")
895+
_is_github_for_input = base_url_host_matches(_host_for_input, "githubcopilot.com")
896+
input_items = _chat_messages_to_responses_input(
897+
replay_messages, is_github_responses=_is_github_for_input,
898+
)
887899

888900
resp_kwargs: Dict[str, Any] = {
889901
"model": model,

0 commit comments

Comments
 (0)