{"record":{"id":"f42ac4fd72242aa7","repo":"NousResearch/hermes-agent","slug":"codex-app-server-client-is-closed","errorCode":null,"errorMessage":"codex app-server client is closed","messagePattern":"codex app-server client is closed","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"agent/transports/codex_app_server.py","lineNumber":303,"sourceCode":"        with self._stderr_lock:\n            return list(self._stderr_lines[-n:])\n\n    def is_alive(self) -> bool:\n        return self._proc.poll() is None\n\n    # ---------- internals ----------\n\n    def _take_id(self) -> int:\n        # JSON-RPC ids only need to be unique per-connection. A simple\n        # monotonically increasing int is the common choice and matches what\n        # codex's own clients use.\n        rid = self._next_id\n        self._next_id += 1\n        return rid\n\n    def _send(self, obj: dict) -> None:\n        if self._closed:\n            raise RuntimeError(\"codex app-server client is closed\")\n        if self._proc.stdin is None:\n            raise RuntimeError(\"codex app-server stdin not available\")\n        try:\n            self._proc.stdin.write((json.dumps(obj) + \"\\n\").encode(\"utf-8\"))\n            self._proc.stdin.flush()\n        except (BrokenPipeError, ValueError) as exc:\n            raise RuntimeError(\n                f\"codex app-server stdin closed unexpectedly: {exc}\"\n            ) from exc\n\n    def _read_stdout(self) -> None:\n        if self._proc.stdout is None:\n            return\n        try:\n            for line in iter(self._proc.stdout.readline, b\"\"):\n                if not line:\n                    break\n                line = line.strip()","sourceCodeStart":285,"sourceCodeEnd":321,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/agent/transports/codex_app_server.py#L285-L321","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Order operations so no sends happen after close(): cancel/await pending interactions before closing.","Check the client's closed state before replying to server-initiated requests and drop stale replies.","If a new conversation is needed, spawn a new client instead of reusing a closed one."],"exampleFix":"# before\nclient.close()\nclient.respond(req_id, {\"decision\": \"approved\"})  # RuntimeError\n\n# after\nif not client._closed:  # or add a public `closed` property\n    client.respond(req_id, {\"decision\": \"approved\"})","handlingStrategy":"validation","validationCode":"def can_send(client) -> bool:\n    return not client._closed and client._proc.stdin is not None","typeGuard":null,"tryCatchPattern":"try:\n    client.respond(req_id, result)\nexcept RuntimeError as exc:\n    if \"closed\" in str(exc):\n        logger.debug(\"dropping reply to %r: client closed\", req_id)\n        return\n    raise","preventionTips":["Sequence shutdown last: drain pending approvals/responses before close().","Treat close() as terminal — spawn a new client for new work.","Check the closed flag before replying to server-initiated requests."],"tags":["jsonrpc","codex","lifecycle","use-after-close"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}