{"record":{"id":"acf2bf417f92dec7","repo":"can1357/oh-my-pi","slug":"cmux-socket-is-not-connected","errorCode":null,"errorMessage":"cmux socket is not connected","messagePattern":"cmux socket is not connected","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/browser/cmux/socket-client.ts","lineNumber":326,"sourceCode":"\t\t\t\t} catch (err) {\n\t\t\t\t\tjob.reject(err instanceof Error ? err : new ToolError(String(err)));\n\t\t\t\t} finally {\n\t\t\t\t\tif (this.#activeJob === job) {\n\t\t\t\t\t\tthis.#activeJob = null;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.#pumping = false;\n\t\t\tif (this.#queue.length > 0 && !this.#disposed) {\n\t\t\t\tthis.#pump();\n\t\t\t}\n\t\t}\n\t}\n\n\t#sendLine(line: string, timeoutMs: number): Promise<string> {\n\t\tif (!this.#socket || this.#socket.destroyed) {\n\t\t\tthrow new ToolError(\"cmux socket is not connected\");\n\t\t}\n\t\tconst read = this.#nextLine(timeoutMs);\n\t\tthis.#socket.write(`${line}\\n`, err => {\n\t\t\tif (err) {\n\t\t\t\tthis.#handleSocketFailure(err);\n\t\t\t}\n\t\t});\n\t\treturn read;\n\t}\n\n\t#nextLine(timeoutMs: number): Promise<string> {\n\t\tconst { promise, resolve, reject } = Promise.withResolvers<string>();\n\t\tlet waiter: LineWaiter;\n\t\tconst timer = setTimeout(() => {\n\t\t\tconst index = this.#lineWaiters.indexOf(waiter);\n\t\t\tif (index >= 0) {\n\t\t\t\tthis.#lineWaiters.splice(index, 1);\n\t\t\t}","sourceCodeStart":308,"sourceCodeEnd":344,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/browser/cmux/socket-client.ts#L308-L344","documentation":"#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).","triggerScenarios":"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.","commonSituations":"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().","solutions":["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","Don't reuse a client after close(); construct a fresh instance per lifecycle","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","Check for prior 'cmux socket error'/'cmux socket closed' logs explaining why the socket was destroyed"],"exampleFix":"// before\nawait client.request(\"tabs.list\", {}); // socket died earlier\n// after\nif (!client) client = new CmuxSocketClient({ socketPath });\ntry {\n  await client.connect();\n  await client.request(\"tabs.list\", {});\n} catch {\n  client.close();\n  client = new CmuxSocketClient({ socketPath });\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":"function isSocketUsable(c: CmuxSocketClient): boolean {\n  // no public accessor exists; rely on connect() instead\n  return true;\n}","tryCatchPattern":"try {\n  result = await client.request(method, params);\n} catch (err) {\n  if (err instanceof ToolError && err.message === \"cmux socket is not connected\") {\n    client.close();\n    client = new CmuxSocketClient({ socketPath });\n    await client.connect();\n    result = await client.request(method, params);\n  } else throw err;\n}","preventionTips":["Never call request() on a client after close(); create a new instance","Reconnect (await client.connect()) after any socket error/timeout before the next request","Avoid sharing one client across tasks that can time out independently; a desync destroy tears the socket down for everyone","Wrap request sequences in a helper that recreates the client on connection-state errors"],"tags":["socket","connection-state","concurrency"],"backgroundTag":"socket-not-connected","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}