can1357/oh-my-pi · error

Daemon broker client is closed

Error message

Daemon broker client is closed

What it means

DaemonBrokerClient.request rejects any RPC issued after the client was closed via close(). #closed is a one-way latch; once closed the socket and subscription state are torn down and no further operations are permitted.

Source

Thrown at packages/coding-agent/src/launch/client.ts:164

	readonly #completionReplays = new Set<string>();
	readonly #inFlightCompletionIds = new Set<string>();
	readonly #completionSubscriptionId = crypto.randomUUID();
	#socket: net.Socket | undefined;
	#connectPromise: Promise<void> | undefined;
	#buffer = "";
	#closed = false;
	#completionReconnectTimer: NodeJS.Timeout | undefined;

	constructor(projectDir: string, runtimeDir: string, token: string, options: DaemonBrokerClientOptions) {
		this.projectDir = projectDir;
		this.#runtimeDir = runtimeDir;
		this.#endpoint = daemonBrokerEndpoint(projectDir, runtimeDir);
		this.#token = token;
		this.#idleGraceMs = options.idleGraceMs;
	}

	async request(operation: DaemonOperation, signal?: AbortSignal): Promise<DaemonRpcResult> {
		if (this.#closed) throw new Error("Daemon broker client is closed");
		if (signal?.aborted) throw new Error("Daemon broker request aborted");
		await this.#connect();
		const socket = this.#socket;
		if (!socket || socket.destroyed) throw new Error("Daemon broker socket is unavailable");

		const completionUnsubscribes = [...this.#completionUnsubscribes];
		const completionReplays = [...this.#completionReplays];
		const id = crypto.randomUUID();
		const { promise, resolve, reject } = Promise.withResolvers<DaemonRpcResult>();
		const timer = setTimeout(() => {
			const pending = this.#pending.get(id);
			if (!pending) return;
			this.#pending.delete(id);
			pending.removeAbort?.();
			reject(new Error(`Daemon ${operation.op} request timed out`));
		}, requestTimeoutMs(operation));
		const pending: PendingRequest = { operation, resolve, reject, timer };
		if (signal) {

View on GitHub (pinned to 9690622007)

Solutions

  1. Check client.isClosed (or track close state) before issuing requests
  2. Cancel/await pending background work (#publishCompletionOwners) before calling close()
  3. Create a new DaemonBrokerClient instance after close instead of reusing the old one

Example fix

// before
await client.close();
await client.request(op); // throws
// after
await client.close();
const client2 = new DaemonBrokerClient(projectDir, runtimeDir, token);
await client2.request(op);
Defensive patterns

Strategy: try-catch

Validate before calling

if (client.isClosed) throw new Error('cannot request: broker client already closed');

Try / catch

try {
  await client.request(op);
} catch (err) {
  if (err.message === 'Daemon broker client is closed') {
    client = new DaemonBrokerClient(projectDir, runtimeDir, token); // recreate instead of failing
    await client.request(op);
  } else throw err;
}

Prevention

When it happens

Trigger: Calling request() after awaiting close(); fire-and-forget async work (like #publishCompletionOwners) issuing a request after the session shut down; reusing a cached client instance across a restart cycle.

Common situations: App shutdown ordering where background completions still publish; hot-reload or restart logic that closes the client but keeps stale references; long-lived callbacks outliving the client.

Related errors


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