github/copilot-sdk · error · RuntimeError

Client not started. Call start() first.

Error message

Client not started. Call start() first.

What it means

JsonRpcClient.request requires the client's background reader loop to exist; it is created by start(). If request() is called before start(), there is no loop to attach futures to and the library raises this error.

Solutions

  1. Call client.start() before issuing any requests.
  2. Recreate or restart the client if its loop was previously stopped.
  3. Add a lifecycle check that the client is running before sending RPCs.
  4. Check for races where shutdown happens before pending requests.

Example fix

# before
client = JsonRpcClient(...)
await client.request("ping")  # raises
# after
client = JsonRpcClient(...)
client.start()
await client.request("ping")
Defensive patterns

Strategy: validation

Validate before calling

if not client.is_running:
    client.start()

Type guard

def client_ready(client) -> bool:
    return getattr(client, "_loop", None) is not None

Try / catch

try:
    resp = await client.request(method, params)
except RuntimeError as e:
    if "Client not started" in str(e):
        client.start(); resp = await client.request(method, params)

Prevention

When it happens

Trigger: Calling request() (or any RPC relying on it) on a JsonRpcClient instance whose start() has not been called, or after the loop was shut down and set to None.

Common situations: Forgetting to call start() after constructing the client; reusing a client after stop/dispose; a race where a request is issued during startup.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09). Data as JSON: /api/errors/7bf378d5fa624f72. Report an issue: GitHub.

Appendix: source

Thrown at python/copilot/_jsonrpc.py:175

                Use this to perform state mutations (for example, registering
                a server-assigned session id) that must be visible before any
                subsequent notification on the same connection is dispatched.
                The callback receives the parsed JSON result. If the callback
                raises, the exception is propagated to the awaiter.

        Returns:
            The result from the response

        Raises:
            JsonRpcError: If the server returns an error
            asyncio.TimeoutError: If the request times out (only when timeout is set)
        """
        request_start = time.perf_counter()
        request_id = str(uuid.uuid4())

        # Use the stored loop to ensure consistency with the reader thread
        if not self._loop:
            raise RuntimeError("Client not started. Call start() first.")

        future = self._loop.create_future()
        with self._pending_lock:
            self.pending_requests[request_id] = future
            if on_response_inline is not None:
                self._pending_inline_callbacks[request_id] = on_response_inline

        message = {
            "jsonrpc": "2.0",
            "id": request_id,
            "method": method,
            "params": params or {},
        }

        try:
            await self._send_message(message)
            if timeout is not None:
                result = await asyncio.wait_for(future, timeout=timeout)

View on GitHub (pinned to cd8cf15dc3)