{"record":{"id":"aac60c7b4913d879","repo":"can1357/oh-my-pi","slug":"daemon-spec-name-is-already-starting","errorCode":null,"errorMessage":"Daemon ${spec.name} is already starting","messagePattern":"Daemon (.+?) is already starting","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/launch/broker.ts","lineNumber":611,"sourceCode":"\t\t}\n\t}\n\n\tasync #start(spec: DaemonSpec, owner?: string): Promise<DaemonRpcResult> {\n\t\tif (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,47}$/.test(spec.name)) {\n\t\t\tthrow new Error(\"Daemon name must be 1-48 letters, numbers, dots, underscores, or hyphens\");\n\t\t}\n\t\tif (spec.detached && spec.pty) {\n\t\t\tthrow new Error(\"A detached daemon cannot allocate a PTY\");\n\t\t}\n\t\tif (\n\t\t\tspec.pty &&\n\t\t\tprocess.platform === \"win32\" &&\n\t\t\t[\".bat\", \".cmd\"].includes(path.extname(spec.application).toLowerCase())\n\t\t) {\n\t\t\tthrow new Error('Windows batch files require application \"cmd.exe\" with the batch path after \"/c\"');\n\t\t}\n\t\tif (this.#startingNames.has(spec.name)) {\n\t\t\tthrow new Error(`Daemon ${spec.name} is already starting`);\n\t\t}\n\t\tthis.#startingNames.add(spec.name);\n\t\tlet record: ManagedDaemon;\n\t\ttry {\n\t\t\tconst existing = this.#records.get(spec.name);\n\t\t\tif (existing) await this.#refreshDetached(existing);\n\t\t\tif (existing && !terminalState(existing.snapshot.state)) {\n\t\t\t\tthrow new Error(`Daemon ${spec.name} is already ${existing.snapshot.state}`);\n\t\t\t}\n\t\t\tif (existing && existing.pendingCompletions.length > 0) {\n\t\t\t\tthrow new Error(`Daemon ${spec.name} has unacknowledged completion notifications`);\n\t\t\t}\n\t\t\tif (spec.ready?.log) {\n\t\t\t\ttry {\n\t\t\t\t\tnew RegExp(spec.ready.log, \"u\");\n\t\t\t\t} catch (error) {\n\t\t\t\t\tthrow new Error(`Invalid readiness regex: ${error instanceof Error ? error.message : String(error)}`);\n\t\t\t\t}","sourceCodeStart":593,"sourceCodeEnd":629,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/launch/broker.ts#L593-L629","documentation":"DaemonBroker.#start tracks in-flight startups in a #startingNames set and throws 'Daemon <name> is already starting' if a start request for the same name arrives while a previous start is still in progress. This prevents duplicate spawns and racy record creation for the same daemon name.","triggerScenarios":"Issuing two overlapping start RPCs with the same spec.name — e.g. double-clicking a start button, retry logic firing while the first start is still initializing, or several clients starting the same named daemon concurrently.","commonSituations":"UI without request de-duplication; network retries with no jitter on slow daemon spawn; orchestration scripts racing multiple workers to start the same daemon; slow #start (waiting on readiness logs) widening the race window.","solutions":["Wait for the in-flight start to finish and reuse its result instead of issuing a second start for the same name","Query the daemon's state first and skip start if it already exists/is starting","Add client-side request coalescing/mutex keyed by daemon name","Retry after receiving this error with backoff, expecting the first start to complete"],"exampleFix":"// before\nPromise.all([broker.start(spec), broker.start(spec)]); // second throws 'already starting'\n// after\nconst starts = new Map<string, Promise<DaemonRpcResult>>();\nconst once = (spec: DaemonSpec) => {\n  let p = starts.get(spec.name);\n  if (!p) { p = broker.start(spec).finally(() => starts.delete(spec.name)); starts.set(spec.name, p); }\n  return p;\n};\nawait Promise.all([once(spec), once(spec)]);","handlingStrategy":"retry","validationCode":"const inflight = new Map<string, Promise<DaemonRpcResult>>();\nfunction startOnce(spec: DaemonSpec): Promise<DaemonRpcResult> {\n  let p = inflight.get(spec.name);\n  if (!p) {\n    p = broker.start(spec).finally(() => inflight.delete(spec.name));\n    inflight.set(spec.name, p);\n  }\n  return p;\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await startOnce(spec);\n} catch (err) {\n  if (err instanceof Error && /is already starting$/.test(err.message)) {\n    await Bun.sleep(500); // or poll daemon state until start settles\n    return startOnce(spec);\n  }\n  throw err;\n}","preventionTips":["Coalesce concurrent start requests per daemon name on the client side","Disable/re-debounce UI triggers (buttons, hotkeys) while a start is pending","Use bounded backoff for retries instead of immediate re-issue","Prefer a status query over a blind start when unsure of daemon state"],"tags":["concurrency","daemon","race-condition","state-conflict"],"backgroundTag":"operation-already-in-progress","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}