NousResearch/hermes-agent · error · LSPProtocolError

client not running

Error message

client not running

What it means

LSPProtocolError raised by open_file when client.is_running is false — the caller asked to sync a file with a server that is not alive. It is a lifecycle misuse error: open_file must be called between start() and stop()/server exit.

Source

Thrown at agent/lsp/client.py:730

        # keep the Event sticky-set so any wait already in progress
        # resolves; waiters re-check their predicate after waking and
        # decide whether to keep waiting.  ``_push_counter`` is what
        # they actually compare against to detect a fresh event.
        self._push_counter += 1
        self._push_event.set()

    # ------------------------------------------------------------------
    # public file-sync API
    # ------------------------------------------------------------------

    async def open_file(self, path: str, *, language_id: str = "plaintext") -> int:
        """Send didOpen (first time) or didChange (subsequent) for ``path``.

        Returns the new document version number that the agent's
        ``wait_for_diagnostics`` should match against.
        """
        if not self.is_running:
            raise LSPProtocolError("client not running")

        abs_path = os.path.abspath(path)
        try:
            text = Path(abs_path).read_text(encoding="utf-8", errors="replace")
        except OSError as e:
            raise LSPProtocolError(f"cannot read {abs_path}: {e}") from e

        uri = file_uri(abs_path)
        doc = self._docs.get(abs_path)

        if doc is not None and doc.version >= 0:
            # Re-open: bump version, fire didChangeWatchedFiles + didChange.
            await self._send_notification(
                "workspace/didChangeWatchedFiles",
                {"changes": [{"uri": uri, "type": 2}]},  # 2 = CHANGED
            )
            new_version = doc.version + 1
            old_text = doc.text

View on GitHub (pinned to c896c09c42)

Solutions

  1. Await start() before any file sync, and guard every open_file with an is_running check.
  2. If the server crashed, restart the client (fresh process, re-open all relevant documents) rather than reusing the dead one.
  3. Propagate startup errors instead of ignoring them, so a not-running client is never silently used.

Example fix

# before
await client.open_file("src/main.py")  # may raise: client not running

# after
if not client.is_running:
    await client.start()
await client.open_file("src/main.py")
Defensive patterns

Strategy: type-guard

Validate before calling

if not client.is_running:
    await client.start()
version = await client.open_file(path)

Type guard

async def ensure_running(client) -> bool:
    """True if the client is alive; starts it if not yet started."""
    if client.is_running:
        return True
    try:
        await client.start()
        return client.is_running
    except LSPProtocolError:
        return False

Prevention

When it happens

Trigger: Calling open_file before start() completed, after stop(), or after the server process exited on its own; also when start() failed earlier (e.g. binary not found) and the failure was swallowed.

Common situations: Fire-and-forget startup where callers do not await start(); a crashed server leaving a half-alive client object; test fixtures reusing a stopped client.

Related errors


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