NousResearch/hermes-agent

TCP keepalives for detecting dropped provider connections

Cluster 10324. Data as of

Nothing open on GitHub. Fix merged in #11277 on 17 Apr 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #11277. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #10933. That is separate from the fix event.
Still open
Nothing open on GitHub.

These issues and PRs address a bug where the agent hangs indefinitely in CLOSE-WAIT state when a custom provider drops a connection mid-response. The fix enables TCP keepalives to detect dead provider connections.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #10933 fix: enable TCP keepalives to detect dead provider connections

    merged to main

    files: run_agent.py

    Show the diff
    • run_agent.py
      @@ -4366,6 +4366,29 @@ def _create_openai_client(self, client_kwargs: dict, *, reason: str, shared: boo
                       self._client_log_context(),
                   )
                   return client
      +        # Inject TCP keepalives to detect dead connections faster (#10324).
      +        # Without keepalives, a provider that drops mid-stream leaves the
      +        # socket in CLOSE-WAIT and epoll_wait may never fire, causing the
      +        # agent to hang indefinitely.  Keepalive probes detect the dead
      +        # peer within ~60s (30s idle + 3×10s probes).
      +        if "http_client" not in client_kwargs:
      +            try:
      +                import httpx as _httpx
      +                import socket as _socket
      +                _sock_opts = [(_socket.SOL_SOCKET, _socket.SO_KEEPALIVE, 1)]
      +                if hasattr(_socket, "TCP_KEEPIDLE"):
      +                    # Linux
      +                    _sock_opts.append((_socket.IPPROTO_TCP, _socket.TCP_KEEPIDLE, 30))
      +                    _sock_opts.append((_socket.IPPROTO_TCP, _socket.TCP_KEEPINTVL, 10))
      +                    _sock_opts.append((_socket.IPPROTO_TCP, _socket.TCP_KEEPCNT, 3))
      +                elif hasattr(_socket, "TCP_KEEPALIVE"):
      +                    # macOS (uses TCP_KEEPALIVE instead of TCP_KEEPIDLE)
      +                    _sock_opts.append((_socket.IPPROTO_TCP, _socket.TCP_KEEPALIVE, 30))
      +                client_kwargs["http_client"] = _httpx.Client(
      +                    transport=_httpx.HTTPTransport(socket_options=_sock_opts),
      +                )
      +            except Exception:
      +                pass  # Fall through to default transport if socket opts fail
               client = OpenAI(**client_kwargs)
               logger.info(
                   "OpenAI client created (%s, shared=%s) %s",
  • #11277 fix: enable TCP keepalives to detect dead provider connections (#10324)

    merged to main

    files: run_agent.py

    Show the diff
    • run_agent.py
      @@ -4395,6 +4395,41 @@ def _create_openai_client(self, client_kwargs: dict, *, reason: str, shared: boo
                       self._client_log_context(),
                   )
                   return client
      +        # Inject TCP keepalives so the kernel detects dead provider connections
      +        # instead of letting them sit silently in CLOSE-WAIT (#10324).  Without
      +        # this, a peer that drops mid-stream leaves the socket in a state where
      +        # epoll_wait never fires, ``httpx`` read timeout may not trigger, and
      +        # the agent hangs until manually killed.  Probes after 30s idle, retry
      +        # every 10s, give up after 3 → dead peer detected within ~60s.
      +        #
      +        # Safety against #10933: the ``client_kwargs = dict(client_kwargs)``
      +        # above means this injection only lands in the local per-call copy,
      +        # never back into ``self._client_kwargs``.  Each ``_create_openai_client``
      +        # invocation therefore gets its OWN fresh ``httpx.Client`` whose
      +        # lifetime is tied to the OpenAI client it is passed to.  When the
      +        # OpenAI client is closed (rebuild, teardown, credential rotation),
      +        # the paired ``httpx.Client`` closes with it, and the next call
      +        # constructs a fresh one — no stale closed transport can be reused.
      +        # Tests in ``tests/run_agent/test_create_openai_client_reuse.py`` and
      +        # ``tests/run_agent/test_sequential_chats_live.py`` pin this invariant.
      +        if "http_client" not in client_kwargs:
      +            try:
      +                import httpx as _httpx
      +                import socket as _socket
      +                _sock_opts = [(_socket.SOL_SOCKET, _socket.SO_KEEPALIVE, 1)]
      +                if hasattr(_socket, "TCP_KEEPIDLE"):
      +                    # Linux
      +                    _sock_opts.append((_socket.IPPROTO_TCP, _socket.TCP_KEEPIDLE, 30))
      +                    _sock_opts.append((_socket.IPPROTO_TCP, _socket.TCP_KEEPINTVL, 10))
      +                    _sock_opts.append((_socket.IPPROTO_TCP, _socket.TCP_KEEPCNT, 3))
      +                elif hasattr(_socket, "TCP_KEEPALIVE"):
      +                    # macOS (uses TCP_KEEPALIVE instead of TCP_KEEPIDLE)
      +                    _sock_opts.append((_socket.IPPROTO_TCP, _socket.TCP_KEEPALIVE, 30))
      +                client_kwargs["http_client"] = _httpx.Client(
      +                    transport=_httpx.HTTPTransport(socket_options=_sock_opts),
      +                )
      +            except Exception:
      +                pass  # Fall through to default transport if socket opts fail
               client = OpenAI(**client_kwargs)
               logger.info(
                   "OpenAI client created (%s, shared=%s) %s",
Closed issues · 1
  • Closed#10324Agent hangs indefinitely on CLOSE-WAIT when custom provider drops connection mid-response
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Merged PRs
#10933, #11277
Closed issues
1
Attempts
0 PRs closed without merging
Fix link checked
17 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.

For your agent

The same facts, typed.