{"record":{"id":"829de9f3b9ca7886","repo":"can1357/oh-my-pi","slug":"daemon-spec-name-is-already-existing-snapshot","errorCode":null,"errorMessage":"Daemon ${spec.name} is already ${existing.snapshot.state}","messagePattern":"Daemon (.+?) is already (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/launch/broker.ts","lineNumber":619,"sourceCode":"\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}\n\t\t\t}\n\t\t\tconst stat = await fs.stat(spec.cwd);\n\t\t\tif (!stat.isDirectory()) throw new Error(`Daemon cwd is not a directory: ${spec.cwd}`);\n\t\t\tconst dir = path.join(this.#runtimeDir, \"daemons\", spec.name);\n\t\t\tconst now = Date.now();\n\t\t\trecord = {\n\t\t\t\tspec,\n\t\t\t\tsnapshot: {","sourceCodeStart":601,"sourceCodeEnd":637,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/launch/broker.ts#L601-L637","documentation":"When #start finds an existing record for the daemon name, it refreshes detached state and checks terminalState(existing.snapshot.state). If the daemon is not in a terminal state (still running/stopping/etc.), starting it again would double-spawn, so the broker throws 'Daemon <name> is already <state>'. A sibling check also blocks restarts with unacknowledged pendingCompletions.","triggerScenarios":"Calling the start RPC for a name whose existing record's snapshot.state is non-terminal — e.g. daemon already 'running' or mid-shutdown — typically a start-again request for an already-launched daemon.","commonSituations":"Scripts idempotently calling start without checking current state; user re-running a launch command in another terminal; a daemon that appears hung being 'started again' rather than restarted; stale client view after another client already started the daemon.","solutions":["Check the daemon's current state first; only start when it is absent or in a terminal state (stopped/exited/crashed)","If the daemon is running and needs a fresh instance, explicitly stop it, wait for a terminal state, then start","If it is stuck mid-shutdown, wait or force-kill before restarting","Acknowledge pending completion notifications if the sibling 'unacknowledged completion notifications' error blocks you"],"exampleFix":"// before\nawait broker.start(spec); // throws: Daemon dev is already running\n// after\nconst snap = await broker.status(spec.name);\nif (snap && !isTerminalState(snap.state)) await broker.stop(spec.name);\nawait broker.start(spec);","handlingStrategy":"validation","validationCode":"const TERMINAL_STATES = new Set(['stopped', 'exited', 'crashed', 'failed']);\nasync function canStart(name: string): Promise<boolean> {\n  const snap = await broker.status(name);\n  return !snap || TERMINAL_STATES.has(snap.state);\n}\n// only call broker.start(spec) when await canStart(spec.name)","typeGuard":"function isTerminalState(state: string): boolean {\n  return ['stopped', 'exited', 'crashed', 'failed'].includes(state);\n}","tryCatchPattern":"try {\n  await broker.start(spec);\n} catch (err) {\n  const m = /^Daemon (.+) is already (.+)$/.exec(err instanceof Error ? err.message : '');\n  if (m) {\n    await broker.stop(m[1]); // or reuse the running daemon\n    await broker.start(spec);\n    return;\n  }\n  throw err;\n}","preventionTips":["Always fetch current daemon state and branch on it before issuing start","Treat start as non-idempotent: build ensure-running logic on top of status + conditional start","Acknowledge pending completion notifications promptly so they don't block restarts","If a daemon seems hung in a non-terminal state, stop/force-kill explicitly rather than calling start again"],"tags":["daemon","state-conflict","lifecycle","idempotency"],"backgroundTag":"already-running","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}