Skip to main content

Decision: Preview Voice Agent for Safer Deploys

Date: 2026-04-22 Status: Approved — pending provisioning Author: Voice Agent Engineer (Claude) Founder action required: Yes — see FA-058 in FOUNDER_ACTIONS.md


Problem

Today's session had a P0 that reached production because there is no way to test voice-agent-livekit/ code changes against real SIP telephony before promoting to the live agent.

Sequence of events (2026-04-22):

  1. PR #133 shipped a calls_limit + at-capacity TTS fix
  2. The fix silently failed in production — all church calls routed to the Sales Agent
  3. Root cause was a pre-existing FK-join bug in _fetch_voice_agent_row (fixed in PR #136) that had nothing to do with the calls_limit code itself
  4. Revert + redeploy cost ~30 minutes of founder downtime and two separate reverts

The gap: behavioral unit tests and lk agent deploy both passed. The bug only manifested on a real SIP call against real Supabase. There was no staging path.


Proposed Architecture

A second LiveKit agent container (CA_PREVIEW_xxx) in the same cwa-voice-9x077mph project, dedicated to feature-branch validation before the production agent is updated.

Production path (current):
Telnyx/Twilio → LiveKit SIP → CA_pX3Me4NK6qK8 (prod agent, main branch code)

Preview path (proposed):
Telnyx DID (~$1/mo) → LiveKit SIP → CA_PREVIEW_xxx (preview agent, branch code)

The preview agent:

  • Runs inside the same LiveKit project (same billing, same SIP infrastructure)
  • Reads the same production Supabase (no data fork needed for most tests)
  • Is deployed from feature branches via lk agent deploy --project cwa-voice --id CA_PREVIEW_xxx
  • Uses a dedicated phone number that only the founder dials during PR review

Workflow

1. Open PR touching voice-agent-livekit/
2. CI runs existing behavioral unit tests (pytest tests/behavioral/)
3. CI runs new integration test (pytest -m integration) against real Supabase
4. On PR approve → auto-deploy feature branch to CA_PREVIEW_xxx
lk agent deploy --project cwa-voice --id CA_PREVIEW_xxx --silent
5. Founder dials preview number (+1-XXX-XXX-XXXX, TBD)
6. Validate: church answers as its own church (not sales), tools work, TTS fires
7. Merge to main → CI deploys to production CA_pX3Me4NK6qK8

Step 4 can be automated as a GitHub Actions workflow triggered on pull_request events that touch voice-agent-livekit/**.


Provisioning Steps

The following steps create the preview agent. They require the founder to run them once (one-time setup); after that, CI handles deployments.

1. Order Telnyx preview DID

In the Telnyx Mission Control portal:

  • Go to Numbers → Search for a number
  • Choose any US local number (~$1/mo)
  • Assign it to the existing "ChurchWiseAI LiveKit" SIP Connection (Connection ID: see knowledge/runbooks/voice-provisioning.md)
  • Note the number (e.g., +12265550123) — this becomes the preview dial number

2. Create the LiveKit preview SIP trunk

# Create a new SIP inbound trunk for the preview number
C:\dev\lk.exe sip inbound create \
--project cwa-voice \
--name "CWA Preview" \
--numbers "+12265550123"
# Note the returned trunk ID (ST_xxx)

3. Create a dispatch rule for the preview trunk

The dispatch rule uses agent_name="churchwiseai-voice" — the same agent name as production. LiveKit dispatches to whichever container is running with that name. When the preview agent is deployed, it registers under the same name, so calls to the preview trunk go to whichever container most recently registered.

Note: If both prod and preview containers are running simultaneously, LiveKit dispatches round-robin. To prevent preview calls hitting prod, always deploy preview AFTER stopping the old preview container, or use a distinct agent name (e.g., churchwiseai-voice-preview) and update the dispatch rule accordingly. The distinct-name approach is recommended for production-quality isolation.

C:\dev\lk.exe sip dispatch create \
--project cwa-voice \
--trunk-id ST_xxx \
--agent-name "churchwiseai-voice-preview" \
--room-prefix "preview-"
# Note the returned dispatch rule ID (SDR_xxx)

4. Update main.py to accept preview agent name

In main.py, change:

@server.rtc_session(agent_name="churchwiseai-voice")

to support both names (one file, two registrations):

@server.rtc_session(agent_name="churchwiseai-voice")
@server.rtc_session(agent_name="churchwiseai-voice-preview")

This allows the preview agent to use a distinct name without code duplication.

Alternatively, make the agent name an env var:

import os
_AGENT_NAME = os.environ.get("AGENT_NAME", "churchwiseai-voice")

@server.rtc_session(agent_name=_AGENT_NAME)

Then set AGENT_NAME=churchwiseai-voice-preview in the preview agent's secrets. This is the cleaner approach and avoids decorator stacking.

5. Add preview number to PHONE_REGISTRY or handle in resolve_route

Option A — add to PHONE_REGISTRY in session.py (simple):

"+12265550123": ("demo_router", "00000000-0000-4000-a000-000000000001"),
# Routes to Grace Community demo for testing

Option B — detect preview numbers in resolve_route and route to a known demo church:

PREVIEW_NUMBERS = {"+12265550123"}
if dialed_number in PREVIEW_NUMBERS:
return "church", "00000000-0000-4000-a000-000000000001"

Option A is simpler. Option B is more explicit and makes preview routing grep-able.

6. Deploy preview agent for the first time

# From voice-agent-livekit/ directory on a feature branch:
C:\dev\lk.exe agent deploy --project cwa-voice --id CA_PREVIEW_xxx --silent

7. Update PHONE_REGISTRY smoke-test

Add the preview number to the test case in test_routing.py to ensure it resolves to a known demo church rather than falling through to sales.


Gotchas

Shared Supabase writes: The preview agent writes to the same voice_call_logs, voice_prayer_requests, etc. tables as production. Demo church UUIDs are the correct test targets (they use the 00000000-0000-4000-a000-* range that is excluded from customer-facing queries). Never dial a real church number on the preview agent.

Cache collisions: The in-process LRU cache in session.py is per-container, so preview and prod caches are independent. No collision risk.

LiveKit agent registration: When you run lk agent deploy, the new container takes ~90 seconds to register. Run lk agent list to confirm the preview agent shows Available before dialing.

SIP credential inheritance: The preview SIP trunk must use the same "no auth" setting as the production trunk (empty credentials). Telnyx sends no SIP credentials by default on outbound calls to LiveKit; the trunk must be configured to accept unauthenticated inbound.

Cost: Telnyx DID ~$1/mo. LiveKit agent compute is billed per-minute-of-active-calls; idle containers are free. Total preview infrastructure cost: ~$1/mo + call-time charges during testing (negligible for a handful of test calls per PR).


Non-Goals (Today)

  • Actual implementation (pending founder provisioning the Telnyx DID)
  • Automated CI step 4 (auto-deploy to preview on PR approve)
  • Full test isolation (separate Supabase branch / schema)

These are follow-on tasks once the preview number is provisioned and the basic workflow is validated manually.


Decision

Approved as the right approach for the ChurchWiseAI voice system:

  • Low cost (~$1/mo)
  • No architectural complexity (same codebase, same LiveKit project)
  • Directly addresses the P0 failure mode: "FK bug reaches prod because there's no SIP-level staging path"
  • Compatible with the existing CI behavioral test suite

Implementation is blocked on the founder provisioning the Telnyx DID (FA-058).


  • knowledge/decisions/2026-04-22-voice-fk-join-regression.md — the P0 this prevents
  • knowledge/runbooks/voice-provisioning.md — existing provisioning runbook
  • FOUNDER_ACTIONS.md#FA-058 — provisioning action item