can1357/oh-my-pi · error · ToolError

cmux socket is not connected

Error message

cmux socket is not connected

What it means

#sendLine refuses to write to a cmux socket that is null or destroyed, throwing this ToolError synchronously. The client treats a destroyed socket as unrecoverable per-call state: the connection must be re-established (via connect/request) before lines can be sent. It surfaces when a request races a socket teardown (error, close, close(), or a timeout-triggered desync destroy).

Source

Thrown at packages/coding-agent/src/tools/browser/cmux/socket-client.ts:326

				} catch (err) {
					job.reject(err instanceof Error ? err : new ToolError(String(err)));
				} finally {
					if (this.#activeJob === job) {
						this.#activeJob = null;
					}
				}
			}
		} finally {
			this.#pumping = false;
			if (this.#queue.length > 0 && !this.#disposed) {
				this.#pump();
			}
		}
	}

	#sendLine(line: string, timeoutMs: number): Promise<string> {
		if (!this.#socket || this.#socket.destroyed) {
			throw new ToolError("cmux socket is not connected");
		}
		const read = this.#nextLine(timeoutMs);
		this.#socket.write(`${line}\n`, err => {
			if (err) {
				this.#handleSocketFailure(err);
			}
		});
		return read;
	}

	#nextLine(timeoutMs: number): Promise<string> {
		const { promise, resolve, reject } = Promise.withResolvers<string>();
		let waiter: LineWaiter;
		const timer = setTimeout(() => {
			const index = this.#lineWaiters.indexOf(waiter);
			if (index >= 0) {
				this.#lineWaiters.splice(index, 1);
			}

View on GitHub (pinned to 9690622007)

Solutions

  1. Create a new CmuxSocketClient (or await connect() again) — the class resets #socket to null on failure and never auto-reconnects an existing instance from sendLine
  2. Don't reuse a client after close(); construct a fresh instance per lifecycle
  3. Reduce cross-task sharing so one request's timeout/desync destroy doesn't poison concurrent requests; serialize requests or give each task its own client
  4. Check for prior 'cmux socket error'/'cmux socket closed' logs explaining why the socket was destroyed

Example fix

// before
await client.request("tabs.list", {}); // socket died earlier
// after
if (!client) client = new CmuxSocketClient({ socketPath });
try {
  await client.connect();
  await client.request("tabs.list", {});
} catch {
  client.close();
  client = new CmuxSocketClient({ socketPath });
}
Defensive patterns

Strategy: retry

Type guard

function isSocketUsable(c: CmuxSocketClient): boolean {
  // no public accessor exists; rely on connect() instead
  return true;
}

Try / catch

try {
  result = await client.request(method, params);
} catch (err) {
  if (err instanceof ToolError && err.message === "cmux socket is not connected") {
    client.close();
    client = new CmuxSocketClient({ socketPath });
    await client.connect();
    result = await client.request(method, params);
  } else throw err;
}

Prevention

When it happens

Trigger: Calling request()/sendLine after close() disposed the client; the underlying net.Socket errored or closed while a job was being pumped; a prior request timed out and #destroySocketForDesync() nulled the socket; a concurrent request triggered #handleSocketFailure.

Common situations: Long-lived client kept after a network blip; requesting before awaiting connect() on a failed connection; sharing one CmuxSocketClient across async tasks where one task's timeout tears the socket down for others; using the client after close().

Related errors


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