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
- Set pty: false if the daemon must be detached
- Set detached: false if the daemon genuinely needs a PTY
- 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
- Model detach and PTY as one mutually exclusive 'execution mode' in config/UI instead of two independent booleans
- Validate merged config objects after combining presets or templates
- Document the constraint where the spec type is defined
- Default pty to false for detached/background launch paths
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
- Windows batch files require application "cmd.exe" with the b
- Daemon ${operation.name} stdin is unavailable
- Daemon broker environment is incomplete
- No model configured
- Azure OpenAI base URL is required. Set AZURE_OPENAI_BASE_URL
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/b8d3b0fd7f078232.
Report an issue: GitHub.