added 60db voice services#7
Conversation
|
@Dev-develope is attempting to deploy a commit to the JS Mastery Pro Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThis PR adds a complete 60db voice provider integration alongside Vapi, enabling environment-driven provider selection. It introduces WebSocket-based STT/TTS, LLM chat with tool-calling, audio I/O, server-side credential serving, agent orchestration, a React hook, and component routing logic. Changes60db Voice Provider Integration
Sequence Diagram(s)sequenceDiagram
participant Browser
participant Backend
participant SixtydbAPI
participant LLM
Browser->>Backend: POST /api/sixtydb/key (Clerk auth)
Backend->>Browser: api_key + api_base
Browser->>SixtydbAPI: WebSocket STT (open)
SixtydbAPI->>Browser: connection_established
Browser->>SixtydbAPI: audio chunks (base64)
Browser->>SixtydbAPI: WebSocket TTS (open)
SixtydbAPI->>Browser: connection_established + ready
SixtydbAPI->>Browser: STT final transcript
Browser->>LLM: POST /v1/chat/completions (with tools)
LLM->>Browser: tool_call (searchBook)
Browser->>Browser: execute searchBook(bookId, query)
Browser->>LLM: tool result + history
LLM->>Browser: final reply + finish_reason
Browser->>SixtydbAPI: TTS speak(reply text)
SixtydbAPI->>Browser: audio chunks (base64)
Browser->>Browser: enqueue audio → play
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install timed out. The project may have too many dependencies for the sandbox. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/api/sixtydb/discovery/route.ts`:
- Around line 47-61: The fetch to `${apiBase}${path}` has no timeout; wrap the
request in an AbortController (create controller, pass signal to fetch) and set
a timer (e.g. from a new DISCOVERY_FETCH_TIMEOUT_MS env var or a constant) that
calls controller.abort() after the timeout, then clear the timer on success;
update the catch to detect abort errors and return an appropriate 504/timeout
message via NextResponse.json (keep existing error format), referencing the
fetch call, apiBase, path, AbortController signal, and NextResponse.json so you
can locate and modify the code.
In `@app/api/sixtydb/key/route.ts`:
- Around line 23-26: The route is returning the long-lived SIXTYDB_API_KEY to
clients via NextResponse.json (the api_key variable) — remove the api_key field
from the response and never expose process.env.SIXTYDB_API_KEY in route.ts;
instead keep only non-secret metadata (e.g., api_base) or nothing. Implement a
server-side proxy endpoint (or use provider-issued short-lived tokens when
available) that forwards client requests to 60db using the server-side secret,
or add a server-only route that mints short-lived tokens and returns those, and
update any client code to call that proxy/mint endpoint instead of reading
api_key from this route.
In `@components/VoiceSelector.tsx`:
- Around line 93-96: In VoiceSelector.tsx update the discovery fetches so non-OK
HTTP responses propagate an error instead of being swallowed by .catch(() =>
({})); for the Promise.all that assigns [defaults, mine] (and the similar
fetches at the other occurrences), check response.ok after each fetch and throw
an Error with context (status or text) when !response.ok so the outer try/catch
can hit the existing error UI; remove the fallback that returns {} in the .catch
so network/401/500 failures are surfaced to the error branch.
In `@hooks/useSixtyDb.ts`:
- Line 72: The unmount cleanup only calls closeSession() but does not cancel
pending startup operations, so add an AbortController (or an "isCancelled" flag)
created in the hook and have the effect cleanup signal it; then modify
startVoiceSession (and any async paths that call fetch('/api/sixtydb/key') and
agent.start()) to check the abort signal before and after each await and bail
without constructing a new SixtyDbAgent, opening audio/socket pipelines, or
starting timers if aborted; finally ensure closeSession still runs but startup
code never proceeds after the abort so no detached voice session can be left
running.
- Line 65: The code is sending durationRef.current (which can be stale) to
endVoiceSession; instead, set startedAtRef.current when the server session is
created (right after startVoiceSession succeeds / when agent.start completes the
server-side session handshake) and compute the final duration inside
closeSession using Math.floor((Date.now() - startedAtRef.current) / 1000) (or
desired seconds rounding) before calling endVoiceSession; update the calls that
currently pass durationRef.current (e.g., in closeSession and the other
endVoiceSession call sites) to use this computed duration and stop relying on
durationRef for the final persisted session length.
In `@lib/sixtydb/agent.ts`:
- Around line 126-143: handleUserFinal can reenter and mutate shared
this.history concurrently causing interleaved messages; serialize turns by
acquiring a simple per-agent lock/queue before emitting onFinalUser, pushing the
user message, calling runTurn, pushing assistant reply and invoking tts; release
the lock in the finally block. Concretely: add a mutex/queue field (e.g.,
this.turnLock or this.turnQueue), have handleUserFinal await acquiring the lock
before mutating history or setting this.busy, ensure all history mutations (push
user, push assistant) and the runTurn/tts call happen while holding the lock,
and always release the lock in the finally so subsequent handleUserFinal calls
are processed in order.
In `@lib/sixtydb/chat-client.ts`:
- Around line 46-55: The request body is sending the tools under the wrong key
("tool") so 60db ignores them; update the JSON payload in the chat client (where
SIXTYDB_LLM_MODEL and req.messages/req.tools are used) to send tools under the
"tools" key instead of "tool" (i.e., replace the property named "tool" with
"tools" so req.tools is passed correctly), keeping the rest of the body fields
(stream, temperature, top_k, max_tokens, chat_template_kwargs) unchanged.
In `@lib/sixtydb/stt-socket.ts`:
- Around line 40-45: The open() promise only rejects on ws.onerror, so if the
socket closes early or the server sends a protocol-level error before the
'connected' handshake the promise can hang; update the ws.onclose handler in the
open() implementation to reject the open promise when the socket closes before
readiness (check that this.ready is false and the promise hasn't been settled)
and update handle(data, resolve) (or add a reject parameter) to detect a
protocol-level error message from 60db (e.g., an "error" frame before a
"connected" frame) and call the promise reject with a descriptive Error,
ensuring resolve is only called on successful 'connected' and ready is set to
true; also ensure events.onClose is still invoked when closing.
In `@lib/sixtydb/tts-socket.ts`:
- Around line 43-44: The ensureContext() promise currently only resolves on
success, so modify the WS close/error handlers (ws.onclose and ws.onerror) and
the failure path in the create_context response handling inside handle() to
reject any pending ensureContext waiters so callers like speak() and start()
don't hang; locate where ensureContext stores its resolve/reject (or the
resolver passed into handle from speak()) and call the stored reject with an
Error (e.g., "socket closed" or the create_context error) from ws.onclose,
ws.onerror, and the create_context-failure branch to ensure pending promises are
rejected and cleaned up.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b3ca3b17-8caf-4843-a6ed-9d3e2a1a0a88
📒 Files selected for processing (12)
README.mdapp/api/sixtydb/discovery/route.tsapp/api/sixtydb/key/route.tscomponents/VapiControls.tsxcomponents/VoiceSelector.tsxhooks/useSixtyDb.tslib/constants.tslib/sixtydb/agent.tslib/sixtydb/audio-pipeline.tslib/sixtydb/chat-client.tslib/sixtydb/stt-socket.tslib/sixtydb/tts-socket.ts
| try { | ||
| const res = await fetch(`${apiBase}${path}`, { | ||
| headers: { Authorization: `Bearer ${apiKey}` }, | ||
| cache: 'no-store', | ||
| }); | ||
| const body = await res.text(); | ||
| return new NextResponse(body, { | ||
| status: res.status, | ||
| headers: { 'Content-Type': res.headers.get('Content-Type') ?? 'application/json' }, | ||
| }); | ||
| } catch (err) { | ||
| return NextResponse.json( | ||
| { error: `60db ${path} request failed: ${(err as Error).message}` }, | ||
| { status: 502 }, | ||
| ); |
There was a problem hiding this comment.
Add an explicit timeout to the upstream discovery fetch.
At Line 48, fetch has no timeout. If 60db stalls, this route can hang until platform-level timeout, tying up request capacity.
Suggested patch
export async function GET(request: Request) {
@@
- try {
- const res = await fetch(`${apiBase}${path}`, {
- headers: { Authorization: `Bearer ${apiKey}` },
- cache: 'no-store',
- });
- const body = await res.text();
- return new NextResponse(body, {
- status: res.status,
- headers: { 'Content-Type': res.headers.get('Content-Type') ?? 'application/json' },
- });
- } catch (err) {
+ try {
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), 8000);
+ try {
+ const res = await fetch(`${apiBase}${path}`, {
+ headers: { Authorization: `Bearer ${apiKey}` },
+ cache: 'no-store',
+ signal: controller.signal,
+ });
+ const body = await res.text();
+ return new NextResponse(body, {
+ status: res.status,
+ headers: { 'Content-Type': res.headers.get('Content-Type') ?? 'application/json' },
+ });
+ } finally {
+ clearTimeout(timeout);
+ }
+ } catch (err) {
+ if ((err as Error).name === 'AbortError') {
+ return NextResponse.json(
+ { error: `60db ${path} request timed out.` },
+ { status: 504 },
+ );
+ }
return NextResponse.json(
{ error: `60db ${path} request failed: ${(err as Error).message}` },
{ status: 502 },
);
}
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/api/sixtydb/discovery/route.ts` around lines 47 - 61, The fetch to
`${apiBase}${path}` has no timeout; wrap the request in an AbortController
(create controller, pass signal to fetch) and set a timer (e.g. from a new
DISCOVERY_FETCH_TIMEOUT_MS env var or a constant) that calls controller.abort()
after the timeout, then clear the timer on success; update the catch to detect
abort errors and return an appropriate 504/timeout message via NextResponse.json
(keep existing error format), referencing the fetch call, apiBase, path,
AbortController signal, and NextResponse.json so you can locate and modify the
code.
| return NextResponse.json({ | ||
| api_key: apiKey, | ||
| api_base: process.env.SIXTYDB_API_BASE || 'https://api.60db.ai', | ||
| }); |
There was a problem hiding this comment.
Do not return the long-lived SIXTYDB_API_KEY to the browser.
At Line 24, this endpoint returns a reusable server secret to clients. Any signed-in user can exfiltrate and use it off-platform, bypassing your app’s controls and potentially incurring uncontrolled spend.
Use a server-side WebSocket proxy (or provider-issued short-lived token when available) so the long-lived key never leaves server trust boundaries.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/api/sixtydb/key/route.ts` around lines 23 - 26, The route is returning
the long-lived SIXTYDB_API_KEY to clients via NextResponse.json (the api_key
variable) — remove the api_key field from the response and never expose
process.env.SIXTYDB_API_KEY in route.ts; instead keep only non-secret metadata
(e.g., api_base) or nothing. Implement a server-side proxy endpoint (or use
provider-issued short-lived tokens when available) that forwards client requests
to 60db using the server-side secret, or add a server-only route that mints
short-lived tokens and returns those, and update any client code to call that
proxy/mint endpoint instead of reading api_key from this route.
| const [defaults, mine] = await Promise.all([ | ||
| fetch('/api/sixtydb/discovery?kind=default-voices').then((r) => r.json()).catch(() => ({})), | ||
| fetch('/api/sixtydb/discovery?kind=my-voices').then((r) => r.json()).catch(() => ({})), | ||
| ]); |
There was a problem hiding this comment.
Don’t collapse discovery failures into “No 60db voices available.”
These requests never check response.ok, and each .catch(() => ({})) turns network/401/500 failures into an empty payload. The error branch is effectively bypassed, so Clerk-gated discovery failures show up as a misleading empty-state message instead of an actionable error. Let the request throw on non-OK responses and keep the existing error UI for that path.
Suggested fix
+async function fetchDiscovery(kind: 'default-voices' | 'my-voices') {
+ const res = await fetch(`/api/sixtydb/discovery?kind=${kind}`);
+ if (!res.ok) {
+ throw new Error(`Failed to load ${kind} (${res.status})`);
+ }
+ return res.json();
+}
+
useEffect(() => {
let cancelled = false;
(async () => {
try {
// Default voices first, then merge the caller's voices on top.
const [defaults, mine] = await Promise.all([
- fetch('/api/sixtydb/discovery?kind=default-voices').then((r) => r.json()).catch(() => ({})),
- fetch('/api/sixtydb/discovery?kind=my-voices').then((r) => r.json()).catch(() => ({})),
+ fetchDiscovery('default-voices'),
+ fetchDiscovery('my-voices'),
]);
if (cancelled) return;
const arr = [...extractVoices(defaults), ...extractVoices(mine)];Also applies to: 101-104, 112-114
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/VoiceSelector.tsx` around lines 93 - 96, In VoiceSelector.tsx
update the discovery fetches so non-OK HTTP responses propagate an error instead
of being swallowed by .catch(() => ({})); for the Promise.all that assigns
[defaults, mine] (and the similar fetches at the other occurrences), check
response.ok after each fetch and throw an Error with context (status or text)
when !response.ok so the outer try/catch can hit the existing error UI; remove
the fallback that returns {} in the .catch so network/401/500 failures are
surfaced to the error branch.
| agentRef.current = null; | ||
| cleanupTimer(); | ||
| if (sessionIdRef.current) { | ||
| endVoiceSession(sessionIdRef.current, durationRef.current).catch((err) => |
There was a problem hiding this comment.
Report final duration from timestamps, not from the UI interval state.
The server session starts at startVoiceSession(...), but startedAtRef.current is set only after agent.start() returns, and endVoiceSession(...) is sent durationRef.current, which is at least one tick stale on manual stop. That means persisted usage misses setup time and under-reports short sessions. Initialize the start timestamp when the server session is created, then compute the final seconds inside closeSession() from Date.now() - startedAtRef.current.
Suggested fix
const closeSession = useCallback(() => {
+ const finalDuration =
+ startedAtRef.current == null
+ ? durationRef.current
+ : Math.floor((Date.now() - startedAtRef.current) / TIMER_INTERVAL_MS);
+
agentRef.current?.stop();
agentRef.current = null;
cleanupTimer();
if (sessionIdRef.current) {
- endVoiceSession(sessionIdRef.current, durationRef.current).catch((err) =>
+ endVoiceSession(sessionIdRef.current, finalDuration).catch((err) =>
console.error('Failed to end voice session:', err),
);
sessionIdRef.current = null;
}
}, [cleanupTimer, durationRef]);
@@
}
sessionIdRef.current = session.sessionId || null;
+ startedAtRef.current = Date.now();
const keyRes = await fetch('/api/sixtydb/key', { method: 'POST' });
@@
- startedAtRef.current = Date.now();
setDuration(0);Also applies to: 87-94, 127-132
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@hooks/useSixtyDb.ts` at line 65, The code is sending durationRef.current
(which can be stale) to endVoiceSession; instead, set startedAtRef.current when
the server session is created (right after startVoiceSession succeeds / when
agent.start completes the server-side session handshake) and compute the final
duration inside closeSession using Math.floor((Date.now() -
startedAtRef.current) / 1000) (or desired seconds rounding) before calling
endVoiceSession; update the calls that currently pass durationRef.current (e.g.,
in closeSession and the other endVoiceSession call sites) to use this computed
duration and stop relying on durationRef for the final persisted session length.
| } | ||
| }, [cleanupTimer, durationRef]); | ||
|
|
||
| useEffect(() => () => closeSession(), [closeSession]); |
There was a problem hiding this comment.
Cancel in-flight startup before it can recreate the agent after unmount.
closeSession() only tears down the session state that already exists. If this hook unmounts while startVoiceSession(...), the /api/sixtydb/key fetch, or agent.start() is still pending, the continuation below can still assign a fresh SixtyDbAgent, open the audio/socket pipeline, and arm a timer after cleanup has already run. That leaves a detached voice session alive with no owner to stop it.
Also applies to: 87-98, 102-140
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@hooks/useSixtyDb.ts` at line 72, The unmount cleanup only calls
closeSession() but does not cancel pending startup operations, so add an
AbortController (or an "isCancelled" flag) created in the hook and have the
effect cleanup signal it; then modify startVoiceSession (and any async paths
that call fetch('/api/sixtydb/key') and agent.start()) to check the abort signal
before and after each await and bail without constructing a new SixtyDbAgent,
opening audio/socket pipelines, or starting timers if aborted; finally ensure
closeSession still runs but startup code never proceeds after the abort so no
detached voice session can be left running.
| this.events.onStatus?.('starting'); | ||
| await this.tts.speak(this.opts.firstMessage); | ||
| this.history.push({ role: 'assistant', content: this.opts.firstMessage }); | ||
| this.events.onFinalAssistant?.(this.opts.firstMessage); | ||
|
|
||
| this.mic = new MicCapture((b64) => this.stt?.sendAudio(b64)); | ||
| await this.mic.start(); |
There was a problem hiding this comment.
Pause mic/STT while assistant audio is playing.
TtsSocket.speak() returns once text is flushed to the websocket, not once playback is done. That means start() enables the mic while the greeting is still speaking, and later turns also leave capture running during assistant playback. The assistant can end up transcribing itself and generating phantom user turns.
Also applies to: 138-138
| private async handleUserFinal(text: string): Promise<void> { | ||
| if (this.stopped || !text.trim()) return; | ||
| this.events.onFinalUser?.(text); | ||
| this.history.push({ role: 'user', content: text }); | ||
| this.busy = true; | ||
| this.events.onStatus?.('thinking'); | ||
|
|
||
| try { | ||
| const reply = await this.runTurn(text); | ||
| if (this.stopped) return; | ||
| this.history.push({ role: 'assistant', content: reply }); | ||
| this.events.onFinalAssistant?.(reply); | ||
| await this.tts?.speak(reply); | ||
| } catch (e) { | ||
| this.events.onError?.(e as Error); | ||
| } finally { | ||
| this.busy = false; | ||
| } |
There was a problem hiding this comment.
Serialize final-transcript handling before mutating history.
busy only drives status right now. A second onFinal can enter while the first LLM/tool/TTS cycle is still running, and both calls append to the shared history independently. That can reorder user/assistant messages and produce interleaved replies. At minimum, guard or queue while a turn is in flight.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/sixtydb/agent.ts` around lines 126 - 143, handleUserFinal can reenter and
mutate shared this.history concurrently causing interleaved messages; serialize
turns by acquiring a simple per-agent lock/queue before emitting onFinalUser,
pushing the user message, calling runTurn, pushing assistant reply and invoking
tts; release the lock in the finally block. Concretely: add a mutex/queue field
(e.g., this.turnLock or this.turnQueue), have handleUserFinal await acquiring
the lock before mutating history or setting this.busy, ensure all history
mutations (push user, push assistant) and the runTurn/tts call happen while
holding the lock, and always release the lock in the finally so subsequent
handleUserFinal calls are processed in order.
| body: JSON.stringify({ | ||
| model: SIXTYDB_LLM_MODEL, | ||
| messages: req.messages, | ||
| tool: req.tools, | ||
| stream: false, | ||
| temperature: req.temperature ?? 0.7, | ||
| top_k: req.topK ?? 20, | ||
| max_tokens: req.maxTokens, | ||
| chat_template_kwargs: { enable_thinking: false }, | ||
| }), |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
According to 60db's /v1/chat/completions API, is the request property for function/tool definitions named toolsortool?
💡 Result:
According to 60db's documentation, their /v1/chat/completions API is explicitly described as "OpenAI-compatible" [1][2]. Because it follows the OpenAI-compatible chat completion specification, the request property used for function or tool definitions is named tools [1].
Citations:
Change tool to tools to match 60db's OpenAI-compatible API spec.
The 60db /v1/chat/completions endpoint is OpenAI-compatible and expects the request field to be named tools (not tool). The current code sends tool: req.tools, which means the server ignores the tool definitions and the searchBook function cannot be invoked.
Suggested fix
body: JSON.stringify({
model: SIXTYDB_LLM_MODEL,
messages: req.messages,
- tool: req.tools,
+ tools: req.tools,
stream: false,
temperature: req.temperature ?? 0.7,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| body: JSON.stringify({ | |
| model: SIXTYDB_LLM_MODEL, | |
| messages: req.messages, | |
| tool: req.tools, | |
| stream: false, | |
| temperature: req.temperature ?? 0.7, | |
| top_k: req.topK ?? 20, | |
| max_tokens: req.maxTokens, | |
| chat_template_kwargs: { enable_thinking: false }, | |
| }), | |
| body: JSON.stringify({ | |
| model: SIXTYDB_LLM_MODEL, | |
| messages: req.messages, | |
| tools: req.tools, | |
| stream: false, | |
| temperature: req.temperature ?? 0.7, | |
| top_k: req.topK ?? 20, | |
| max_tokens: req.maxTokens, | |
| chat_template_kwargs: { enable_thinking: false }, | |
| }), |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/sixtydb/chat-client.ts` around lines 46 - 55, The request body is sending
the tools under the wrong key ("tool") so 60db ignores them; update the JSON
payload in the chat client (where SIXTYDB_LLM_MODEL and req.messages/req.tools
are used) to send tools under the "tools" key instead of "tool" (i.e., replace
the property named "tool" with "tools" so req.tools is passed correctly),
keeping the rest of the body fields (stream, temperature, top_k, max_tokens,
chat_template_kwargs) unchanged.
| ws.onerror = () => reject(new Error('60db STT WebSocket error')); | ||
| ws.onclose = () => { | ||
| this.ready = false; | ||
| this.events.onClose?.(); | ||
| }; | ||
| ws.onmessage = (e) => this.handle(e.data, resolve); |
There was a problem hiding this comment.
Settle open() when the handshake fails before readiness.
Right now open() only rejects on the browser onerror event. If the socket closes early or 60db replies with its protocol-level error before connected, the promise never resolves or rejects, so agent startup can hang indefinitely in the connect phase.
Suggested fix
return new Promise((resolve, reject) => {
+ let settled = false;
+ const fail = (err: Error) => {
+ if (settled) return;
+ settled = true;
+ reject(err);
+ };
+ const succeed = () => {
+ if (settled) return;
+ settled = true;
+ resolve();
+ };
const ws = new WebSocket(url);
this.ws = ws;
- ws.onerror = () => reject(new Error('60db STT WebSocket error'));
+ ws.onerror = () => fail(new Error('60db STT WebSocket error'));
ws.onclose = () => {
this.ready = false;
this.events.onClose?.();
+ if (!settled) fail(new Error('60db STT WebSocket closed before ready'));
};
- ws.onmessage = (e) => this.handle(e.data, resolve);
+ ws.onmessage = (e) => this.handle(e.data, succeed, fail);
});Also applies to: 118-120
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/sixtydb/stt-socket.ts` around lines 40 - 45, The open() promise only
rejects on ws.onerror, so if the socket closes early or the server sends a
protocol-level error before the 'connected' handshake the promise can hang;
update the ws.onclose handler in the open() implementation to reject the open
promise when the socket closes before readiness (check that this.ready is false
and the promise hasn't been settled) and update handle(data, resolve) (or add a
reject parameter) to detect a protocol-level error message from 60db (e.g., an
"error" frame before a "connected" frame) and call the promise reject with a
descriptive Error, ensuring resolve is only called on successful 'connected' and
ready is set to true; also ensure events.onClose is still invoked when closing.
| ws.onclose = () => this.events.onClose?.(); | ||
| ws.onmessage = (e) => this.handle(e.data, resolve); |
There was a problem hiding this comment.
Reject pending ensureContext() waiters on close/error.
ensureContext() only has a success path. If create_context fails or the socket closes before context_created, speak() never resumes, which can block the first greeting in start() and any later assistant reply.
Suggested fix
- private contextReadyResolvers: Array<() => void> = [];
+ private contextReadyWaiters: Array<{
+ resolve: () => void;
+ reject: (err: Error) => void;
+ }> = [];
private ensureContext(): Promise<void> {
if (this.contextId) return Promise.resolve();
- return new Promise((resolve) => {
- this.contextReadyResolvers.push(resolve);
+ return new Promise((resolve, reject) => {
+ this.contextReadyWaiters.push({ resolve, reject });
this.ws?.send(
JSON.stringify({
type: 'create_context',
@@
case 'context_created':
this.contextId = String(msg.context_id || '');
- this.contextReadyResolvers.splice(0).forEach((fn) => fn());
+ this.contextReadyWaiters.splice(0).forEach(({ resolve }) => resolve());
return;
@@
case 'error':
- this.events.onError?.(new Error(String(msg.message ?? 'TTS error')));
+ const err = new Error(String(msg.message ?? 'TTS error'));
+ this.contextReadyWaiters.splice(0).forEach(({ reject }) => reject(err));
+ this.events.onError?.(err);
return;Also applies to: 79-91, 123-125
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/sixtydb/tts-socket.ts` around lines 43 - 44, The ensureContext() promise
currently only resolves on success, so modify the WS close/error handlers
(ws.onclose and ws.onerror) and the failure path in the create_context response
handling inside handle() to reject any pending ensureContext waiters so callers
like speak() and start() don't hang; locate where ensureContext stores its
resolve/reject (or the resolver passed into handle from speak()) and call the
stored reject with an Error (e.g., "socket closed" or the create_context error)
from ws.onclose, ws.onerror, and the create_context-failure branch to ensure
pending promises are rejected and cleaned up.
Summary by CodeRabbit
New Features
Documentation