can1357/oh-my-pi · error

A detached daemon cannot allocate a PTY

Error message

A detached daemon cannot allocate a PTY

What it means

DaemonBroker.#start rejects combinations of spec.detached and spec.pty: a daemon started detached (surviving independently of the broker/session) cannot also be given a pseudo-terminal allocation, since the PTY lifecycle is tied to the broker process. The check runs after name validation and before any process is spawned.

Source

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

			}
			case "restart":
				return this.#restart(operation.name);
			case "describe": {
				const record = this.#record(operation.name);
				await this.#refreshDetached(record);
				return { op: "describe", daemon: record.snapshot, spec: record.spec };
			}
			case "shutdown":
				return { op: "shutdown" };
		}
	}

	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}`);

View on GitHub (pinned to 9690622007)

Solutions

  1. Set pty: false if the daemon must be detached
  2. Set detached: false if the daemon genuinely needs a PTY
  3. Split into two daemons: a detached non-PTY one plus an interactive PTY one, if both behaviors are required

Example fix

// before
await broker.start({ name: 'dev', detached: true, pty: true }); // throws
// after
await broker.start({ name: 'dev', detached: true, pty: false });
Defensive patterns

Strategy: validation

Validate before calling

function assertValidDaemonSpec(spec: DaemonSpec): void {
  if (spec.detached && spec.pty) {
    throw new Error('detached and pty are mutually exclusive');
  }
}
// call before broker.start(spec)

Try / catch

try {
  await broker.start(spec);
} catch (err) {
  if (err instanceof Error && err.message === 'A detached daemon cannot allocate a PTY') {
    return broker.start({ ...spec, pty: false });
  }
  throw err;
}

Prevention

When it happens

Trigger: Submitting a DaemonSpec to the start RPC with both detached: true and pty: true — e.g. config that enables 'run in background' and 'interactive terminal' at once.

Common situations: Merging user-facing toggles ('detach', 'use PTY') that were assumed independent; copying a spec template from an interactive setup into a detached launcher; migrating an interactive dev-server daemon to a detached startup service without clearing pty.

Related errors


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