can1357/oh-my-pi · error

Daemon ${spec.name} is already starting

Error message

Daemon ${spec.name} is already starting

What it means

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.

Source

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

		}
	}

	async #start(spec: DaemonSpec, owner?: string): Promise<DaemonRpcResult> {
		if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,47}$/.test(spec.name)) {
			throw new Error("Daemon name must be 1-48 letters, numbers, dots, underscores, or hyphens");
		}
		if (spec.detached && spec.pty) {
			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)}`);
				}

View on GitHub (pinned to 9690622007)

Solutions

  1. Wait for the in-flight start to finish and reuse its result instead of issuing a second start for the same name
  2. Query the daemon's state first and skip start if it already exists/is starting
  3. Add client-side request coalescing/mutex keyed by daemon name
  4. Retry after receiving this error with backoff, expecting the first start to complete

Example fix

// before
Promise.all([broker.start(spec), broker.start(spec)]); // second throws 'already starting'
// after
const starts = new Map<string, Promise<DaemonRpcResult>>();
const once = (spec: DaemonSpec) => {
  let p = starts.get(spec.name);
  if (!p) { p = broker.start(spec).finally(() => starts.delete(spec.name)); starts.set(spec.name, p); }
  return p;
};
await Promise.all([once(spec), once(spec)]);
Defensive patterns

Strategy: retry

Validate before calling

const inflight = new Map<string, Promise<DaemonRpcResult>>();
function startOnce(spec: DaemonSpec): Promise<DaemonRpcResult> {
  let p = inflight.get(spec.name);
  if (!p) {
    p = broker.start(spec).finally(() => inflight.delete(spec.name));
    inflight.set(spec.name, p);
  }
  return p;
}

Try / catch

try {
  return await startOnce(spec);
} catch (err) {
  if (err instanceof Error && /is already starting$/.test(err.message)) {
    await Bun.sleep(500); // or poll daemon state until start settles
    return startOnce(spec);
  }
  throw err;
}

Prevention

When it happens

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

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

Related errors


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