can1357/oh-my-pi · error

Client not started

Error message

Client not started

What it means

#send() throws synchronously if the client has no spawned process/stdin — i.e. start() was never called, or stop()/process exit already cleared this.#process. No RPC command can be written without an established child process.

Solutions

  1. await client.start() before issuing any command.
  2. Check the client's running state before sending, especially after stop().
  3. Re-create and start a client after the previous one was stopped or crashed.
  4. Serialize your lifecycle: gate commands on a ready-promise so they wait for start to finish.

Example fix

// before
const client = new RpcClient(opts);
const msgs = await client.getMessages(); // throws
// after
const client = new RpcClient(opts);
await client.start();
const msgs = await client.getMessages();
Defensive patterns

Strategy: try-catch

Validate before calling

if (!client.isRunning()) await client.start();

Try / catch

try {
  const res = await client.sendCommand(cmd);
} catch (err) {
  if ((err as Error).message === "Client not started") {
    await client.start();
  } else throw err;
}

Prevention

When it happens

Trigger: Calling any command that routes through #send before start(), or after stop() or an unexpected child exit cleared this.#process.

Common situations: Forgetting to await client.start() during setup; issuing commands after the session was stopped; a crash of the child process leaving the client without a process; async races where a queued command fires post-stop.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/347fad1ea6ced608. Report an issue: GitHub.

Appendix: source

Thrown at packages/coding-agent/src/modes/rpc/rpc-client.ts:1128

			return;
		}

		if (!isAgentSessionEvent(data)) return;

		for (const listener of this.#sessionEventListeners) {
			listener(data);
		}

		if (!isAgentEvent(data)) return;

		for (const listener of this.#eventListeners) {
			listener(data);
		}
	}

	#send(command: RpcCommandBody, timeoutMs = 30_000): Promise<RpcResponse> {
		if (!this.#process?.stdin) {
			throw new Error("Client not started");
		}

		const id = `req_${++this.#requestId}`;
		const fullCommand = { ...command, id } as RpcCommand;
		const { promise, resolve, reject } = Promise.withResolvers<RpcResponse>();
		let settled = false;
		const timeoutId = this.#startTimeout(timeoutMs, () => {
			if (settled) return;
			this.#pendingRequests.delete(id);
			settled = true;
			reject(
				new Error(`Timeout waiting for response to ${command.type}. Stderr: ${this.#process?.peekStderr() ?? ""}`),
			);
		});

		this.#pendingRequests.set(id, {
			resolve: response => {
				if (settled) return;

View on GitHub (pinned to 9690622007)