Skip to content

added 60db voice services#7

Open
Dev-develope wants to merge 1 commit into
adrianhajdin:mainfrom
Dev-develope:main
Open

added 60db voice services#7
Dev-develope wants to merge 1 commit into
adrianhajdin:mainfrom
Dev-develope:main

Conversation

@Dev-develope

@Dev-develope Dev-develope commented Jun 11, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features

    • Added 60db voice provider as an alternative to Vapi, enabling parallel provider support
    • Enhanced voice selector with dynamic voice catalog loading based on configured provider
  • Documentation

    • Updated setup instructions with comprehensive 60db provider configuration and required environment variables

@vercel

vercel Bot commented Jun 11, 2026

Copy link
Copy Markdown

@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.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This 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.

Changes

60db Voice Provider Integration

Layer / File(s) Summary
Configuration & Provider Selection
lib/constants.ts, README.md
Provider enum, 60db model/voice/RAG configuration, system prompt template, and OpenAI-compatible searchBook tool definition. README documents setup, activation diff, and RAG flow.
Backend API Routes & Authentication
app/api/sixtydb/discovery/route.ts, app/api/sixtydb/key/route.ts
Clerk-gated discovery proxy and credential-serving routes that keep SIXTYDB_API_KEY server-side while returning long-lived key to client for WebSocket usage.
WebSocket Protocol Clients (STT & TTS)
lib/sixtydb/stt-socket.ts, lib/sixtydb/tts-socket.ts
SttSocket handles speech-to-text connection, message routing, audio transmission, and transcript dispatch. TtsSocket manages text-to-speech context creation, message parsing, audio chunk streaming, and playback frame tracking.
Audio I/O Pipeline
lib/sixtydb/audio-pipeline.ts
MicCapture wraps getUserMedia with downsampling and PCM encoding; AudioPlayer decodes and schedules Web Audio playback; helpers convert between base64 and 16-bit PCM for WebSocket transmission.
LLM Chat Client & Content Detection
lib/sixtydb/chat-client.ts
Chat completions client for OpenAI-compatible /v1/chat/completions endpoint with tool-calling support. Heuristics for content questions and evasive refusals trigger RAG fallback.
Agent Orchestration & Conversation Logic
lib/sixtydb/agent.ts
SixtyDbAgent coordinates STT/TTS sockets, audio pipeline, and LLM with full conversation turn logic. Handles user final transcripts, runs LLM with tool-calling (up to MAX_TOOL_ROUNDS), executes searchBook tool, and implements RAG fallback for empty or evasive replies.
React Hook & Session Management
hooks/useSixtyDb.ts
useSixtyDb hook provides drop-in replacement for useVapi, manages agent lifecycle, enforces max-duration session limits with interval timer, orchestrates voice session start/end, fetches credentials from /api/sixtydb/key, and returns status/message/control API.
Component Integration & Provider Routing
components/VapiControls.tsx, components/VoiceSelector.tsx
VapiControls selects voice-agent hook at module init based on VOICE_PROVIDER. VoiceSelector refactored with shared VoiceCard UI, ElevenLabsSelector, and new SixtyDbSelector that fetches dynamic voices from /api/sixtydb/discovery and deduplicates by voice_id.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • adrianhajdin/jsm_bookified#1: Extends the voice-selection and configuration code by adding provider switching and 60db discovery/tool-calling integration.
  • adrianhajdin/jsm_bookified#5: The new hooks/useSixtyDb.ts integrates with existing billing/limit enforcement from lib/actions/session.actions.ts.

Poem

🐰 A rabbit hops through 60db's door,
WebSockets singing what voices explored,
With tools that search and audio that flows,
Two providers now, wherever speech goes! 🎙️✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding support for 60db as an alternative voice provider alongside the existing Vapi integration. It is concise and specific.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6a6a21d and eaf7e61.

📒 Files selected for processing (12)
  • README.md
  • app/api/sixtydb/discovery/route.ts
  • app/api/sixtydb/key/route.ts
  • components/VapiControls.tsx
  • components/VoiceSelector.tsx
  • hooks/useSixtyDb.ts
  • lib/constants.ts
  • lib/sixtydb/agent.ts
  • lib/sixtydb/audio-pipeline.ts
  • lib/sixtydb/chat-client.ts
  • lib/sixtydb/stt-socket.ts
  • lib/sixtydb/tts-socket.ts

Comment on lines +47 to +61
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 },
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

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.

Comment on lines +23 to +26
return NextResponse.json({
api_key: apiKey,
api_base: process.env.SIXTYDB_API_BASE || 'https://api.60db.ai',
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

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.

Comment on lines +93 to +96
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(() => ({})),
]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment thread hooks/useSixtyDb.ts
agentRef.current = null;
cleanupTimer();
if (sessionIdRef.current) {
endVoiceSession(sessionIdRef.current, durationRef.current).catch((err) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment thread hooks/useSixtyDb.ts
}
}, [cleanupTimer, durationRef]);

useEffect(() => () => closeSession(), [closeSession]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

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.

Comment thread lib/sixtydb/agent.ts
Comment on lines +108 to +114
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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

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

Comment thread lib/sixtydb/agent.ts
Comment on lines +126 to +143
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +46 to +55
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 },
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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.

Suggested change
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.

Comment thread lib/sixtydb/stt-socket.ts
Comment on lines +40 to +45
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment thread lib/sixtydb/tts-socket.ts
Comment on lines +43 to +44
ws.onclose = () => this.events.onClose?.();
ws.onmessage = (e) => this.handle(e.data, resolve);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

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.

1 participant