Summary
ctx.sendError in src/server/routes/ws.ts:73-75 only writes to the server log. The 7 call sites in src/server/routes/ws/commands/message.ts and src/server/routes/ws/commands/fork.ts use it for early-return error paths (missing user, missing chat model, conversation create failure), but the client never receives any frame.
sendError: (error: string, conversationId?: string) => {
log.warn({ error, conversationId }, 'Command error');
},
Impact
When a client sends a message or fork command, it optimistically renders a user bubble and sets isProcessing = true in its reducer. If the server short-circuits via sendError (e.g., the user picked a chatModelId that was just disabled), the client's optimistic state stays forever:
- No spinner clears
- No error toast
- The "send" appears successful but never produces a response
- This was newly exposed by commit
2cb1d88 ("Keep chat model selector in sync with conversation model"), which added three new sendError early-returns on stale chatModelId. Previously the validation didn't exist, so the bug was latent.
The client already has machinery for run-level errors: run_stopped { reason: 'error', error } (src/client/hooks/useWebSocketChat.ts:1416-1428) clears isProcessing, releases the wake lock, and fires onErrorCb.
Proposed fix
Have sendError emit a run_stopped { reason: 'error' } frame when both conversationId and runId are in scope at the call site. No new ServerMessage type; the client handler is reused.
I have a fix ready locally and will open a PR shortly.
Summary
ctx.sendErrorinsrc/server/routes/ws.ts:73-75only writes to the server log. The 7 call sites insrc/server/routes/ws/commands/message.tsandsrc/server/routes/ws/commands/fork.tsuse it for early-return error paths (missing user, missing chat model, conversation create failure), but the client never receives any frame.Impact
When a client sends a
messageorforkcommand, it optimistically renders a user bubble and setsisProcessing = truein its reducer. If the server short-circuits viasendError(e.g., the user picked achatModelIdthat was just disabled), the client's optimistic state stays forever:2cb1d88("Keep chat model selector in sync with conversation model"), which added three newsendErrorearly-returns on stalechatModelId. Previously the validation didn't exist, so the bug was latent.The client already has machinery for run-level errors:
run_stopped { reason: 'error', error }(src/client/hooks/useWebSocketChat.ts:1416-1428) clearsisProcessing, releases the wake lock, and firesonErrorCb.Proposed fix
Have
sendErroremit arun_stopped { reason: 'error' }frame when bothconversationIdandrunIdare in scope at the call site. No new ServerMessage type; the client handler is reused.I have a fix ready locally and will open a PR shortly.