{"record":{"id":"05326e1b63ec0b48","repo":"can1357/oh-my-pi","slug":"client-already-started","errorCode":null,"errorMessage":"Client already started","messagePattern":"Client already started","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/modes/rpc/rpc-client.ts","lineNumber":303,"sourceCode":"\t#extensionUiListeners: Set<(req: RpcExtensionUIRequest) => void> = new Set();\n\t#abortController = new AbortController();\n\n\tconstructor(private options: RpcClientOptions = {}) {\n\t\tthis.#customTools = [...(options.customTools ?? [])];\n\t}\n\n\t/**\n\t * Start the RPC agent process.\n\t *\n\t * Safe to call again after {@link stop} on the same instance: a fresh\n\t * {@link AbortController} is minted for each start, and any failure after\n\t * the child spawn kills the child and clears internal state so callers may\n\t * retry without leaking processes.\n\t */\n\tasync start(): Promise<void> {\n\t\tawait this.#reaping;\n\t\tif (this.#process) {\n\t\t\tthrow new Error(\"Client already started\");\n\t\t}\n\n\t\t// Mint a fresh controller so a previous stop()'s abort does not\n\t\t// short-circuit the new stdout reader (issue #4079).\n\t\tthis.#abortController = new AbortController();\n\t\tthis.#protocolVersion = 1;\n\n\t\tconst cliPath = this.options.cliPath ?? \"dist/cli.js\";\n\t\tconst args = [\"--mode\", \"rpc\"];\n\n\t\tif (this.options.provider) {\n\t\t\targs.push(\"--provider\", this.options.provider);\n\t\t}\n\t\tif (this.options.model) {\n\t\t\targs.push(\"--model\", this.options.model);\n\t\t}\n\t\tif (this.options.sessionDir) {\n\t\t\targs.push(\"--session-dir\", this.options.sessionDir);","sourceCodeStart":285,"sourceCodeEnd":321,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/modes/rpc/rpc-client.ts#L285-L321","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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."],"exampleFix":"// before\nawait client.start();\nawait client.start(); // throws\n// after\nif (!client.isRunning()) await client.start();","handlingStrategy":"try-catch","validationCode":"if (client.isRunning?.()) return; // already started, skip\n","typeGuard":null,"tryCatchPattern":"try {\n  await client.start();\n} catch (err) {\n  if ((err as Error).message === \"Client already started\") return; // idempotent start\n  throw err;\n}","preventionTips":["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."],"tags":["rpc","lifecycle","double-start","ipc"],"backgroundTag":"client-already-started","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}