can1357/oh-my-pi · error
Client already started
Error message
Client already started
What it means
RpcClient.start() is a lifecycle guard: it awaits any pending reaping, then throws if this.#process is already set. The client forbids double-start so a second spawn cannot orphan a running child process or clobber its abort controller.
Source
Thrown at packages/coding-agent/src/modes/rpc/rpc-client.ts:303
#extensionUiListeners: Set<(req: RpcExtensionUIRequest) => void> = new Set();
#abortController = new AbortController();
constructor(private options: RpcClientOptions = {}) {
this.#customTools = [...(options.customTools ?? [])];
}
/**
* Start the RPC agent process.
*
* Safe to call again after {@link stop} on the same instance: a fresh
* {@link AbortController} is minted for each start, and any failure after
* the child spawn kills the child and clears internal state so callers may
* retry without leaking processes.
*/
async start(): Promise<void> {
await this.#reaping;
if (this.#process) {
throw new Error("Client already started");
}
// Mint a fresh controller so a previous stop()'s abort does not
// short-circuit the new stdout reader (issue #4079).
this.#abortController = new AbortController();
this.#protocolVersion = 1;
const cliPath = this.options.cliPath ?? "dist/cli.js";
const args = ["--mode", "rpc"];
if (this.options.provider) {
args.push("--provider", this.options.provider);
}
if (this.options.model) {
args.push("--model", this.options.model);
}
if (this.options.sessionDir) {
args.push("--session-dir", this.options.sessionDir);View on GitHub (pinned to 9690622007)
Solutions
- Call stop() before start() again, or make the caller idempotent: skip start when the client is already running.
- Guard with a running check: only start if not already running.
- Create a fresh RpcClient instance instead of reusing the started one.
- If the old process crashed, start() already awaits #reaping, so a completed reap makes start safe — just retry once.
Example fix
// before await client.start(); await client.start(); // throws // after if (!client.isRunning()) await client.start();
Defensive patterns
Strategy: try-catch
Validate before calling
if (client.isRunning?.()) return; // already started, skip
Try / catch
try {
await client.start();
} catch (err) {
if ((err as Error).message === "Client already started") return; // idempotent start
throw err;
} Prevention
- Make start idempotent in your wrapper layer.
- Always pair start() with stop() in lifecycle hooks (mount/unmount).
- Never retry start() blindly on failure without stop() first.
- Use a single owner for the client lifecycle.
When it happens
Trigger: Calling start() (e.g. from a __enter__/init wrapper) twice on the same RpcClient without stop() in between — this.#process is non-null on the second call.
Common situations: Framework re-entry calling an init/setup method more than once (double-mount of a UI component, hot-reload re-running setup code, retry logic that forgets the first start succeeded).
Related errors
- Client not started
- bridge call {name!r} failed
- Daemon broker client is closed
- LSP mux already listening on ${endpoint}
- Host URI write failed for ${url.href}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/05326e1b63ec0b48.
Report an issue: GitHub.