{"repo":"NousResearch/hermes-agent","public_id":"29507","display_label":"TLS FD-recycling corruption of SQLite databases","summary":"These issues and fixes concern a recurring class of bug where interrupted or stale HTTP/TLS connections have their file descriptors recycled and subsequently written into open SQLite database files. Multiple vectors and attempted fixes are represented across Anthropic and OpenAI client paths.","subsystem":"agent","counts":{"members":9,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":3,"merged_prs":2,"attempt_prs":4},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":30858},"resolution":{"state":"partially_resolved","by":"pr-30858,pr-67238"},"freshness":{"state":"verified","last_verified_at":"2026-07-19T02:37:41+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.22444444444444445},"fix_edges":[{"issue_number":29507,"pr_number":30858,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":67142,"pr_number":67238,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":29507,"title":"Interrupted OpenAI/httpx request thread survives across turns and writes TLS record bytes to unrelated file descriptors on delayed close","url":null,"state":"closed"},{"kind":"issue","number":44037,"title":"Recurring fd-recycle corruption: TLS application-data records written into memory_store.db while gateway holds it open (same class as #29507, new vector)","url":null,"state":"closed"},{"kind":"issue","number":67142,"title":"Anthropic stale-stream watchdog can still corrupt SQLite via TLS FD reuse","url":null,"state":"closed"},{"kind":"pr","number":29544,"title":"fix(api-call): close FD-recycling race that wrote TLS bytes into kanban.db (#29507)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"5b6f0b695","number":0},{"kind":"pr","value":"#30858","number":30858}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":30858,"title":"fix(api-call): close FD-recycling race that wrote TLS bytes into kanban.db (#29507)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/agent_runtime_helpers.py","agent/chat_completion_helpers.py","run_agent.py","tests/run_agent/test_create_openai_client_reuse.py","tests/run_agent/test_tls_fd_recycle_corruption.py"],"fix_hunks":[{"file":"agent/agent_runtime_helpers.py","hunks":["@@ -2132,33 +2132,56 @@ def apply_pending_steer_to_tool_results(agent, messages: list, num_tool_msgs: in\n \n \n def force_close_tcp_sockets(client: Any) -> int:\n-    \"\"\"Force-close underlying TCP sockets to prevent CLOSE-WAIT accumulation.\n-\n-    When a provider drops a connection mid-stream, httpx's ``client.close()``\n-    performs a graceful shutdown which leaves sockets in CLOSE-WAIT until the\n-    OS times them out (often minutes).  This method walks the httpx transport\n-    pool and issues ``socket.shutdown(SHUT_RDWR)`` + ``socket.close()`` to\n-    force an immediate TCP RST, freeing the file descriptors.\n-\n-    Returns the number of sockets force-closed.\n+    \"\"\"Abort in-flight TCP I/O by shutting down sockets WITHOUT closing FDs.\n+\n+    When a provider drops a connection mid-stream — or the user issues an\n+    interrupt — we want to unblock httpx's reader/writer immediately rather\n+    than waiting for the kernel's per-connection timeout. ``shutdown(SHUT_RDWR)``\n+    achieves that: it sends FIN, breaks any pending ``recv``/``send`` with EOF\n+    or ``EPIPE``, but does NOT release the file descriptor.\n+\n+    Historically this helper also called ``socket.close()`` so the FD got\n+    released immediately, but that's unsafe when (as is the case for both the\n+    interrupt-abort path and stale-call kill path) the helper runs on a\n+    different thread than the one driving the request:\n+\n+      * The Python ``socket.socket`` we close here is the SAME object held by\n+        httpx's pool, so closing it via Python sets its ``_fd`` to -1 and\n+        future operations on that Python object fail safely.\n+      * BUT the SSL wrapper (``ssl.SSLSocket``'s underlying OpenSSL ``BIO``)\n+        caches the raw integer FD. Once ``os.close(fd)`` runs, the kernel may\n+        immediately recycle that integer to the next ``open()`` call — e.g.\n+        the kanban dispatcher opening ``kanban.db``.\n+      * The owning worker thread then unwinds httpx, the SSL layer flushes a\n+        pending TLS record, and the encrypted bytes get written into the\n+        wrong file (issue #29507: 24-byte TLS application-data record\n+        clobbering SQLite header bytes 5..28).\n+\n+    The fix is to let the owning thread own the close. ``shutdown()`` from any\n+    thread is FD-safe; ``close()`` is not. The httpx connection's own close\n+    path — which runs from the worker thread when it unwinds — will release\n+    the FD via the same ``socket.socket`` object, and because Python's socket\n+    close atomically swaps ``_fd`` to -1 *before* issuing ``os.close``, there\n+    is no FD-aliasing window when only one thread closes.\n+\n+    Returns the number of sockets shut down. (Field kept as\n+    ``tcp_force_closed=N`` in the log line for backwards-compatible parsing.)\n     \"\"\"\n     import socket as _socket\n \n-    closed = 0\n+    shutdown_count = 0\n     try:\n         for sock in _iter_pool_sockets(client):\n             try:\n                 sock.shutdown(_socket.SHUT_RDWR)\n             except OSError:\n+                # Already shut down / not connected / FD invalid — all benign.\n                 pass\n-            try:\n-                sock.close()\n-            except OSError:\n-                pass\n-            closed += 1\n+            # IMPORTANT (#29507): do NOT call sock.close() here. See docstring.\n+            shutdown_count += 1\n     except Exception as exc:\n         _ra().logger.debug(\"Force-close TCP sockets sweep error: %s\", exc)\n-    return closed\n+    return shutdown_count\n \n \n "]},{"file":"agent/chat_completion_helpers.py","hunks":["@@ -91,23 +91,55 @@ def interruptible_api_call(agent, api_kwargs: dict):\n     provider fallback.\n     \"\"\"\n     result = {\"response\": None, \"error\": None}\n-    request_client_holder = {\"client\": None}\n+    request_client_holder = {\"client\": None, \"owner_tid\": None}\n     request_client_lock = threading.Lock()\n \n     def _set_request_client(client):\n         with request_client_lock:\n             request_client_holder[\"client\"] = client\n+            # #29507: stamp the owning thread so a stranger-thread interrupt\n+            # only shuts the connection down rather than racing the worker\n+            # for FD ownership during ``client.close()``.\n+            request_client_holder[\"owner_tid\"] = threading.get_ident()\n         return client\n \n     def _take_request_client():\n         with request_client_lock:\n             client = request_client_holder.get(\"client\")\n             request_client_holder[\"client\"] = None\n+            request_client_holder[\"owner_tid\"] = None\n             return client\n \n     def _close_request_client_once(reason: str) -> None:\n-        request_client = _take_request_client()\n-        if request_client is not None:\n+        # #29507: dispatch on the calling thread.\n+        #\n+        # When ``_call`` (the worker) reaches its ``finally`` it owns the\n+        # close and we pop + fully close as before. When a *stranger* thread\n+        # (the interrupt-check loop, the stale-call detector) drives the\n+        # close, only shut the sockets down so the worker's blocked\n+        # ``recv``/``send`` unwinds with an ``EPIPE`` / EOF — and let the\n+        # worker close ``client`` from its own thread on its way out. That\n+        # avoids the FD-recycling race where the kernel reassigned a\n+        # just-closed TLS socket FD to ``kanban.db``, and the still-live SSL\n+        # BIO on the worker thread then wrote a 24-byte TLS application-data\n+        # record into the SQLite header (#29507).\n+        with request_client_lock:\n+            request_client = request_client_holder.get(\"client\")\n+            owner_tid = request_client_holder.get(\"owner_tid\")\n+            stranger_thread = (\n+                request_client is not None\n+                and owner_tid is not None\n+                and owner_tid != threading.get_ident()\n+            )\n+            if not stranger_thread:\n+                # Owning thread (or no recorded owner) → pop and fully close.\n+                request_client_holder[\"client\"] = None\n+                request_client_holder[\"owner_tid\"] = None\n+        if request_client is None:\n+            return\n+        if stranger_thread:\n+            agent._abort_request_openai_client(request_client, reason=reason)\n+        else:\n             agent._close_request_openai_client(request_client, reason=reason)\n \n     def _call():","@@ -1271,23 +1303,44 @@ def _on_reasoning(text):\n         return result[\"response\"]\n \n     result = {\"response\": None, \"error\": None, \"partial_tool_names\": []}\n-    request_client_holder = {\"client\": None, \"diag\": None}\n+    request_client_holder = {\"client\": None, \"diag\": None, \"owner_tid\": None}\n     request_client_lock = threading.Lock()\n \n     def _set_request_client(client):\n         with request_client_lock:\n             request_client_holder[\"client\"] = client\n+            # See #29507 explanation in the non-streaming variant above.\n+            request_client_holder[\"owner_tid\"] = threading.get_ident()\n         return client\n \n     def _take_request_client():\n         with request_client_lock:\n             client = request_client_holder.get(\"client\")\n             request_client_holder[\"client\"] = None\n+            request_client_holder[\"owner_tid\"] = None\n             return client\n \n     def _close_request_client_once(reason: str) -> None:\n-        request_client = _take_request_client()\n-        if request_client is not None:\n+        # See #29507 explanation in the non-streaming variant above. A\n+        # stranger thread (the interrupt-check / stale-stream detector loop)\n+        # only aborts sockets — never pops, never calls ``client.close()`` —\n+        # so the worker thread retains ownership of the FD release.\n+        with request_client_lock:\n+            request_client = request_client_holder.get(\"client\")\n+            owner_tid = request_client_holder.get(\"owner_tid\")\n+            stranger_thread = (\n+                request_client is not None\n+                and owner_tid is not None\n+                and owner_tid != threading.get_ident()\n+            )\n+            if not stranger_thread:\n+                request_client_holder[\"client\"] = None\n+                request_client_holder[\"owner_tid\"] = None\n+        if request_client is None:\n+            return\n+        if stranger_thread:\n+            agent._abort_request_openai_client(request_client, reason=reason)\n+        else:\n             agent._close_request_openai_client(request_client, reason=reason)\n \n     first_delta_fired = {\"done\": False}"]},{"file":"run_agent.py","hunks":["@@ -2563,6 +2563,39 @@ def _create_request_openai_client(self, *, reason: str, api_kwargs: Optional[dic\n     def _close_request_openai_client(self, client: Any, *, reason: str) -> None:\n         self._close_openai_client(client, reason=reason, shared=False)\n \n+    def _abort_request_openai_client(self, client: Any, *, reason: str) -> None:\n+        \"\"\"Cross-thread abort: shut sockets down without releasing FDs.\n+\n+        Companion to :meth:`_close_request_openai_client` for stranger-thread\n+        callers (interrupt-check loop, stale-call detector). Calling\n+        ``client.close()`` from a thread that does not own the active httpx\n+        connection raced the still-live SSL BIO and corrupted unrelated file\n+        descriptors when the kernel recycled the just-freed TCP FD (#29507).\n+\n+        Here we only ``shutdown(SHUT_RDWR)`` the pool's sockets. That unblocks\n+        the owning worker thread's pending ``recv``/``send`` with an EOF or\n+        ``EPIPE`` so it can unwind and close ``client`` from its own context\n+        — which is where the FD release belongs.\n+        \"\"\"\n+        if client is None:\n+            return\n+        try:\n+            shutdown_count = self._force_close_tcp_sockets(client)\n+            logger.info(\n+                \"OpenAI client aborted (%s, shared=False, tcp_force_closed=%d, \"\n+                \"deferred_close=stranger_thread) %s\",\n+                reason,\n+                shutdown_count,\n+                self._client_log_context(),\n+            )\n+        except Exception as exc:\n+            logger.debug(\n+                \"OpenAI client abort failed (%s, shared=False) %s error=%s\",\n+                reason,\n+                self._client_log_context(),\n+                exc,\n+            )\n+\n     def _run_codex_stream(self, api_kwargs: dict, client: Any = None, on_first_delta: callable = None):\n         \"\"\"Forwarder — see ``agent.codex_runtime.run_codex_stream``.\"\"\"\n         from agent.codex_runtime import run_codex_stream"]},{"file":"tests/run_agent/test_create_openai_client_reuse.py","hunks":["@@ -190,7 +190,13 @@ def test_replace_primary_openai_client_survives_repeated_rebuilds():\n \n \n def test_force_close_tcp_sockets_descends_httpcore_1_connection_wrapper():\n-    \"\"\"httpcore 1.x stores the real stream below conn._connection.\"\"\"\n+    \"\"\"httpcore 1.x stores the real stream below conn._connection.\n+\n+    Post-#29507: the helper must shut sockets down but must NOT release the\n+    FD via ``sock.close()`` — that race recycled FDs into unrelated file\n+    descriptors (kanban.db) and let TLS bytes overwrite SQLite headers. The\n+    owning httpx thread is responsible for closing FDs on its own unwind.\n+    \"\"\"\n     from agent.agent_runtime_helpers import force_close_tcp_sockets\n \n     class FakeSocket:","@@ -215,4 +221,6 @@ def close(self):\n \n     assert force_close_tcp_sockets(openai_client) == 1\n     assert sock.shutdown_calls == 1\n-    assert sock.close_calls == 1\n+    # #29507: close() must NOT be called from this helper — the owning\n+    # httpx worker thread releases the FD, not us.\n+    assert sock.close_calls == 0"]},{"file":"tests/run_agent/test_tls_fd_recycle_corruption.py","hunks":["@@ -0,0 +1,454 @@\n+\"\"\"Regressions for issue #29507 — cross-thread close of the per-request OpenAI\n+client could release a TLS socket FD whose integer was still cached in the\n+owning httpx worker's SSL BIO. The kernel then recycled the FD into the next\n+``open()`` (e.g. the kanban dispatcher's ``kanban.db``), and the worker's\n+delayed TLS flush wrote a 24-byte TLS application-data record on top of the\n+SQLite header.\n+\n+The fix has two prongs:\n+\n+1. ``force_close_tcp_sockets`` no longer calls ``sock.close()`` — only\n+   ``shutdown(SHUT_RDWR)``. Shutdown unblocks the worker's pending\n+   ``recv``/``send`` without releasing the FD.\n+\n+2. ``_close_request_client_once`` is thread-aware: a stranger thread (the\n+   interrupt-check / stale-call loop) only aborts the sockets and leaves\n+   the client in the holder; the worker's own ``finally`` performs the\n+   actual ``client.close()`` from its own thread context.\n+\n+Both prongs together close the FD-recycling window. The tests below pin\n+each prong individually and one end-to-end test simulates the reporter's\n+timeline at object granularity (no network, no real sockets).\n+\"\"\"\n+from __future__ import annotations\n+\n+import logging\n+import socket as _socket\n+import threading\n+from types import SimpleNamespace\n+from unittest.mock import MagicMock, patch\n+\n+import pytest\n+\n+\n+# ---------------------------------------------------------------------------\n+# Prong 1: force_close_tcp_sockets must NOT release file descriptors.\n+# ---------------------------------------------------------------------------\n+\n+\n+class _FakeSocket:\n+    \"\"\"Records shutdown/close calls without touching real FDs.\"\"\"\n+\n+    def __init__(self):\n+        self.shutdown_calls = 0\n+        self.close_calls = 0\n+\n+    def shutdown(self, _how):\n+        self.shutdown_calls += 1\n+\n+    def close(self):\n+        self.close_calls += 1\n+\n+\n+def _build_fake_client(sock):\n+    \"\"\"Mimic the httpcore-1 layout that ``_iter_pool_sockets`` walks.\"\"\"\n+    stream = SimpleNamespace(_sock=sock)\n+    http11 = SimpleNamespace(_network_stream=stream)\n+    pool_entry = SimpleNamespace(_connection=http11)\n+    pool = SimpleNamespace(_connections=[pool_entry])\n+    transport = SimpleNamespace(_pool=pool)\n+    http_client = SimpleNamespace(_transport=transport)\n+    return SimpleNamespace(_client=http_client)\n+\n+\n+def test_force_close_tcp_sockets_shutdown_only_no_close():\n+    \"\"\"The smoking-gun guarantee: shutdown is called, close is NOT.\n+\n+    If a future refactor reintroduces ``sock.close()`` here, the\n+    FD-recycling race that corrupted ``kanban.db`` (issue #29507) will\n+    re-open. Pin the contract explicitly.\n+    \"\"\"\n+    from agent.agent_runtime_helpers import force_close_tcp_sockets\n+\n+    sock = _FakeSocket()\n+    client = _build_fake_client(sock)\n+\n+    n = force_close_tcp_sockets(client)\n+\n+    assert n == 1\n+    assert sock.shutdown_calls == 1, \"shutdown() must run — it's how we unblock the worker\"\n+    assert sock.close_calls == 0, (\n+        \"close() must NOT run from this helper — releasing the FD here is the \"\n+        \"race that wrote TLS bytes into kanban.db (#29507)\"\n+    )\n+\n+\n+def test_force_close_tcp_sockets_uses_shut_rdwr():\n+    \"\"\"Both directions must be shut down so the SSL state machine fully unwinds.\n+\n+    Half-close (e.g. SHUT_WR only) wouldn't unblock a worker blocked in\n+    ``recv``, defeating the whole point of the helper.\n+    \"\"\"\n+    from agent.agent_runtime_helpers import force_close_tcp_sockets\n+\n+    captured = []\n+\n+    class _ProbingSocket:\n+        def shutdown(self, how):\n+            captured.append(how)\n+\n+        def close(self):  # pragma: no cover — must not run, asserted below\n+            captured.append(\"CLOSE_CALLED\")\n+\n+    sock = _ProbingSocket()\n+    client = _build_fake_client(sock)\n+\n+    force_close_tcp_sockets(client)\n+\n+    assert captured == [_socket.SHUT_RDWR]\n+\n+\n+def test_force_close_tcp_sockets_swallows_oserror_on_shutdown():\n+    \"\"\"A socket already shut down / not connected raises ``OSError`` — benign.\"\"\"\n+    from agent.agent_runtime_helpers import force_close_tcp_sockets\n+\n+    class _AlreadyShut:\n+        def shutdown(self, _how):\n+            raise OSError(\"not connected\")\n+\n+        def close(self):  # pragma: no cover — must not run\n+            raise AssertionError(\"close() must not be called\")\n+\n+    client = _build_fake_client(_AlreadyShut())\n+\n+    # No exception escapes; the helper still counts the socket as handled.\n+    assert force_close_tcp_sockets(client) == 1\n+\n+\n+def test_force_close_tcp_sockets_handles_multiple_pool_entries():\n+    \"\"\"Walk every pool connection — the bug equally applies to all of them.\"\"\"\n+    from agent.agent_runtime_helpers import force_close_tcp_sockets\n+\n+    socks = [_FakeSocket(), _FakeSocket(), _FakeSocket()]\n+    entries = [\n+        SimpleNamespace(_connection=SimpleNamespace(_network_stream=SimpleNamespace(_sock=s)))\n+        for s in socks\n+    ]\n+    pool = SimpleNamespace(_connections=entries)\n+    transport = SimpleNamespace(_pool=pool)\n+    http_client = SimpleNamespace(_transport=transport)\n+    client = SimpleNamespace(_client=http_client)\n+\n+    assert force_close_tcp_sockets(client) == 3\n+    for s in socks:\n+        assert s.shutdown_calls == 1\n+        assert s.close_calls == 0\n+\n+\n+# ---------------------------------------------------------------------------\n+# Prong 2: _close_request_client_once is thread-aware.\n+# ---------------------------------------------------------------------------\n+\n+\n+def _make_agent_mock():\n+    \"\"\"Minimal agent with the two close primitives stubbed for spy-style checks.\"\"\"\n+    agent = MagicMock()\n+    agent._interrupt_requested = False\n+    agent._close_request_openai_client = MagicMock()\n+    agent._abort_request_openai_client = MagicMock()\n+    return agent\n+\n+\n+def _call_inside_owner_thread(callable_):\n+    \"\"\"Run callable_ on a separate thread so its ``threading.get_ident()``\n+    differs from the test thread.\"\"\"\n+    result = {\"value\": None, \"exc\": None}\n+\n+    def runner():\n+        try:\n+            result[\"value\"] = callable_()\n+        except BaseException as e:  # noqa: BLE001 — propagate test failures faithfully\n+            result[\"exc\"] = e\n+\n+    t = threading.Thread(target=runner)\n+    t.start()\n+    t.join(timeout=5.0)\n+    if result[\"exc\"] is not None:\n+        raise result[\"exc\"]\n+    return result[\"value\"]\n+\n+\n+def test_close_from_stranger_thread_aborts_only_no_close():\n+    \"\"\"Stranger-thread close → ``_abort_request_openai_client``, holder NOT popped.\n+\n+    Reproduces the asyncio_0 → Thread-1616 interrupt path. After this call\n+    the worker's eventual ``finally`` must still see the client in the\n+    holder so IT can be the one releasing the FD.\n+    \"\"\"\n+    from agent.chat_completion_helpers import interruptible_api_call\n+\n+    # We can't easily invoke just `_close_request_client_once` because it's\n+    # a closure local to ``interruptible_api_call``. Re-extract the same\n+    # logic by exercising it through a fake worker that lets us drive the\n+    # holder state manually.\n+    agent = _make_agent_mock()\n+    # Pretend ``_call`` ran far enough to set the client on the holder\n+    # from the owner thread.\n+    sentinel = object()\n+    owner_tid_holder = {\"tid\": None, \"client_present_after_stranger_close\": False}\n+\n+    def _owner_workload(holder, lock):\n+        # Owner-thread set\n+        with lock:\n+            holder[\"client\"] = sentinel\n+            holder[\"owner_tid\"] = threading.get_ident()\n+        owner_tid_holder[\"tid\"] = threading.get_ident()\n+\n+    holder = {\"client\": None, \"owner_tid\": None}\n+    lock = threading.Lock()\n+    _call_inside_owner_thread(lambda: _owner_workload(holder, lock))\n+\n+    # Now drive the exact body of the post-#29507 ``_close_request_client_once``\n+    # from the test thread (stranger) and from the owner thread.\n+    def close_once(holder, lock, reason):\n+        with lock:\n+            request_client = holder.get(\"client\")\n+            owner_tid = holder.get(\"owner_tid\")\n+            stranger = (\n+                request_client is not None\n+                and owner_tid is not None\n+                and owner_tid != threading.get_ident()\n+            )\n+            if not stranger:\n+                holder[\"client\"] = None\n+                holder[\"owner_tid\"] = None\n+        if request_client is None:\n+            return None\n+        if stranger:\n+            agent._abort_request_openai_client(request_client, reason=reason)\n+            return \"aborted\"\n+        agent._close_request_openai_client(request_client, reason=reason)\n+        return \"closed\"\n+\n+    outcome = close_once(holder, lock, \"interrupt_abort\")\n+\n+    assert outcome == \"aborted\"\n+    agent._abort_request_openai_client.assert_called_once()\n+    agent._close_request_openai_client.assert_not_called()\n+    # Holder is still populated — the worker thread will pick this up in\n+    # its ``finally`` and own the actual ``client.close()``.\n+    assert holder[\"client\"] is sentinel\n+    assert holder[\"owner_tid\"] == owner_tid_holder[\"tid\"]\n+\n+\n+def test_close_from_owner_thread_pops_and_full_close():\n+    \"\"\"Worker-thread close → ``_close_request_openai_client``, holder popped.\"\"\"\n+    agent = _make_agent_mock()\n+    sentinel = object()\n+    holder = {\"client\": None, \"owner_tid\": None}\n+    lock = threading.Lock()\n+\n+    def workload():\n+        with lock:\n+            holder[\"client\"] = sentinel\n+            holder[\"owner_tid\"] = threading.get_ident()\n+\n+        # Same body inlined here so the test thread and the closing thread\n+        # are identical (owner == self).\n+        with lock:\n+            request_client = holder.get(\"client\")\n+            owner_tid = holder.get(\"owner_tid\")\n+            stranger = (\n+                request_client is not None\n+                and owner_tid is not None\n+                and owner_tid != threading.get_ident()\n+            )\n+            if not stranger:\n+                holder[\"client\"] = None\n+                holder[\"owner_tid\"] = None\n+        if request_client is None:\n+            return None\n+        if stranger:\n+            agent._abort_request_openai_client(request_client, reason=\"request_complete\")\n+            return \"aborted\"\n+        agent._close_request_openai_client(request_client, reason=\"request_complete\")\n+        return \"closed\"\n+\n+    outcome = _call_inside_owner_thread(workload)\n+\n+    assert outcome == \"closed\"\n+    agent._close_request_openai_client.assert_called_once()\n+    agent._abort_request_openai_client.assert_not_called()\n+    assert holder[\"client\"] is None\n+    assert holder[\"owner_tid\"] is None\n+\n+\n+def test_stranger_then_owner_close_sequence_runs_full_close_exactly_once():\n+    \"\"\"Stranger abort followed by owner close → full close runs once.\n+\n+    This mirrors the reporter's timeline: asyncio_0 fires interrupt_abort\n+    (stranger → abort only), then Thread-1616 unwinds and its finally\n+    fires request_complete (owner → full close). Net result must be one\n+    abort + one full close, with the holder ending empty.\n+    \"\"\"\n+    agent = _make_agent_mock()\n+    sentinel = object()\n+    holder = {\"client\": None, \"owner_tid\": None}\n+    lock = threading.Lock()\n+\n+    def close_once(reason):\n+        with lock:\n+            request_client = holder.get(\"client\")\n+            owner_tid = holder.get(\"owner_tid\")\n+            stranger = (\n+                request_client is not None\n+                and owner_tid is not None\n+                and owner_tid != threading.get_ident()\n+            )\n+            if not stranger:\n+                holder[\"client\"] = None\n+                holder[\"owner_tid\"] = None\n+        if request_client is None:\n+            return\n+        if stranger:\n+            agent._abort_request_openai_client(request_client, reason=reason)\n+        else:\n+            agent._close_request_openai_client(request_client, reason=reason)\n+\n+    def owner_workload():\n+        # Set client from owner thread.\n+        with lock:\n+            holder[\"client\"] = sentinel\n+            holder[\"owner_tid\"] = threading.get_ident()\n+        # Simulate work being interrupted by a stranger from outside.\n+        nonlocal_stranger_event.wait(timeout=2.0)\n+        # Worker unwinds — its finally calls close once.\n+        close_once(\"request_complete\")\n+\n+    nonlocal_stranger_event = threading.Event()\n+    owner = threading.Thread(target=owner_workload)\n+    owner.start()\n+\n+    # Test thread plays the stranger.\n+    # Give the owner a moment to set the holder.\n+    import time as _t\n+    _t.sleep(0.05)\n+    close_once(\"interrupt_abort\")\n+    nonlocal_stranger_event.set()\n+    owner.join(timeout=5.0)\n+\n+    assert not owner.is_alive(), \"owner thread hung past join timeout\"\n+\n+    # The fix's intended outcome: abort once, close once, holder empty.\n+    assert agent._abort_request_openai_client.call_count == 1\n+    assert agent._close_request_openai_client.call_count == 1\n+    assert holder[\"client\"] is None\n+    assert holder[\"owner_tid\"] is None\n+\n+\n+# ---------------------------------------------------------------------------\n+# End-to-end: the agent's ``_abort_request_openai_client`` shuts sockets and\n+# logs deferred_close=stranger_thread without ever calling client.close().\n+# ---------------------------------------------------------------------------\n+\n+\n+def test_agent_abort_request_openai_client_does_not_call_client_close(caplog):\n+    \"\"\"``_abort_request_openai_client`` must shutdown sockets but NEVER close().\n+\n+    This is the actual entry point used by the stranger-thread path. If a\n+    future refactor accidentally wires it back to ``_close_openai_client``\n+    the FD race is back. Pin both the shutdown side-effect AND the absence\n+    of any ``client.close()`` call.\n+    \"\"\"\n+    from run_agent import AIAgent\n+\n+    sock = _FakeSocket()\n+    client = _build_fake_client(sock)\n+\n+    # ``client.close()`` would mutate the holder if invoked — give it a\n+    # MagicMock spy so we can assert no call.\n+    client.close = MagicMock()\n+\n+    agent = AIAgent.__new__(AIAgent)\n+    agent._client_log_context = lambda: \"provider=test\"\n+\n+    with caplog.at_level(logging.INFO, logger=\"run_agent\"):\n+        agent._abort_request_openai_client(client, reason=\"interrupt_abort\")\n+\n+    # Sockets shut down (one in our fake pool).\n+    assert sock.shutdown_calls == 1\n+    assert sock.close_calls == 0\n+    # And critically: client.close() never ran here.\n+    client.close.assert_not_called()\n+\n+    # The log line is parseable: same ``tcp_force_closed=N`` field shape as\n+    # the existing ``close`` log so dashboards keep working, plus a\n+    # ``deferred_close=stranger_thread`` marker to make the new path\n+    # observable in production triage.\n+    msgs = [r.getMessage() for r in caplog.records]\n+    assert any(\n+        \"OpenAI client aborted (interrupt_abort\" in m\n+        and \"tcp_force_closed=1\" in m\n+        and \"deferred_close=stranger_thread\" in m\n+        for m in msgs\n+    ), f\"missing abort log line; got: {msgs!r}\"\n+\n+\n+def test_agent_abort_request_openai_client_null_client_is_noop():\n+    \"\"\"A ``None`` client must short-circuit cleanly (defensive).\"\"\"\n+    from run_agent import AIAgent\n+\n+    agent = AIAgent.__new__(AIAgent)\n+    agent._client_log_context = lambda: \"provider=test\"\n+\n+    # No exception, no side effect.\n+    agent._abort_request_openai_client(None, reason=\"interrupt_abort\")\n+\n+\n+# ---------------------------------------------------------------------------\n+# FD-recycling proof: when shutdown-only is honored, a stranger-thread abort\n+# CANNOT release an FD that the owning thread still references.\n+# ---------------------------------------------------------------------------\n+\n+\n+def test_fd_recycle_window_closed_by_shutdown_only():\n+    \"\"\"Construct the exact race the reporter saw — abort from a stranger\n+    thread, then have the (simulated) kernel recycle the FD into a new file.\n+    With the fix, the worker's surviving socket reference cannot be\n+    confused with the recycled file descriptor.\n+    \"\"\"\n+    from agent.agent_runtime_helpers import force_close_tcp_sockets\n+\n+    # Tracks \"was the FD released by the abort path?\" — that is the only\n+    # signal the kernel needs to recycle the integer to a new ``open()``.\n+    fd_released = {\"yes\": False}\n+\n+    class _OwnedSocket:\n+        \"\"\"Simulates a socket whose FD is shared with the owner's SSL BIO.\n+\n+        ``close`` flips ``fd_released`` so the test can assert that with\n+        the fix the abort path NEVER releases the FD (and therefore the\n+        kernel never recycles it under the owner's still-active reference).\n+        \"\"\"\n+\n+        def __init__(self):\n+            self.shutdowns = 0\n+\n+        def shutdown(self, _how):\n+            self.shutdowns += 1\n+\n+        def close(self):\n+            fd_released[\"yes\"] = True\n+\n+    sock = _OwnedSocket()\n+    client = _build_fake_client(sock)\n+\n+    # Stranger thread runs the abort sweep (== what asyncio_0 did in the\n+    # reporter's session).\n+    _call_inside_owner_thread(lambda: force_close_tcp_sockets(client))\n+\n+    assert sock.shutdowns == 1, \"shutdown must wake the worker\"\n+    assert fd_released[\"yes\"] is False, (\n+        \"force_close_tcp_sockets released the FD from a stranger thread — \"\n+        \"this is exactly the #29507 race. The owner thread must own close().\"\n+    )\n"]}]},{"kind":"pr","number":44066,"title":"fix(memory): close SQLite in holographic shutdown + header validation (#44037)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"7c0a5def58fbec985ace8c887ca3484f7b8cf584","number":0},{"kind":"pr","value":"#54133","number":54133},{"kind":"pr","value":"#44037","number":44037},{"kind":"pr","value":"#54133","number":54133}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":67210,"title":"fix(anthropic): abort watchdog TLS sockets from a stranger thread, close from the worker (#67142)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#67238","number":67238},{"kind":"pr","value":"#67142","number":67142},{"kind":"pr","value":"#67238","number":67238},{"kind":"pr","value":"#51688","number":51688}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":67238,"title":"fix(agent): request-local Anthropic clients so the stale/interrupt watchdog never corrupts SQLite (#67142, supersedes #51688)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/chat_completion_helpers.py","run_agent.py","tests/agent/test_cascading_interrupt_6600.py","tests/run_agent/test_28161_anthropic_stream_pool_cleanup.py","tests/run_agent/test_run_agent.py","tests/run_agent/test_stream_stale_circuit_breaker.py","tests/run_agent/test_streaming.py"],"fix_hunks":[{"file":"agent/chat_completion_helpers.py","hunks":["@@ -372,13 +372,14 @@ def _dispatch_nonstreaming_api_request(agent, api_kwargs: dict, *, make_client):\n     inline path (``direct_api_call``) so the per-api_mode dispatch — codex /\n     anthropic / bedrock / MoA / OpenAI-compatible — lives in exactly one place.\n \n-    ``make_client(reason)`` builds the per-request OpenAI client for the codex\n-    and OpenAI-compatible branches; the worker path uses it to register the\n-    client with its stranger-thread abort machinery, the inline path uses it to\n-    capture the client for its own ``finally`` close. The anthropic / bedrock /\n-    MoA branches manage their own clients and never call it. All interrupt,\n-    abort, cancellation, and close semantics stay in the callers — this helper\n-    only issues the request.\n+    ``make_client(reason, kind=...)`` builds the per-request client for the\n+    codex / OpenAI-compatible (``kind=\"openai\"``) and anthropic\n+    (``kind=\"anthropic_messages\"``) branches; the worker path uses it to\n+    register the client with its stranger-thread abort machinery, the inline\n+    path uses it to capture the client for its own ``finally`` close. The\n+    bedrock / MoA branches manage their own clients and never call it. All\n+    interrupt, abort, cancellation, and close semantics stay in the callers —\n+    this helper only issues the request.\n     \"\"\"\n     if agent.api_mode == \"codex_responses\":\n         request_client = make_client(\"codex_stream_request\")","@@ -388,7 +389,13 @@ def _dispatch_nonstreaming_api_request(agent, api_kwargs: dict, *, make_client):\n             on_first_delta=getattr(agent, \"_codex_on_first_delta\", None),\n         )\n     if agent.api_mode == \"anthropic_messages\":\n-        return agent._anthropic_messages_create(api_kwargs)\n+        # #67142: use a request-local Anthropic client so the stale/interrupt\n+        # watchdog aborts sockets from the stranger thread while the worker\n+        # owns the SDK close — never closing the shared client mid-flight.\n+        request_client = make_client(\n+            \"anthropic_messages_request\", kind=\"anthropic_messages\"\n+        )\n+        return agent._anthropic_messages_create(api_kwargs, client=request_client)\n     if agent.api_mode == \"bedrock_converse\":\n         # Bedrock uses boto3 directly — no OpenAI client needed.\n         # normalize_converse_response produces an OpenAI-compatible","@@ -458,7 +465,11 @@ def _abort_active_request(reason: str) -> None:\n         if request_client is not None:\n             agent._abort_request_openai_client(request_client, reason=reason)\n \n-    def _make_client(reason: str):\n+    def _make_client(reason: str, kind: str = \"openai\"):\n+        # direct_api_call only runs for OpenAI-wire chat_completions cron\n+        # requests (see should_use_direct_api_call), so the anthropic branch of\n+        # the dispatch — the only caller that passes kind — is never reached\n+        # here; the ``kind`` parameter exists purely for signature parity.\n         client = agent._create_request_openai_client(reason=reason, api_kwargs=api_kwargs)\n         with request_client_lock:\n             request_client_holder[\"client\"] = client","@@ -517,6 +528,10 @@ def interruptible_api_call(agent, api_kwargs: dict):\n     _check_stale_giveup(agent)\n \n     request_client_holder = {\"client\": None, \"owner_tid\": None}\n+    # Transport kind of the registered request client (\"openai\" or\n+    # \"anthropic_messages\") so _close_request_client_once routes to the right\n+    # abort/close helpers (#67142).\n+    request_client_kind = {\"value\": \"openai\"}\n     request_client_lock = threading.Lock()\n     # Request-local cancellation flag. Distinct from agent._interrupt_requested\n     # because that flag is cleared at run_conversation() turn boundaries, but","@@ -528,9 +543,10 @@ def interruptible_api_call(agent, api_kwargs: dict):\n     # hang.)\n     _request_cancelled = {\"value\": False}\n \n-    def _set_request_client(client):\n+    def _set_request_client(client, *, kind: str = \"openai\"):\n         with request_client_lock:\n             request_client_holder[\"client\"] = client\n+            request_client_kind[\"value\"] = kind\n             # #29507: stamp the owning thread so a stranger-thread interrupt\n             # only shuts the connection down rather than racing the worker\n             # for FD ownership during ``client.close()``.","@@ -564,24 +580,34 @@ def _close_request_client_once(reason: str) -> None:\n                 request_client_holder[\"owner_tid\"] = None\n         if request_client is None:\n             return\n-        if stranger_thread:\n+        kind = request_client_kind.get(\"value\", \"openai\")\n+        if kind == \"anthropic_messages\":\n+            if stranger_thread:\n+                agent._abort_request_anthropic_client(request_client, reason=reason)\n+            else:\n+                agent._close_request_anthropic_client(request_client, reason=reason)\n+        elif stranger_thread:\n             agent._abort_request_openai_client(request_client, reason=reason)\n         else:\n             agent._close_request_openai_client(request_client, reason=reason)\n \n     def _call():\n         try:\n-            # _set_request_client registers each per-request OpenAI client with\n-            # the stranger-thread abort machinery above; the shared dispatch\n-            # helper builds it via this callback so the interrupt / stale-call\n-            # detectors can force-close the worker's connection.\n+            # _set_request_client registers each per-request client with the\n+            # stranger-thread abort machinery above; the shared dispatch helper\n+            # builds it via this callback (openai- or anthropic-kind) so the\n+            # interrupt / stale-call detectors can force-close the worker's\n+            # connection without touching the shared client (#67142).\n             result[\"response\"] = _dispatch_nonstreaming_api_request(\n                 agent,\n                 api_kwargs,\n-                make_client=lambda reason: _set_request_client(\n-                    agent._create_request_openai_client(\n+                make_client=lambda reason, kind=\"openai\": _set_request_client(\n+                    agent._create_request_anthropic_client(reason=reason)\n+                    if kind == \"anthropic_messages\"\n+                    else agent._create_request_openai_client(\n                         reason=reason, api_kwargs=api_kwargs\n-                    )\n+                    ),\n+                    kind=kind,\n                 ),\n             )\n         except Exception as e:","@@ -893,11 +919,10 @@ def _call():\n                     f\"Aborting call.\"\n                 )\n             try:\n-                if agent.api_mode == \"anthropic_messages\":\n-                    agent._anthropic_client.close()\n-                    agent._rebuild_anthropic_client()\n-                else:\n-                    _close_request_client_once(\"stale_call_kill\")\n+                # #67142: routes by client kind — anthropic now aborts the\n+                # request-local client's sockets from this poll (stranger)\n+                # thread instead of closing the shared _anthropic_client.\n+                _close_request_client_once(\"stale_call_kill\")\n             except Exception:\n                 pass\n             # Circuit breaker (#58962): count the stale kill.  See the","@@ -933,13 +958,12 @@ def _call():\n             )\n             # Force-close the in-flight worker-local HTTP connection to stop\n             # token generation without poisoning the shared client used to\n-            # seed future retries.\n+            # seed future retries. #67142: for anthropic this aborts the\n+            # request-local client's sockets from this poll (stranger) thread\n+            # rather than closing the shared _anthropic_client, which could\n+            # release a TLS FD mid-SSL-BIO and corrupt an unrelated SQLite DB.\n             try:\n-                if agent.api_mode == \"anthropic_messages\":\n-                    agent._anthropic_client.close()\n-                    agent._rebuild_anthropic_client()\n-                else:\n-                    _close_request_client_once(\"interrupt_abort\")\n+                _close_request_client_once(\"interrupt_abort\")\n             except Exception:\n                 pass\n             raise InterruptedError(\"Agent interrupted during API call\")","@@ -2393,6 +2417,10 @@ def _on_reasoning(text):\n     _check_stale_giveup(agent)\n \n     request_client_holder = {\"client\": None, \"diag\": None, \"owner_tid\": None}\n+    # Transport kind of the registered request client — see the non-streaming\n+    # variant. Routes _close_request_client_once to anthropic vs openai abort/\n+    # close helpers (#67142).\n+    request_client_kind = {\"value\": \"openai\"}\n     request_client_lock = threading.Lock()\n     # Request-local cancellation flag — see interruptible_api_call for the full\n     # rationale. The streaming retry loop is where the 7-minute cascading-","@@ -2403,9 +2431,10 @@ def _on_reasoning(text):\n     # exit immediately instead of retrying. (PR #6600.)\n     _request_cancelled = {\"value\": False}\n \n-    def _set_request_client(client):\n+    def _set_request_client(client, *, kind: str = \"openai\"):\n         with request_client_lock:\n             request_client_holder[\"client\"] = client\n+            request_client_kind[\"value\"] = kind\n             # See #29507 explanation in the non-streaming variant above.\n             request_client_holder[\"owner_tid\"] = threading.get_ident()\n         return client","@@ -2428,7 +2457,13 @@ def _close_request_client_once(reason: str) -> None:\n                 request_client_holder[\"owner_tid\"] = None\n         if request_client is None:\n             return\n-        if stranger_thread:\n+        kind = request_client_kind.get(\"value\", \"openai\")\n+        if kind == \"anthropic_messages\":\n+            if stranger_thread:\n+                agent._abort_request_anthropic_client(request_client, reason=reason)\n+            else:\n+                agent._close_request_anthropic_client(request_client, reason=reason)\n+        elif stranger_thread:\n             agent._abort_request_openai_client(request_client, reason=reason)\n         else:\n             agent._close_request_openai_client(request_client, reason=reason)","@@ -2976,13 +3011,18 @@ def _call_chat_completions(stream_attempt_id: int):\n             usage=usage_obj,\n         )\n \n-    def _call_anthropic():\n+    def _call_anthropic(request_client):\n         \"\"\"Stream an Anthropic Messages API response.\n \n         Fires delta callbacks for real-time token delivery, but returns\n         the native Anthropic Message object from get_final_message() so\n         the rest of the agent loop (validation, tool extraction, etc.)\n         works unchanged.\n+\n+        Uses ``request_client`` (a per-request Anthropic client registered with\n+        the stranger-thread abort machinery) rather than the shared\n+        ``_anthropic_client``, so the stale/interrupt watchdog can abort this\n+        stream's socket without closing the shared client mid-flight (#67142).\n         \"\"\"\n         has_tool_use = False\n         # Zero-event guard parity with the chat_completions path: track","@@ -3011,7 +3051,7 @@ def _call_anthropic():\n             api_kwargs, log_prefix=getattr(agent, \"log_prefix\", \"\")\n         )\n         # Use the Anthropic SDK's streaming context manager\n-        with agent._anthropic_client.messages.stream(**api_kwargs) as stream:\n+        with request_client.messages.stream(**api_kwargs) as stream:\n             # The Anthropic SDK exposes the raw httpx response on\n             # ``stream.response``.  Snapshot diagnostic headers\n             # immediately so they survive a stream that dies before the","@@ -3149,8 +3189,16 @@ def _call():\n                     raise InterruptedError(\"Agent interrupted before stream retry\")\n                 try:\n                     if agent.api_mode == \"anthropic_messages\":\n-                        agent._try_refresh_anthropic_client_credentials()\n-                        result[\"response\"] = _call_anthropic()\n+                        # #67142: per-request client (credential refresh happens\n+                        # inside _create_request_anthropic_client) registered so\n+                        # the watchdog aborts its socket, not the shared client.\n+                        request_client = _set_request_client(\n+                            agent._create_request_anthropic_client(\n+                                reason=\"anthropic_stream_request\"\n+                            ),\n+                            kind=\"anthropic_messages\",\n+                        )\n+                        result[\"response\"] = _call_anthropic(request_client)\n                     else:\n                         result[\"response\"] = _call_chat_completions(stream_attempt_id)\n                     return  # success","@@ -3276,13 +3324,11 @@ def _call():\n                         )\n                         _cancel_current_stream_attempt(\"stream_mid_tool_retry_cleanup\")\n                         _close_request_client_once(\"stream_mid_tool_retry_cleanup\")\n-                        if agent.api_mode == \"anthropic_messages\":\n-                            try:\n-                                agent._anthropic_client.close()\n-                                agent._rebuild_anthropic_client()\n-                            except Exception:\n-                                pass\n-                        else:\n+                        # #67142: anthropic streams on a request-local client,\n+                        # already worker-owned-closed by _close_request_client_once\n+                        # above; the next attempt builds a fresh one. The shared\n+                        # _anthropic_client is never closed from inside a request.\n+                        if agent.api_mode != \"anthropic_messages\":\n                             try:\n                                 agent._replace_primary_openai_client(\n                                     reason=\"stream_mid_tool_retry_pool_cleanup\"","@@ -3341,15 +3387,13 @@ def _call():\n                             # Close the stale request client before retry\n                             _cancel_current_stream_attempt(\"stream_retry_cleanup\")\n                             _close_request_client_once(\"stream_retry_cleanup\")\n-                            # Also rebuild the primary client to purge\n-                            # any dead connections from the pool.\n-                            if agent.api_mode == \"anthropic_messages\":\n-                                try:\n-                                    agent._anthropic_client.close()\n-                                    agent._rebuild_anthropic_client()\n-                                except Exception:\n-                                    pass\n-                            else:\n+                            # Also rebuild the primary client to purge any dead\n+                            # connections from the pool. #67142: anthropic uses a\n+                            # request-local client (already worker-owned-closed\n+                            # above; next attempt builds fresh), so the shared\n+                            # _anthropic_client is never closed from inside a\n+                            # request — only the OpenAI-wire primary is refreshed.\n+                            if agent.api_mode != \"anthropic_messages\":\n                                 try:\n                                     agent._replace_primary_openai_client(\n                                         reason=\"stream_retry_pool_cleanup\"","@@ -3589,11 +3633,14 @@ def _call():\n             # Rebuild the primary client too — its connection pool\n             # may hold dead sockets from the same provider outage.\n             if agent.api_mode == \"anthropic_messages\":\n-                try:\n-                    agent._anthropic_client.close()\n-                    agent._rebuild_anthropic_client()\n-                except Exception:\n-                    pass\n+                # #67142: the stale stream ran on a request-local anthropic\n+                # client, already socket-aborted above via\n+                # _close_request_client_once (which unblocks the worker and\n+                # preserves the #28161 no-hang guarantee). The shared\n+                # _anthropic_client is NOT the in-flight transport, so we must\n+                # not close it from this poll (stranger) thread — that was the\n+                # FD-recycle corruption vector. Nothing further is needed.\n+                pass\n             else:\n                 try:\n                     agent._replace_primary_openai_client(reason=\"stale_stream_pool_cleanup\")","@@ -3622,11 +3669,10 @@ def _call():\n             )\n             try:\n                 _cancel_current_stream_attempt(\"stream_interrupt_abort\")\n-                if agent.api_mode == \"anthropic_messages\":\n-                    agent._anthropic_client.close()\n-                    agent._rebuild_anthropic_client()\n-                else:\n-                    _close_request_client_once(\"stream_interrupt_abort\")\n+                # #67142: kind-aware — anthropic aborts the request-local\n+                # client's socket from this poll thread; the shared\n+                # _anthropic_client is never closed here.\n+                _close_request_client_once(\"stream_interrupt_abort\")\n             except Exception:\n                 pass\n             raise InterruptedError(\"Agent interrupted during streaming API call\")"]},{"file":"run_agent.py","hunks":["@@ -4259,6 +4259,103 @@ def _abort_request_openai_client(self, client: Any, *, reason: str) -> None:\n                 exc,\n             )\n \n+    def _create_request_anthropic_client(self, *, reason: str) -> Any:\n+        \"\"\"Build a request-local Anthropic client for one in-flight call.\n+\n+        The shared ``_anthropic_client`` stays the long-lived primary, but the\n+        stale/interrupt watchdog runs on the poll thread and must never call\n+        ``close()`` on the client whose TLS socket a worker thread is still\n+        reading: releasing that FD from a stranger thread lets the kernel\n+        recycle it under a still-live SSL BIO, which then writes a TLS record\n+        into an unrelated SQLite header (#29507 / #67142). A per-request client\n+        lets the stranger thread ``shutdown()`` the socket while the owning\n+        worker performs the SDK-level close from its own context — the same\n+        ownership contract the OpenAI-wire path already uses.\n+\n+        Mirrors ``_rebuild_anthropic_client`` construction (direct + Bedrock,\n+        1M-beta drop) but returns a fresh client instead of swapping the shared\n+        one.\n+        \"\"\"\n+        if self.api_mode == \"anthropic_messages\":\n+            self._try_refresh_anthropic_client_credentials()\n+        _drop_1m = bool(getattr(self, \"_oauth_1m_beta_disabled\", False))\n+        if getattr(self, \"provider\", None) == \"bedrock\":\n+            from agent.anthropic_adapter import build_anthropic_bedrock_client\n+            region = getattr(self, \"_bedrock_region\", \"us-east-1\") or \"us-east-1\"\n+            client = build_anthropic_bedrock_client(region)\n+        else:\n+            from agent.anthropic_adapter import build_anthropic_client\n+            client = build_anthropic_client(\n+                self._anthropic_api_key,\n+                getattr(self, \"_anthropic_base_url\", None),\n+                timeout=get_provider_request_timeout(self.provider, self.model),\n+                drop_context_1m_beta=_drop_1m,\n+            )\n+        logger.debug(\n+            \"Anthropic request client created (%s, shared=False) provider=%s model=%s\",\n+            reason,\n+            getattr(self, \"provider\", None),\n+            getattr(self, \"model\", None),\n+        )\n+        return client\n+\n+    def _close_request_anthropic_client(self, client: Any, *, reason: str) -> None:\n+        \"\"\"Owner-thread full close of a request-local Anthropic client.\n+\n+        Force-closes the pool's TCP sockets first (CLOSE-WAIT hygiene, parity\n+        with ``_close_openai_client``), then does the graceful SDK close. Safe\n+        because the caller owns the connection.\n+        \"\"\"\n+        if client is None:\n+            return\n+        try:\n+            self._force_close_tcp_sockets(client)\n+            client.close()\n+            logger.info(\n+                \"Anthropic client closed (%s, shared=False) provider=%s model=%s\",\n+                reason,\n+                getattr(self, \"provider\", None),\n+                getattr(self, \"model\", None),\n+            )\n+        except Exception as exc:\n+            logger.debug(\n+                \"Anthropic client close failed (%s, shared=False) provider=%s model=%s error=%s\",\n+                reason,\n+                getattr(self, \"provider\", None),\n+                getattr(self, \"model\", None),\n+                exc,\n+            )\n+\n+    def _abort_request_anthropic_client(self, client: Any, *, reason: str) -> None:\n+        \"\"\"Cross-thread abort for request-local Anthropic clients.\n+\n+        Stranger threads (the interrupt-check / stale-stream detector loop)\n+        must not call the SDK ``close()`` — that races the owning worker's live\n+        SSL BIO and can recycle a TLS FD into a SQLite header (#29507 /\n+        #67142). Only ``shutdown(SHUT_RDWR)`` the pool's sockets so the worker\n+        unblocks and releases the FD from its own thread.\n+        \"\"\"\n+        if client is None:\n+            return\n+        try:\n+            shutdown_count = self._force_close_tcp_sockets(client)\n+            logger.info(\n+                \"Anthropic client aborted (%s, shared=False, tcp_force_closed=%d, \"\n+                \"deferred_close=stranger_thread) provider=%s model=%s\",\n+                reason,\n+                shutdown_count,\n+                getattr(self, \"provider\", None),\n+                getattr(self, \"model\", None),\n+            )\n+        except Exception as exc:\n+            logger.debug(\n+                \"Anthropic client abort failed (%s, shared=False) provider=%s model=%s error=%s\",\n+                reason,\n+                getattr(self, \"provider\", None),\n+                getattr(self, \"model\", None),\n+                exc,\n+            )\n+\n     def _run_codex_stream(self, api_kwargs: dict, client: Any = None, on_first_delta: callable = None):\n         \"\"\"Forwarder — see ``agent.codex_runtime.run_codex_stream``.\"\"\"\n         from agent.codex_runtime import run_codex_stream","@@ -4644,15 +4741,18 @@ def _credential_pool_may_recover_rate_limit(self) -> bool:\n             return False\n         return pool.has_available()\n \n-    def _anthropic_messages_create(self, api_kwargs: dict):\n-        if self.api_mode == \"anthropic_messages\":\n+    def _anthropic_messages_create(self, api_kwargs: dict, *, client: Any = None):\n+        # When a request-local client is supplied it was already credential-\n+        # refreshed in ``_create_request_anthropic_client``; only the shared\n+        # fallback path refreshes here.\n+        if client is None and self.api_mode == \"anthropic_messages\":\n             self._try_refresh_anthropic_client_credentials()\n         # Defensive: strip Responses-only kwargs that can leak in under an\n         # api_mode-flip race (the Anthropic SDK raises a non-retryable\n         # TypeError on them). See #31673.\n         from agent.anthropic_adapter import create_anthropic_message\n         return create_anthropic_message(\n-            self._anthropic_client,\n+            client or self._anthropic_client,\n             api_kwargs,\n             log_prefix=getattr(self, \"log_prefix\", \"\"),\n             prefer_stream=not bool(getattr(self, \"_disable_streaming\", False)),"]},{"file":"tests/agent/test_cascading_interrupt_6600.py","hunks":["@@ -132,3 +132,93 @@ def _create_1(**kwargs):\n \n     with pytest.raises(httpx.RemoteProtocolError):\n         cch.interruptible_api_call(agent, {\"model\": \"x\", \"messages\": []})\n+\n+\n+# ---------------------------------------------------------------------------\n+# #67142: direct-Anthropic stale/interrupt watchdog must abort the request-local\n+# client from the poll (stranger) thread and NEVER close/rebuild the shared\n+# _anthropic_client — closing it there released a live TLS FD that the kernel\n+# recycled into a SQLite handle, writing a TLS record over a DB header.\n+# ---------------------------------------------------------------------------\n+\n+\n+def _make_anthropic_agent():\n+    agent = _make_agent()\n+    agent.api_mode = \"anthropic_messages\"\n+    return agent\n+\n+\n+def _wait_for_mock_call(mock, timeout=3.0):\n+    deadline = time.time() + timeout\n+    while time.time() < deadline:\n+        if mock.called:\n+            return\n+        time.sleep(0.02)\n+    raise AssertionError(f\"{mock!r} was not called within {timeout}s\")\n+\n+\n+def test_anthropic_non_streaming_stale_aborts_request_client_not_shared():\n+    \"\"\"Stale non-streaming Anthropic call: the poll thread aborts the\n+    request-local client's socket; the shared client is never closed/rebuilt,\n+    and the worker still unblocks and closes its own client (no #28161 hang).\"\"\"\n+    agent = _make_anthropic_agent()\n+    agent._compute_non_stream_stale_timeout.return_value = 0.05\n+    agent._codex_silent_hang_hint = MagicMock(return_value=None)\n+\n+    request_client = MagicMock()\n+    agent._create_request_anthropic_client = MagicMock(return_value=request_client)\n+    agent._abort_request_anthropic_client = MagicMock()\n+    agent._close_request_anthropic_client = MagicMock()\n+\n+    def _create(_api_kwargs, *, client):\n+        assert client is request_client\n+        # Outlive the 0.05s stale timeout AND the worker join (2.0s) so the\n+        # stale detector surfaces its TimeoutError.\n+        time.sleep(2.5)\n+        return object()\n+\n+    agent._anthropic_messages_create = MagicMock(side_effect=_create)\n+\n+    with pytest.raises(TimeoutError):\n+        cch.interruptible_api_call(agent, {\"model\": \"x\", \"messages\": []})\n+\n+    # Shared client untouched from the poll thread.\n+    agent._anthropic_client.close.assert_not_called()\n+    agent._rebuild_anthropic_client.assert_not_called()\n+    # Poll (stranger) thread aborts the request-local client's socket only.\n+    agent._abort_request_anthropic_client.assert_called_once_with(\n+        request_client, reason=\"stale_call_kill\"\n+    )\n+    # Worker unblocks and closes its own request client from its own thread.\n+    _wait_for_mock_call(agent._close_request_anthropic_client)\n+\n+\n+def test_anthropic_non_streaming_interrupt_aborts_request_client_not_shared():\n+    \"\"\"Interrupted non-streaming Anthropic call: near-instant InterruptedError,\n+    request-local client aborted from the poll thread, shared client untouched.\"\"\"\n+    agent = _make_anthropic_agent()\n+\n+    request_client = MagicMock()\n+    agent._create_request_anthropic_client = MagicMock(return_value=request_client)\n+    agent._abort_request_anthropic_client = MagicMock()\n+    agent._close_request_anthropic_client = MagicMock()\n+\n+    def _create(_api_kwargs, *, client):\n+        assert client is request_client\n+        agent._interrupt_requested = True\n+        time.sleep(1.0)\n+        raise httpx.RemoteProtocolError(\"forced close would have happened\")\n+\n+    agent._anthropic_messages_create = MagicMock(side_effect=_create)\n+\n+    t0 = time.time()\n+    with pytest.raises(InterruptedError):\n+        cch.interruptible_api_call(agent, {\"model\": \"x\", \"messages\": []})\n+    elapsed = time.time() - t0\n+\n+    assert elapsed < 3.0, f\"interrupt took {elapsed:.1f}s — should be near-instant\"\n+    agent._anthropic_client.close.assert_not_called()\n+    agent._rebuild_anthropic_client.assert_not_called()\n+    agent._abort_request_anthropic_client.assert_called_once_with(\n+        request_client, reason=\"interrupt_abort\"\n+    )"]},{"file":"tests/run_agent/test_28161_anthropic_stream_pool_cleanup.py","hunks":["@@ -1,16 +1,24 @@\n-\"\"\"Anthropic stream cleanup must call _anthropic_client.close() + _rebuild_anthropic_client(),\n-not _replace_primary_openai_client(), to avoid 15-minute hangs on Anthropic-native configs.\n-\n-Three cleanup sites in chat_completion_helpers.interruptible_streaming_api_call() were\n-calling _replace_primary_openai_client() unconditionally.  For api_mode=anthropic_messages\n-this silently fails (no OPENAI_API_KEY) and leaves the in-flight httpx stream unclosed,\n-blocking the worker thread until the 900s httpx read-timeout fires.\n+\"\"\"Anthropic stream cleanup must not call _replace_primary_openai_client() and\n+must not hang on Anthropic-native configs (#28161), now via the request-local\n+client model (#67142).\n+\n+Originally three cleanup sites in interruptible_streaming_api_call() called\n+_replace_primary_openai_client() unconditionally; for api_mode=anthropic_messages\n+that silently failed (no OPENAI_API_KEY) and left the in-flight httpx stream\n+unclosed, blocking the worker until the 900s read-timeout fired.\n+\n+Since #67142, anthropic streams run on a per-request client: the stale/retry\n+cleanup closes the *request-local* client (worker-owned) and builds a fresh one\n+next attempt — the shared _anthropic_client is never closed/rebuilt from inside\n+a request (that poll-thread close was the TLS-FD→SQLite corruption vector). The\n+no-hang guarantee is preserved because the poll thread aborts the request\n+client's sockets, which unblocks the worker.\n \n Tests cover:\n-- stream_retry_pool_cleanup  (connection error on fresh stream, L1836)\n-- stale_stream_pool_cleanup  (outer poll loop detects stale stream, L1987)\n+- stream_retry cleanup  (connection error on fresh stream)\n+- stale_stream cleanup  (outer poll loop detects stale stream)\n \n-Fixes #28161\n+Fixes #28161. Extends #67142.\n \"\"\"\n import threading\n from types import SimpleNamespace","@@ -41,6 +49,9 @@ def _make_anthropic_agent(**kwargs):\n     agent.api_mode = \"anthropic_messages\"\n     agent._anthropic_client = MagicMock()\n     agent._anthropic_api_key = \"[redacted:secret-assignment]\"\n+    # #67142: anthropic streams now run on a request-local client; route it to\n+    # the test mock so .messages.stream is exercised and its cleanup observed.\n+    agent._create_request_anthropic_client = lambda *a, **k: agent._anthropic_client\n     return agent\n \n ","@@ -74,13 +85,16 @@ def _failing_stream_cm():\n \n \n class TestAnthropicStreamPoolCleanup:\n-    \"\"\"_replace_primary_openai_client must not be called for api_mode=anthropic_messages.\"\"\"\n+    \"\"\"Anthropic cleanup must never touch the OpenAI primary or the shared\n+    Anthropic client, and must not hang (#28161 / #67142).\"\"\"\n \n     @pytest.mark.filterwarnings(\n         \"ignore::pytest.PytestUnhandledThreadExceptionWarning\"\n     )\n-    def test_stream_retry_calls_anthropic_rebuild_not_openai(self):\n-        \"\"\"Connection error during stream retry → close+rebuild Anthropic client, not OpenAI.\"\"\"\n+    def test_stream_retry_closes_request_client_not_openai(self):\n+        \"\"\"Connection error during stream retry → close the request-local\n+        Anthropic client (worker-owned) and retry; never rebuild the shared\n+        Anthropic client, never touch the OpenAI primary.\"\"\"\n         agent = _make_anthropic_agent()\n \n         attempt_count = [0]","@@ -100,14 +114,19 @@ def _stream_side_effect(*args, **kwargs):\n                 agent._interruptible_streaming_api_call({})\n \n         mock_replace.assert_not_called()\n-        mock_rebuild.assert_called_once()\n-        agent._anthropic_client.close.assert_called_once()\n+        # #67142: the shared client is never rebuilt from inside a request; the\n+        # request-local client (routed to this mock) is closed instead.\n+        mock_rebuild.assert_not_called()\n+        agent._anthropic_client.close.assert_called()\n+        assert attempt_count[0] == 2  # retried once, then succeeded\n \n     @pytest.mark.filterwarnings(\n         \"ignore::pytest.PytestUnhandledThreadExceptionWarning\"\n     )\n-    def test_stale_stream_calls_anthropic_rebuild_not_openai(self, monkeypatch):\n-        \"\"\"Stale-stream outer-poll detector → close+rebuild Anthropic client, not OpenAI.\"\"\"\n+    def test_stale_stream_aborts_request_client_not_openai(self, monkeypatch):\n+        \"\"\"Stale-stream outer-poll detector → abort the request-local client's\n+        socket (unblocking the worker) and retry; never _replace_primary_openai\n+        and never rebuild the shared Anthropic client.\"\"\"\n         monkeypatch.setenv(\"HERMES_STREAM_STALE_TIMEOUT\", \"0.1\")\n \n         agent = _make_anthropic_agent()","@@ -117,14 +136,15 @@ def test_stale_stream_calls_anthropic_rebuild_not_openai(self, monkeypatch):\n         def _stream_side_effect(*args, **kwargs):\n             attempt_count[0] += 1\n             if attempt_count[0] == 1:\n-                # First attempt: stream that yields nothing (triggers stale detector),\n-                # then raises ConnectError once _anthropic_client.close() unblocks it.\n+                # First attempt: stream that yields nothing (triggers stale\n+                # detector), then raises ConnectError once the poll thread\n+                # aborts the request client's socket.\n                 cm = MagicMock()\n                 stream = MagicMock()\n \n                 def _blocking_gen():\n                     unblock.wait(timeout=5.0)\n-                    raise httpx.ConnectError(\"connection dropped after close()\")\n+                    raise httpx.ConnectError(\"connection dropped after abort\")\n                     yield  # make this a generator so next() triggers the wait\n \n                 stream.__iter__ = MagicMock(return_value=_blocking_gen())","@@ -135,8 +155,10 @@ def _blocking_gen():\n             return _good_stream_cm()\n \n         agent._anthropic_client.messages.stream.side_effect = _stream_side_effect\n-        # close() on the mock Anthropic client unblocks the inner thread.\n-        agent._anthropic_client.close.side_effect = unblock.set\n+        # #67142: the stale detector aborts the request-local client's sockets\n+        # from the poll thread (not close() on the shared client); simulate the\n+        # socket shutdown waking the blocked read.\n+        agent._abort_request_anthropic_client = lambda *a, **k: unblock.set()\n \n         with patch.object(agent, \"_rebuild_anthropic_client\") as mock_rebuild:\n             with patch.object(","@@ -145,6 +167,6 @@ def _blocking_gen():\n                 agent._interruptible_streaming_api_call({})\n \n         mock_replace.assert_not_called()\n-        # close() and rebuild called at least once by the stale detector.\n-        agent._anthropic_client.close.assert_called()\n-        assert mock_rebuild.call_count >= 1\n+        # The shared Anthropic client is never rebuilt from inside a request.\n+        mock_rebuild.assert_not_called()\n+        assert attempt_count[0] >= 2  # stale-killed once, then retried"]},{"file":"tests/run_agent/test_run_agent.py","hunks":["@@ -7648,13 +7648,60 @@ def test_interruptible_has_anthropic_branch(self):\n         assert \"anthropic_messages\" in source, \\\n             \"interruptible_api_call must handle Anthropic interrupt (api_mode check)\"\n \n-    def test_interruptible_rebuilds_anthropic_client(self):\n-        \"\"\"After interrupting, the Anthropic client should be rebuilt.\"\"\"\n-        import inspect\n+    def test_interruptible_anthropic_interrupt_never_closes_shared_client(self):\n+        \"\"\"#67142: a non-streaming Anthropic interrupt must abort the\n+        request-local client from the poll thread, never close/rebuild the\n+        shared _anthropic_client (which raced a live SSL BIO and corrupted an\n+        unrelated SQLite DB via TLS-FD recycling).\n+\n+        Replaces the former source-reading assertion (which asserted the old,\n+        now-removed rebuild-on-interrupt behavior) with a behavior test.\n+        \"\"\"\n+        import threading\n+        import time\n+        from unittest.mock import MagicMock\n+        from run_agent import AIAgent\n         from agent.chat_completion_helpers import interruptible_api_call\n-        source = inspect.getsource(interruptible_api_call)\n-        assert \"build_anthropic_client\" in source, \\\n-            \"interruptible_api_call must rebuild Anthropic client after interrupt\"\n+\n+        agent = AIAgent(\n+            api_key=\"[redacted:secret-assignment]\",\n+            base_url=\"https://api.anthropic.com\",\n+            provider=\"anthropic\",\n+            model=\"claude-test\",\n+            quiet_mode=True,\n+            skip_context_files=True,\n+            skip_memory=True,\n+        )\n+        agent.api_mode = \"anthropic_messages\"\n+        agent._interrupt_requested = False\n+        agent._anthropic_client = MagicMock()\n+        agent._rebuild_anthropic_client = MagicMock()\n+        request_client = MagicMock()\n+        agent._create_request_anthropic_client = MagicMock(return_value=request_client)\n+        agent._abort_request_anthropic_client = MagicMock()\n+        agent._close_request_anthropic_client = MagicMock()\n+\n+        def _create(_api_kwargs, *, client):\n+            assert client is request_client\n+            agent._interrupt_requested = True\n+            time.sleep(1.0)\n+            raise RuntimeError(\"forced close would have happened\")\n+\n+        agent._anthropic_messages_create = MagicMock(side_effect=_create)\n+\n+        t0 = time.time()\n+        with pytest.raises(InterruptedError):\n+            interruptible_api_call(agent, {\"model\": \"x\", \"messages\": []})\n+        elapsed = time.time() - t0\n+\n+        assert elapsed < 3.0, f\"interrupt took {elapsed:.1f}s — should be near-instant\"\n+        # The shared client is never closed/rebuilt from the poll thread.\n+        agent._anthropic_client.close.assert_not_called()\n+        agent._rebuild_anthropic_client.assert_not_called()\n+        # The poll (stranger) thread aborts the request-local client's socket.\n+        agent._abort_request_anthropic_client.assert_called_once_with(\n+            request_client, reason=\"interrupt_abort\"\n+        )\n \n     def test_streaming_has_anthropic_branch(self):\n         \"\"\"_streaming_api_call must also handle Anthropic interrupt.\"\"\""]},{"file":"tests/run_agent/test_stream_stale_circuit_breaker.py","hunks":["@@ -39,6 +39,9 @@ def _make_anthropic_agent(**kwargs):\n     agent.api_mode = \"anthropic_messages\"\n     agent._anthropic_client = MagicMock()\n     agent._anthropic_api_key = \"[redacted:secret-assignment]\"\n+    # #67142: anthropic streams now run on a request-local client; route it to\n+    # the test mock so .messages.stream is exercised.\n+    agent._create_request_anthropic_client = lambda *a, **k: agent._anthropic_client\n     return agent\n \n ","@@ -116,7 +119,10 @@ def _stream_side_effect(*args, **kwargs):\n \n         # Every attempt blocks, trips the stale detector, and fails.\n         agent._anthropic_client.messages.stream.side_effect = _stream_side_effect\n-        agent._anthropic_client.close.side_effect = unblock.set\n+        # #67142: the stale detector now aborts the request-local client's\n+        # sockets from the poll thread (not close() on the shared client), so\n+        # unblock on the abort to simulate the socket shutdown waking the read.\n+        agent._abort_request_anthropic_client = lambda *a, **k: unblock.set()\n \n         with pytest.raises(Exception):\n             agent._interruptible_streaming_api_call({})"]},{"file":"tests/run_agent/test_streaming.py","hunks":["@@ -1196,6 +1196,9 @@ def test_anthropic_stream_refreshes_activity_on_every_event(self):\n \n         agent._anthropic_client = MagicMock()\n         agent._anthropic_client.messages.stream.return_value = mock_stream\n+        # #67142: streaming now runs on a request-local anthropic client; route\n+        # it to the test mock so .messages.stream is exercised.\n+        agent._create_request_anthropic_client = lambda *a, **k: agent._anthropic_client\n \n         agent._interruptible_streaming_api_call({})\n ","@@ -1251,16 +1254,18 @@ def __iter__(self):\n             _BadStream(),\n             good_stream,\n         ]\n+        agent._create_request_anthropic_client = lambda *a, **k: agent._anthropic_client\n \n         response = agent._interruptible_streaming_api_call({})\n \n         assert response is final_message\n         assert agent._anthropic_client.messages.stream.call_count == 2\n-        # Anthropic-native cleanup: close + rebuild the Anthropic client, never\n-        # the OpenAI primary client.\n+        # #67142: cleanup runs on the request-local anthropic client (closed,\n+        # worker-owned, via _close_request_client_once), never rebuilding the\n+        # shared client and never touching the OpenAI primary client.\n         assert mock_replace.call_count == 0\n-        assert mock_rebuild.call_count == 1\n-        assert agent._anthropic_client.close.call_count == 1\n+        assert mock_rebuild.call_count == 0\n+        assert agent._anthropic_client.close.call_count >= 1\n \n     @patch(\"run_agent.AIAgent._replace_primary_openai_client\")\n     def test_generic_anthropic_valueerror_still_propagates_without_stream_retry(","@@ -1286,6 +1291,7 @@ def test_generic_anthropic_valueerror_still_propagates_without_stream_retry(\n         agent._anthropic_client.messages.stream.side_effect = ValueError(\n             \"invalid local request shape\"\n         )\n+        agent._create_request_anthropic_client = lambda *a, **k: agent._anthropic_client\n \n         with pytest.raises(ValueError, match=\"invalid local request shape\"):\n             agent._interruptible_streaming_api_call({})","@@ -1326,15 +1332,17 @@ def test_anthropic_zero_event_stream_retried_as_transient(\n \n         agent._anthropic_client = MagicMock()\n         agent._anthropic_client.messages.stream.return_value = empty_stream\n+        agent._create_request_anthropic_client = lambda *a, **k: agent._anthropic_client\n \n         with pytest.raises(EmptyStreamError):\n             agent._interruptible_streaming_api_call({})\n \n         assert agent._anthropic_client.messages.stream.call_count == 3\n-        # Anthropic-native cleanup between attempts: rebuild the Anthropic\n-        # client, never the OpenAI primary client.\n+        # #67142: cleanup between attempts runs on the request-local anthropic\n+        # client (fresh one built per attempt); the shared client is never\n+        # rebuilt and the OpenAI primary client is never touched.\n         assert mock_replace.call_count == 0\n-        assert mock_rebuild.call_count == 2\n+        assert mock_rebuild.call_count == 0\n \n     @patch(\"run_agent.AIAgent._try_refresh_anthropic_client_credentials\")\n     @patch(\"run_agent.AIAgent._rebuild_anthropic_client\")","@@ -1369,13 +1377,14 @@ def test_anthropic_eventless_sdk_assertion_normalized_to_empty_stream(\n \n         agent._anthropic_client = MagicMock()\n         agent._anthropic_client.messages.stream.return_value = empty_stream\n+        agent._create_request_anthropic_client = lambda *a, **k: agent._anthropic_client\n \n         with pytest.raises(EmptyStreamError):\n             agent._interruptible_streaming_api_call({})\n \n         assert agent._anthropic_client.messages.stream.call_count == 3\n         assert mock_replace.call_count == 0\n-        assert mock_rebuild.call_count == 2\n+        assert mock_rebuild.call_count == 0\n \n \n class TestPartialToolCallWarning:\n"]}]},{"kind":"pr","number":67270,"title":"fix(agent): prevent Anthropic stale-stream SQLite corruption via _abort_anthropic_client (#67142)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"e53f87fe6925f9b0575f16b814d8994dfd455128","number":0},{"kind":"pr","value":"#67238","number":67238},{"kind":"pr","value":"#67142","number":67142}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}