{"record":{"id":"913406329ba9c400","repo":"tinyhumansai/openhuman","slug":"socket-not-connected-no-client-id-for-event-rout","errorCode":null,"errorMessage":"Socket not connected — no client ID for event routing","messagePattern":"Socket not connected — no client ID for event routing","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/src/services/chatService.ts","lineNumber":1268,"sourceCode":"  queueMode?: QueueMode | null;\n}\n\n/**\n * Send a chat message via core RPC.\n *\n * The Rust core spawns the agent loop asynchronously and streams events\n * (tool_call, tool_result, chat_done, chat_error) back over the socket\n * connection using the `client_id` (socket ID) for routing.\n *\n * Returns the turn's `request_id` (from the RPC ack) when the core provides\n * one — used by `parallel` sends to register the forked turn's stream lane.\n * `undefined` if the ack carried no id.\n */\nexport async function chatSend(params: ChatSendParams): Promise<string | undefined> {\n  const socket = socketService.getSocket();\n  const clientId = socket?.id;\n  if (!clientId) {\n    throw new Error('Socket not connected — no client ID for event routing');\n  }\n\n  const result = await callCoreRpc({\n    method: 'openhuman.channel_web_chat',\n    params: {\n      client_id: clientId,\n      thread_id: params.threadId,\n      message: params.message,\n      model_override: params.model ?? undefined,\n      profile_id: params.profileId ?? undefined,\n      locale: params.locale ?? undefined,\n      speak_reply: params.speakReply ?? undefined,\n      source: params.source ?? undefined,\n      session_id: params.sessionId ?? undefined,\n      queue_mode: params.queueMode ?? undefined,\n    },\n  });\n","sourceCodeStart":1250,"sourceCodeEnd":1286,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/app/src/services/chatService.ts#L1250-L1286","documentation":"Thrown by chatSend() before any RPC is issued: socketService.getSocket() returned no socket or a socket without an `id`. The chat pipeline is socket-routed — the core spawns the agent loop and streams tool_call/tool_result/chat_done/chat_error events keyed by client_id (the socket session id). Without a connected socket the turn's events would be undeliverable, so the send is refused up front rather than starting an orphaned turn.","triggerScenarios":"User submits a message before SocketProvider finishes connecting; socket dropped (core restarted, network blip, laptop sleep/resume) and reconnection hasn't completed; running outside the socket-enabled host; a race at app boot where the chat UI mounts before the socket handshake.","commonSituations":"Sending immediately after app open or after core restart; transient disconnects on flaky networks; tests that mount chat components without a SocketProvider mock; socket connect blocked in restrictive network environments.","solutions":["Wait for the socket to be connected before enabling send: gate the composer on the socket connection state from SocketProvider.","Retry after reconnect: listen for the socket 'connect' event and re-attempt the send (message text is still in state).","If it persists, check core socketio service health (core logs, /health) — the backend of the socket may be down, keeping the client disconnected.","In tests, provide a mock socketService whose getSocket() returns {id: 'test'} before invoking chatSend."],"exampleFix":"// before\nconst socket = socketService.getSocket();\nconst clientId = socket?.id;\nif (!clientId) {\n  throw new Error('Socket not connected — no client ID for event routing');\n}\n\n// after — await reconnection with a bounded wait instead of failing instantly\nasync function awaitClientId(timeoutMs = 5000): Promise<string> {\n  const deadline = Date.now() + timeoutMs;\n  for (;;) {\n    const id = socketService.getSocket()?.id;\n    if (id) return id;\n    if (Date.now() > deadline) throw new Error('Socket not connected — no client ID for event routing');\n    await new Promise(r => setTimeout(r, 100));\n  }\n}","handlingStrategy":"validation","validationCode":"const clientId = socketService.getSocket()?.id;\nif (!clientId) {\n  // disable the composer / queue the message until socket 'connect' fires\n  setSendingDisabled(true);\n  socketService.once('connect', () => setSendingDisabled(false));\n}","typeGuard":"function hasSocketClientId(socket: unknown): socket is { id: string } {\n  return Boolean(socket) && typeof (socket as { id?: unknown } | null)?.id === 'string';\n}","tryCatchPattern":"try {\n  await chatSend(params);\n} catch (e) {\n  if (e instanceof Error && e.message === 'Socket not connected — no client ID for event routing') {\n    await socketService.waitForConnect(); // resubscribe on 'connect'\n    await chatSend(params); // retry once connected\n  } else throw e;\n}","preventionTips":["Bind the composer's disabled state to the socket connection state from SocketProvider.","Preserve the drafted message so a reconnect-and-retry is one click.","Mock getSocket() → {id:'test'} in unit tests that exercise chatSend."],"tags":["socket","chat","connection-state","event-routing","race-condition"],"backgroundTag":"socket-not-connected","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}