NousResearch/hermes-agent · error · RuntimeError

codex app-server client is closed

Error message

codex app-server client is closed

What it means

RuntimeError('codex app-server client is closed') from CodexAppServerClient._send() (agent/transports/codex_app_server.py:303). Every outgoing frame goes through _send, which refuses to write once _closed is set (by close()); it is a use-after-close guard on the client's stdin pipe.

Source

Thrown at agent/transports/codex_app_server.py:303

        with self._stderr_lock:
            return list(self._stderr_lines[-n:])

    def is_alive(self) -> bool:
        return self._proc.poll() is None

    # ---------- internals ----------

    def _take_id(self) -> int:
        # JSON-RPC ids only need to be unique per-connection. A simple
        # monotonically increasing int is the common choice and matches what
        # codex's own clients use.
        rid = self._next_id
        self._next_id += 1
        return rid

    def _send(self, obj: dict) -> None:
        if self._closed:
            raise RuntimeError("codex app-server client is closed")
        if self._proc.stdin is None:
            raise RuntimeError("codex app-server stdin not available")
        try:
            self._proc.stdin.write((json.dumps(obj) + "\n").encode("utf-8"))
            self._proc.stdin.flush()
        except (BrokenPipeError, ValueError) as exc:
            raise RuntimeError(
                f"codex app-server stdin closed unexpectedly: {exc}"
            ) from exc

    def _read_stdout(self) -> None:
        if self._proc.stdout is None:
            return
        try:
            for line in iter(self._proc.stdout.readline, b""):
                if not line:
                    break
                line = line.strip()

View on GitHub (pinned to c896c09c42)

Solutions

  1. Order operations so no sends happen after close(): cancel/await pending interactions before closing.
  2. Check the client's closed state before replying to server-initiated requests and drop stale replies.
  3. If a new conversation is needed, spawn a new client instead of reusing a closed one.

Example fix

# before
client.close()
client.respond(req_id, {"decision": "approved"})  # RuntimeError

# after
if not client._closed:  # or add a public `closed` property
    client.respond(req_id, {"decision": "approved"})
Defensive patterns

Strategy: validation

Validate before calling

def can_send(client) -> bool:
    return not client._closed and client._proc.stdin is not None

Try / catch

try:
    client.respond(req_id, result)
except RuntimeError as exc:
    if "closed" in str(exc):
        logger.debug("dropping reply to %r: client closed", req_id)
        return
    raise

Prevention

When it happens

Trigger: Calling request()/notify()/respond() after close() — e.g. a response to a server-initiated approval arriving after the session teardown closed the client, or a background thread racing shutdown.

Common situations: Approval/reply flows where the UI answers after the timeout path already closed the client; concurrent shutdown and in-flight requests; forgetting that close() is terminal for the whole client, not per-thread.

Related errors


AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14). Data as JSON: /api/errors/f42ac4fd72242aa7. Report an issue: GitHub.