can1357/oh-my-pi · error

Daemon ${spec.name} has unacknowledged completion notificati

Error message

Daemon ${spec.name} has unacknowledged completion notifications

What it means

DaemonBroker.start throws this when a daemon with the same name was previously launched and exited, but its completion notifications have not been acknowledged yet. The broker keeps pending completions per record so callers can observe daemon termination; starting a new daemon under the same name before acknowledging would silently discard that record. The caller must acknowledge completions (or remove the daemon record) before reusing the name.

Source

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

			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}`);
			}
			if (existing && existing.pendingCompletions.length > 0) {
				throw new Error(`Daemon ${spec.name} has unacknowledged completion notifications`);
			}
			if (spec.ready?.log) {
				try {
					new RegExp(spec.ready.log, "u");
				} catch (error) {
					throw new Error(`Invalid readiness regex: ${error instanceof Error ? error.message : String(error)}`);
				}
			}
			const stat = await fs.stat(spec.cwd);
			if (!stat.isDirectory()) throw new Error(`Daemon cwd is not a directory: ${spec.cwd}`);
			const dir = path.join(this.#runtimeDir, "daemons", spec.name);
			const now = Date.now();
			record = {
				spec,
				snapshot: {
					name: spec.name,
					id: crypto.randomUUID(),
					state: "starting",

View on GitHub (pinned to 9690622007)

Solutions

  1. Acknowledge the pending completion notifications for that daemon name (via the broker's completion/notification acknowledge API or list operation) before calling start again.
  2. Remove or stop the stale daemon record so pendingCompletions are cleared, then start the daemon.
  3. Use a different daemon name for the new launch if the old notifications must be preserved.

Example fix

// before
await broker.start({ name: "build", ... });
// after
await broker.acknowledgeCompletions("build"); // drain pending exit notifications
await broker.start({ name: "build", ... });
Defensive patterns

Strategy: validation

Validate before calling

const rec = await broker.get?.(spec.name);
if (rec && (rec.pendingCompletions?.length ?? 0) > 0) {
  await broker.acknowledgeCompletions(spec.name); // or use another name
}
await broker.start(spec);

Try / catch

try {
  await broker.start(spec);
} catch (err) {
  if (err instanceof Error && err.message.includes("unacknowledged completion notifications")) {
    await broker.acknowledgeCompletions(spec.name);
    await broker.start(spec);
  } else throw err;
}

Prevention

When it happens

Trigger: Calling broker.start(spec) for a daemon name whose existing record has pendingCompletions.length > 0 — i.e. the previous daemon under that name terminated and its exit notification was never fetched/acknowledged.

Common situations: A daemon crashed or exited and the client never polled for or acknowledged the completion event, then the operator re-runs the launch command with the same daemon name. Also happens after restarting a supervisor client that lost track of prior notifications.

Related errors


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