{"record":{"id":"347fad1ea6ced608","repo":"can1357/oh-my-pi","slug":"client-not-started","errorCode":null,"errorMessage":"Client not started","messagePattern":"Client not started","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/modes/rpc/rpc-client.ts","lineNumber":1128,"sourceCode":"\t\t\treturn;\n\t\t}\n\n\t\tif (!isAgentSessionEvent(data)) return;\n\n\t\tfor (const listener of this.#sessionEventListeners) {\n\t\t\tlistener(data);\n\t\t}\n\n\t\tif (!isAgentEvent(data)) return;\n\n\t\tfor (const listener of this.#eventListeners) {\n\t\t\tlistener(data);\n\t\t}\n\t}\n\n\t#send(command: RpcCommandBody, timeoutMs = 30_000): Promise<RpcResponse> {\n\t\tif (!this.#process?.stdin) {\n\t\t\tthrow new Error(\"Client not started\");\n\t\t}\n\n\t\tconst id = `req_${++this.#requestId}`;\n\t\tconst fullCommand = { ...command, id } as RpcCommand;\n\t\tconst { promise, resolve, reject } = Promise.withResolvers<RpcResponse>();\n\t\tlet settled = false;\n\t\tconst timeoutId = this.#startTimeout(timeoutMs, () => {\n\t\t\tif (settled) return;\n\t\t\tthis.#pendingRequests.delete(id);\n\t\t\tsettled = true;\n\t\t\treject(\n\t\t\t\tnew Error(`Timeout waiting for response to ${command.type}. Stderr: ${this.#process?.peekStderr() ?? \"\"}`),\n\t\t\t);\n\t\t});\n\n\t\tthis.#pendingRequests.set(id, {\n\t\t\tresolve: response => {\n\t\t\t\tif (settled) return;","sourceCodeStart":1110,"sourceCodeEnd":1146,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/modes/rpc/rpc-client.ts#L1110-L1146","documentation":"#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.","triggerScenarios":"Calling any command that routes through #send before start(), or after stop() or an unexpected child exit cleared this.#process.","commonSituations":"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.","solutions":["await client.start() before issuing any command.","Check the client's running state before sending, especially after stop().","Re-create and start a client after the previous one was stopped or crashed.","Serialize your lifecycle: gate commands on a ready-promise so they wait for start to finish."],"exampleFix":"// before\nconst client = new RpcClient(opts);\nconst msgs = await client.getMessages(); // throws\n// after\nconst client = new RpcClient(opts);\nawait client.start();\nconst msgs = await client.getMessages();","handlingStrategy":"try-catch","validationCode":"if (!client.isRunning()) await client.start();","typeGuard":null,"tryCatchPattern":"try {\n  const res = await client.sendCommand(cmd);\n} catch (err) {\n  if ((err as Error).message === \"Client not started\") {\n    await client.start();\n  } else throw err;\n}","preventionTips":["Always await start() in an init function before any command.","Register commands only after start resolves.","Handle process exit by restarting the client, not reusing it.","Keep a single lifecycle owner so stop() cannot race commands."],"tags":["rpc","lifecycle","not-started","ipc"],"backgroundTag":"client-not-started","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}