Skip to content

Tell a caller why the voice service did not start the call - #654

Merged
davidmckayv merged 2 commits into
CopilotKit:mainfrom
kevin9327:fix/voice-call-error-message
Sep 28, 2026
Merged

davidmckayv merged 2 commits into
CopilotKit:mainfrom
kevin9327:fix/voice-call-error-message

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

What this changes

server/src/voice/provider.ts defines VoiceError for "only fixed, safe messages", and the providers throw it with text written for the caller: "The voice service is busy. Please retry shortly." when the service answers 429, "The voice service returned an invalid response.", "The voice service requires an audio SDP offer." The /api/voice/calls route caught every error the same way and answered "The voice service could not start a call. Please retry.", so none of those messages ever reached anyone. A person who hit the provider's rate limit was told to retry at once, which is the one thing that does not help.

The route now answers with the VoiceError's own message, still as a 502, the way server/src/dictation/routes.ts already passes a TranscriptionError's message on. Any other error, which may carry an upstream body, still gets the generic sentence, and a cancelled or timed-out handshake still gets its 504. The existing "provider errors do not leak secrets" test is unchanged and passes.

Where it runs

OpenBot is deployed as several server processes behind a load balancer, serving a whole company.
Consecutive requests from the same person reach different processes, and the process that answered a
WebSocket upgrade is rarely the one that answers the next call on that conversation.

State that outlives a single request therefore has to be shared, or the change works on one machine
and stops working the moment there are two, without saying so. That failure is worse than not
shipping the feature: it passes review, passes CI, passes a local demo, and only surfaces as a Bot
that forgets, a question nobody can answer, or a boundary that never fires.

Answer these even when the answer is "none":

  • New state that outlives a request? None.
  • What happens on the second replica? The same; this is the body of one response.
  • Anything serialised? None.
  • Anything fanned out to a browser? None.
  • New listener, port, or schedule? None.

Boundary and audit

  • Every acting call still goes through the gateway: resolve, decide, audit, then act. Untouched.
  • New refusals and new failures each write a row. None added; the same failures, with the message their provider already chose.
  • Nothing new is trusted from the client that the server can resolve itself.

Changelog

  • A line in CHANGELOG.md under Unreleased.

Proof

New test in server/tests/voice-routes.test.ts: a provider that throws the busy VoiceError. On unmodified main:

- Expected  - 1
+ Received  + 1
(fail) a caller is told what the voice service said when it gave a safe reason

After the fix: bun test tests/voice-routes.test.ts in server → 10 pass, 0 fail. tsc --noEmit in server exits 0. Biome check clean on the changed files.

🤖 Generated with Claude Code

The voice call route replaced every provider error with one generic
sentence, including the fixed VoiceError messages written for the
caller, such as the service being busy. It now passes those on, as the
dictation route does, and keeps the generic line for anything else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

davidmckayv
davidmckayv previously approved these changes Sep 28, 2026
Move the unchanged changelog entry to its own existing anchor.
@davidmckayv
davidmckayv merged commit d94171c into CopilotKit:main Sep 28, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants