can1357/oh-my-pi · error
Daemon 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.
Solutions
- Start the daemon with a PTY (or stdin capture) enabled so input can be delivered.
- Restart the daemon with the correct input configuration, then re-send.
- 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
- Always launch interactive daemons with pty: true (or stdin capture) in the spec.
- Keep launch configuration and send expectations in one place so dropping pty breaks visibly at startup.
- Document which daemons accept input; treat input-less daemons as fire-and-forget.
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
- A detached daemon cannot allocate a PTY
- Daemon broker environment is incomplete
- Windows batch files require application "cmd.exe" with the…
- a user-supplied replacement endpoint is required
- Agent " " is disabled in settings. Enable it via /agents…
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/1272d9cbbed1ff08.
Report an issue: GitHub.
Appendix: 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)