can1357/oh-my-pi · error

Daemon ${spec.name} is already ${existing.snapshot.state}

Error message

Daemon ${spec.name} is already ${existing.snapshot.state}

What it means

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.

Source

Thrown at packages/coding-agent/src/launch/broker.ts:619

			throw new Error("A detached daemon cannot allocate a PTY");
		}
		if (
			spec.pty &&
			process.platform === "win32" &&
			[".bat", ".cmd"].includes(path.extname(spec.application).toLowerCase())
		) {
			throw new Error('Windows batch files require application "cmd.exe" with the batch path after "/c"');
		}
		if (this.#startingNames.has(spec.name)) {
			throw new Error(`Daemon ${spec.name} is already starting`);
		}
		this.#startingNames.add(spec.name);
		let record: ManagedDaemon;
		try {
			const existing = this.#records.get(spec.name);
			if (existing) await this.#refreshDetached(existing);
			if (existing && !terminalState(existing.snapshot.state)) {
				throw new Error(`Daemon ${spec.name} is already ${existing.snapshot.state}`);
			}
			if (existing && existing.pendingCompletions.length > 0) {
				throw new Error(`Daemon ${spec.name} has unacknowledged completion notifications`);
			}
			if (spec.ready?.log) {
				try {
					new RegExp(spec.ready.log, "u");
				} catch (error) {
					throw new Error(`Invalid readiness regex: ${error instanceof Error ? error.message : String(error)}`);
				}
			}
			const stat = await fs.stat(spec.cwd);
			if (!stat.isDirectory()) throw new Error(`Daemon cwd is not a directory: ${spec.cwd}`);
			const dir = path.join(this.#runtimeDir, "daemons", spec.name);
			const now = Date.now();
			record = {
				spec,
				snapshot: {

View on GitHub (pinned to 9690622007)

Solutions

  1. Check the daemon's current state first; only start when it is absent or in a terminal state (stopped/exited/crashed)
  2. If the daemon is running and needs a fresh instance, explicitly stop it, wait for a terminal state, then start
  3. If it is stuck mid-shutdown, wait or force-kill before restarting
  4. Acknowledge pending completion notifications if the sibling 'unacknowledged completion notifications' error blocks you

Example fix

// before
await broker.start(spec); // throws: Daemon dev is already running
// after
const snap = await broker.status(spec.name);
if (snap && !isTerminalState(snap.state)) await broker.stop(spec.name);
await broker.start(spec);
Defensive patterns

Strategy: validation

Validate before calling

const TERMINAL_STATES = new Set(['stopped', 'exited', 'crashed', 'failed']);
async function canStart(name: string): Promise<boolean> {
  const snap = await broker.status(name);
  return !snap || TERMINAL_STATES.has(snap.state);
}
// only call broker.start(spec) when await canStart(spec.name)

Type guard

function isTerminalState(state: string): boolean {
  return ['stopped', 'exited', 'crashed', 'failed'].includes(state);
}

Try / catch

try {
  await broker.start(spec);
} catch (err) {
  const m = /^Daemon (.+) is already (.+)$/.exec(err instanceof Error ? err.message : '');
  if (m) {
    await broker.stop(m[1]); // or reuse the running daemon
    await broker.start(spec);
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/829de9f3b9ca7886. Report an issue: GitHub.