{"record":{"id":"14c9bd01dccdb843","repo":"NousResearch/hermes-agent","slug":"codex-app-server-stdin-closed-unexpectedly-exc","errorCode":null,"errorMessage":"codex app-server stdin closed unexpectedly: {exc}","messagePattern":"codex app-server stdin closed unexpectedly: (.+?)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"agent/transports/codex_app_server.py","lineNumber":310,"sourceCode":"\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()\n                if not line:\n                    continue\n                try:\n                    msg = json.loads(line)\n                except json.JSONDecodeError:\n                    # Non-JSON output is unexpected on stdout; tracing belongs\n                    # on stderr. Surface it via stderr buffer for diagnostics.","sourceCodeStart":292,"sourceCodeEnd":328,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/agent/transports/codex_app_server.py#L292-L328","documentation":"RuntimeError from _send() (agent/transports/codex_app_server.py:310) wrapping BrokenPipeError/ValueError raised while writing a JSON-RPC frame to the app-server's stdin. It means the write side is gone in practice: the codex subprocess has exited (or closed stdin), so the pipe is broken. ValueError is included because writing to a closed file object raises it.","triggerScenarios":"The codex app-server process crashed or exited (bad args, panic, OOM-kill, explicit quit) while the client still had queued requests; sends from a background thread after the reader observed EOF; flushing after the child died mid-request.","commonSituations":"Codex binary mismatch making the process exit at startup; the child killed by the OS; a close() race where another writer already flushed; diagnosing this usually reveals an earlier stderr message from codex that was swallowed.","solutions":["Capture and log the subprocess's stderr — it usually contains the reason codex exited.","Check _proc.poll() after this error; a non-None exit code confirms the child died and the client must be recreated.","Validate the codex command/version before spawning (bad flags cause immediate exit).","Treat this error as terminal for the connection: tear down and respawn rather than retrying sends."],"exampleFix":"# before\ntry:\n    client.request(\"thread/start\", params)\nexcept RuntimeError:\n    retry()  # pointless: the child is dead\n\n# after\ntry:\n    client.request(\"thread/start\", params)\nexcept RuntimeError as exc:\n    if client._proc.poll() is not None:\n        raise RuntimeError(\n            f\"codex exited rc={client._proc.returncode}: \"\n            f\"{client._proc.stderr.read().decode(errors='replace')[:500]}\"\n        ) from exc\n    raise","handlingStrategy":"try-catch","validationCode":"def child_healthy(proc) -> bool:\n    return proc.poll() is None and proc.stdin is not None and not proc.stdin.closed","typeGuard":null,"tryCatchPattern":"try:\n    client.request(method, params)\nexcept RuntimeError as exc:\n    if \"stdin closed unexpectedly\" in str(exc):\n        stderr = proc.stderr.read().decode(errors=\"replace\") if proc.stderr else \"\"\n        raise RuntimeError(f\"codex exited (rc={proc.poll()}): {stderr[:500]}\") from exc\n    raise","preventionTips":["Capture the child's stderr into a log from the start — it explains most exits.","Check proc.poll() after any transport error; a dead child means recreate, not retry.","Validate the codex command/version before spawn to avoid instant exits."],"tags":["subprocess","codex","broken-pipe","crash"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}