can1357/oh-my-pi · error

Daemon ${operation.name} process is unavailable

Error message

Daemon ${operation.name} process is unavailable

What it means

To deliver a signal on non-Windows platforms (or Windows without a PTY), the broker resolves the daemon's recorded pid via Process.fromPid and kills its process tree. If the snapshot has no pid, or the pid could not be resolved to a live process handle, the broker throws this error instead of signaling blindly.

Source

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

			throw new Error(`Daemon ${operation.name} is ${record.snapshot.state}`);
		}
		if (operation.data === undefined && operation.signal === undefined) {
			throw new Error("send requires data or signal");
		}
		if (operation.data !== undefined) {
			if (record.pty) record.pty.write(operation.data);
			else if (record.input) {
				record.input.write(operation.data);
				await record.input.flush();
			} else throw new Error(`Daemon ${operation.name} stdin is unavailable`);
		}
		if (operation.signal) {
			if (process.platform === "win32" && record.pty) {
				if (operation.signal === "SIGINT") record.pty.write("\u0003");
				else record.pty.kill();
			} else {
				const processRef = record.snapshot.pid === undefined ? null : Process.fromPid(record.snapshot.pid);
				if (!processRef) throw new Error(`Daemon ${operation.name} process is unavailable`);
				processRef.killTree(SIGNAL_NUMBER[operation.signal]);
			}
		}
		return { op: "send", daemon: record.snapshot };
	}

	async #stopRecord(record: ManagedDaemon, timeoutMs: number): Promise<void> {
		await this.#refreshDetached(record);
		if (terminalState(record.snapshot.state)) return;
		record.stopRequested = true;
		if (record.restartTimer) {
			clearTimeout(record.restartTimer);
			record.restartTimer = undefined;
			record.snapshot.state = "exited";
			record.snapshot.exitedAt = Date.now();
			this.#persist(record);
			await record.log?.close();
			record.log = undefined;

View on GitHub (pinned to 9690622007)

Solutions

  1. Refresh the daemon state (list/get) and confirm the pid before sending signals.
  2. Use the broker's stop operation instead of a raw signal if you just want the daemon to terminate.
  3. If the daemon is already gone, treat this as already-stopped: clear the record and move on.

Example fix

// before
await broker.send({ name: "worker", signal: "SIGTERM" }); // pid missing -> throws
// after
const snap = await broker.get("worker");
if (snap?.pid !== undefined) {
  await broker.send({ name: "worker", signal: "SIGTERM" });
} else {
  await broker.stop({ name: "worker" });
}
Defensive patterns

Strategy: validation

Validate before calling

const snap = await broker.get(name);
if (!snap || snap.pid === undefined) throw new Error(`${name} has no pid; use stop instead of signals`);

Type guard

function hasPid(s: { pid?: number }): s is { pid: number } {
  return typeof s.pid === "number";
}

Try / catch

try {
  await broker.send({ name, signal: "SIGTERM" });
} catch (err) {
  if (err instanceof Error && err.message.endsWith("process is unavailable")) {
    // treat as already-stopped or fall back to broker.stop
    await broker.stop({ name });
  } else throw err;
}

Prevention

When it happens

Trigger: Sending a signal to a daemon whose snapshot lacks a pid (never observed or lost across broker restart with a detached record) or whose pid is no longer resolvable as a process handle.

Common situations: Daemon already reaped but state not yet refreshed; broker restarted and reattached to a detached daemon whose pid record is missing; pid reuse/exit raced with the signal send.

Related errors


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