* fix: reject misrouted smallwebrtc runs on the telephony websocket
A smallwebrtc (browser/WebRTC) workflow run is established through the WebRTC
signaling endpoint, not the PSTN telephony websocket. When such a run reached
_handle_telephony_websocket it read no "provider" from initial_context and
closed with an opaque "Provider type not found". Detect smallwebrtc runs and
close with a clear reason pointing to the signaling endpoint, without setting
the run to running or invoking a telephony provider. Also store the provider on
smallwebrtc runs at creation so they are self-describing, and make the generic
no-provider close reason include the run id and mode.
Closes#433
* fix: merge workflow run initial context defaults
---------
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Abhishek Kumar <abhishek@a6k.me>
* feat(twilio): add Answering Machine Detection (AMD) support via telephony config
Closes#339
* chore: regenerate OpenAPI spec to fix drift-check
The openapi.json snapshot had drifted from the FastAPI app definition
because main gained new organization endpoints (billing, credits,
context) after this branch was created. Regenerate it with
'python -m scripts.dump_docs_openapi' to bring it back in sync.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add provider-level AMD hooks
* fix: handle db error while persisting amd result
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Sabiha Khan <sabihak89@gmail.com>
Co-authored-by: Sabiha Khan <87858386+chewwbaka@users.noreply.github.com>
* feat(scripts): generate REDIS_PASSWORD on setup, plumb through compose
Per the discussion on #453, this takes the recommended path of extending
the setup scripts rather than introducing a parallel compose file.
- scripts/setup_remote.sh now generates REDIS_PASSWORD alongside
OSS_JWT_SECRET and POSTGRES_PASSWORD and writes it to the rendered
.env (with a short comment noting it can be rotated, unlike the
postgres password which is baked into the volume on first init).
- scripts/start_docker.sh now generates REDIS_PASSWORD on first run
if missing, mirroring the existing OSS_JWT_SECRET pattern (reuses
generate_secret, which falls back through python3 → openssl →
/dev/urandom).
- docker-compose.yaml and docker-compose-local.yaml now interpolate
${REDIS_PASSWORD:-redissecret} in the redis --requirepass, the redis
healthcheck, and the api REDIS_URL.
The :-redissecret fallback preserves backwards compatibility for users
with an existing .env that predates this change — they keep the old
value until they regenerate. New installs (via either script) get a
secure random hex.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Harden local Docker secret setup
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Abhishek Kumar <abhishek@a6k.me>
* update tuner documentation
* docs: update Tuner integration walkthrough video
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: mohamed salem <259547077+mohamedsalem-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ui): proxy WebSocket signaling upgrade so local web calls work (#425)
1.34.0 replaced the next.config `/api/* -> BACKEND_URL` rewrite with the
Route Handler `api/v1/[...path]/route.ts`. Route Handlers proxy HTTP fine
(so `/api/v1/*` still 200s) but cannot upgrade WebSocket connections. The
removed *rewrite* used to carry the upgrade, so without it the signaling
socket (`/api/v1/ws/signaling/...`) has no proxy path and every local web
call dies before WebRTC negotiation — the symptom reported in #425. nginx
would proxy the upgrade but only runs in the `remote` compose profile, so
local OSS deployments have nothing to carry it.
Re-add a `beforeFiles` rewrite scoped to `/api/v1/ws/:path*` so the upgrade
is proxied to the backend *before* the `[...path]` Route Handler can swallow
it. HTTP `/api/v1/*` is untouched and still flows through the Route Handler
(auth/cookie handling intact).
Verified on a 1.34.0-derived source build: signaling WS now reports
`[accepted]` / `connection open` server-side and `WebSocket connected` +
`ICE connection state: connected` client-side; WebRTC negotiates end-to-end.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ui): use direct localhost WebSocket signaling
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Abhishek Kumar <abhishek@a6k.me>
* fix: disable duplicate trigger nodes in workflow builder
AddNodePanel: disable trigger buttons and show tooltip when a trigger
already exists on the canvas, using bySpecName to identify trigger-
category specs from the live node list.
useWorkflowState: preflight in saveWorkflow rejects saves with multiple
trigger nodes via a sonner toast before the network request is made.
text_chat_session_service: include the original exception message in
TextChatSessionExecutionError so the HTTP 500 detail surfaces the root
cause without DB inspection.
Closes#378
* style: format test_text_chat_session_service.py with ruff
* chore: retrigger CI checks
* fix(workflow): enforce node instance constraints
---------
Co-authored-by: Abhishek Kumar <abhishek@a6k.me>
* fix(qa): tolerate non-dict JSON from QA LLM instead of crashing
parse_llm_json is explicitly designed to return a list when the model emits a
top-level JSON array (it has a dedicated test for that). The QA analyzers then
call parsed.get("tags", ...) directly on the result. When parsed is a list,
that raises AttributeError, which is NOT caught by the surrounding
except (json.JSONDecodeError, ValueError) — so a single stray array response
from the QA model crashed the entire QA analysis run instead of degrading to
empty results.
The live variable-extraction path already guards this exact case with an
isinstance(..., dict) check; mirror it in both QA analysis call sites
(_run_qa_analysis per-node and _run_whole_call_qa_analysis fallback) so a
non-dict parse result coerces to {} and the run produces empty defaults.
Adds a regression test that drives the whole-call analyzer with an array
response and asserts empty results rather than a crash.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(qa): log non-object QA JSON responses
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Abhishek Kumar <abhishek@a6k.me>
validate_trigger_paths used seen_paths.get(trigger_path) and treated a None
result as "path not seen yet". But None is also what node.get("id") returns
for a node without an id, so when the first trigger node sharing a path had no
id, it was stored as None and every later node with the same path was silently
accepted as unique — duplicate trigger paths slipped through validation.
Use a membership test (trigger_path not in seen_paths) so "first occurrence"
and "node_id happens to be None" are no longer conflated. Behavior is
unchanged for nodes that have ids.
Adds a regression test that fails before and passes after.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Add model_options to SmallestAITTSConfiguration's voice field so the UI
renders the correct voice list per model — 15 standard voices for
lightning_v3.1 and 6 premium voices (meher, rhea, aviraj, cressida,
willow, maverick) for lightning_v3.1_pro. All 21 voice IDs verified
against the Smallest AI API. The frontend's existing model_options
machinery already handles dropdown filtering and auto-reset on model
change, so no UI changes are needed.
* fix: add language field to CartesiaTTSConfiguration and pass to TTS service
Closes#432
* chore: regenerate OpenAPI spec to fix drift-check
The openapi.json snapshot had drifted from the FastAPI app definition
because main gained new organization endpoints (billing, credits,
context) after this branch was created. Regenerate it with
'python -m scripts.dump_docs_openapi' to bring it back in sync.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: clarify Cartesia language schema
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Abhishek Kumar <abhishek@a6k.me>
* fix(ui): release microphone stream on call teardown
The browser "Call Voice Agent" test call worked only once per page load.
A second attempt failed (mic stuck / "Could not acquire media") until the
user reloaded the page or cleared site data and re-granted mic permission.
Root cause: the MediaStream from getUserMedia() was added to the peer
connection but never retained or explicitly stopped on teardown. On hangup
only sender.track.stop() (via pc.getSenders()) ran; browsers can keep the
microphone device held through the original MediaStream reference, so the
next getUserMedia() is blocked.
Fix: keep the stream in localStreamRef and stop all of its tracks in
cleanupConnection() (the shared teardown path) and in the unmount cleanup,
so the device is fully released between calls.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ui): harden microphone release in webrtc hook and embed widget
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: AYMENPAKISS2 <tech.nomatrade@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Abhishek Kumar <abhishek@a6k.me>