can1357/oh-my-pi · error

Daemon ${operation.name} stdin is unavailable

Error message

Daemon ${operation.name} stdin is unavailable

What it means

When a send operation includes data, the broker writes it to the daemon's PTY if one exists, otherwise to its captured stdin stream. If the daemon was started without a PTY and without stdin capture (record.pty and record.input are both unset), there is no writable input channel and the broker throws this error rather than silently dropping the data.

Source

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

		const timedOut = operation.for === "ready" && !pattern ? !readyObserved() : !woke;
		return { op: "wait", daemon: record.snapshot, matched, timedOut };
	}

	async #send(operation: Extract<DaemonOperation, { op: "send" }>): Promise<DaemonRpcResult> {
		const record = this.#record(operation.name);
		await this.#refreshDetached(record);
		if (terminalState(record.snapshot.state) || record.snapshot.state === "stopping") {
			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;

View on GitHub (pinned to 9690622007)

Solutions

  1. Start the daemon with a PTY (or stdin capture) enabled so input can be delivered.
  2. Restart the daemon with the correct input configuration, then re-send.
  3. If the daemon doesn't need input, remove the send-data call instead of relying on stdin.

Example fix

// before
await broker.start({ name: "repl", cwd, command: "node", args: ["repl.js"] }); // no pty/stdin
await broker.send({ name: "repl", data: "1+1\n" }); // throws
// after
await broker.start({ name: "repl", cwd, command: "node", args: ["repl.js"], pty: true });
await broker.send({ name: "repl", data: "1+1\n" });
Defensive patterns

Strategy: validation

Validate before calling

// At launch time, ensure an input channel exists:
const spec = { name, cwd, command, args, pty: true }; // or configure stdin capture
// Before sending:
const snap = await broker.get(name);
if (snap && !snap.hasPty && !snap.hasStdin) throw new Error(`${name} has no input channel`);

Try / catch

try {
  await broker.send({ name, data });
} catch (err) {
  if (err instanceof Error && err.message.endsWith("stdin is unavailable")) {
    // restart daemon with pty/stdin enabled, then resend
  } else throw err;
}

Prevention

When it happens

Trigger: Sending data to a daemon launched without pty: true and with stdin not piped/captured — the record has no input handle, so writes are impossible.

Common situations: Starting a detached daemon that doesn't allocate a PTY or stdin pipe, then later trying to feed it interactive input; changing launch flags (dropping pty) while clients still assume interactive stdin.

Related errors


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