NousResearch/hermes-agent · error · LSPProtocolError

cannot send {method!r}: stdin closed

Error message

cannot send {method!r}: stdin closed

What it means

LSPProtocolError raised by _send_request when the server process is gone or its stdin pipe is closing — the request can never be delivered. It fires before any bytes are written, distinguishing it from the BrokenPipe 'send failed' variant: here the client already knows the transport is dead.

Source

Thrown at agent/lsp/client.py:513

                proc.terminate()
                try:
                    await asyncio.wait_for(proc.wait(), timeout=SHUTDOWN_GRACE)
                except asyncio.TimeoutError:
                    try:
                        proc.kill()
                        await proc.wait()
                    except ProcessLookupError:
                        pass
            except ProcessLookupError:
                pass

    # ------------------------------------------------------------------
    # request / notification plumbing
    # ------------------------------------------------------------------

    async def _send_request(self, method: str, params: Any) -> Any:
        if self._proc is None or self._proc.stdin is None or self._proc.stdin.is_closing():
            raise LSPProtocolError(f"cannot send {method!r}: stdin closed")
        loop = asyncio.get_running_loop()
        req_id = self._next_id
        self._next_id += 1
        fut: asyncio.Future = loop.create_future()
        self._pending[req_id] = fut
        try:
            self._proc.stdin.write(encode_message(make_request(req_id, method, params)))
            await self._proc.stdin.drain()
        except (BrokenPipeError, ConnectionResetError, OSError) as e:
            self._pending.pop(req_id, None)
            raise LSPProtocolError(f"send failed for {method!r}: {e}") from e
        try:
            return await fut
        finally:
            self._pending.pop(req_id, None)

    async def _send_request_with_retry(self, method: str, params: Any, *, timeout: float) -> Any:
        """Send a request, retrying on ``ContentModified`` (-32801).

View on GitHub (pinned to c896c09c42)

Solutions

  1. Check client.is_running before issuing requests and restart the client if it exited.
  2. Register for the client's exit/error callback and fail fast in your own code when the server dies instead of continuing to send.
  3. Investigate why the server terminated (stderr is drained at debug level — enable debug logs to see the crash reason).

Example fix

# before
hover = await client.request("textDocument/hover", params)  # may raise: stdin closed

# after
if not client.is_running:
    await client.start()  # or recreate the client
hover = await client.request("textDocument/hover", params)
Defensive patterns

Strategy: try-catch

Validate before calling

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

Try / catch

try:
    result = await client.request(method, params)
except LSPProtocolError as e:
    if "stdin closed" in str(e) or "send failed" in str(e):
        client = await restart_client(client)  # fresh process + re-open docs
        result = await client.request(method, params)
    else:
        raise

Prevention

When it happens

Trigger: Calling any request API (hover, completions, definitions) after the server crashed, after stop() was called, or after the reader loop detected EOF and tore down the process while pending futures were never awaited by the caller.

Common situations: Server process killed by OOM or by the user mid-session; reusing a client object across a restart; requests racing shutdown in tests.

Related errors


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