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
- Check client.isClosed (or track close state) before issuing requests
- Cancel/await pending background work (#publishCompletionOwners) before calling close()
- 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
- Track client lifecycle: close() only after all background publishers have settled
- Use AbortController/cancellation to stop background work before close()
- Avoid caching client instances across restart cycles
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
- blob daemon ${input} responded ${response.status}
- Daemon broker authentication failed
- Daemon ${spec.name} is already ${existing.snapshot.state}
- Daemon ${operation.name} is ${record.snapshot.state}
- Daemon broker environment is incomplete
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/b0bfd14f16a07a0e.
Report an issue: GitHub.