Operations
One service, one deploy command, a deliberate log vocabulary, live self-checks, and offline verification suites. This page is the runbook, plus the full configuration reference, variable names and meanings only; values live in Secret Manager and the service config, never in documentation.
Deploy
npm install # once per checkout, before anything below ./tools/deploy.sh # typecheck, run every offline suite, then deploy ./tools/deploy.sh --dry-run # print the exact command, change nothing
The deploy configuration lives in the script rather than in a command someone retypes.
Retyping is how a variable gets dropped, and a dropped variable does not fail loudly: it falls
back to a default and the service comes up subtly wrong. Two details in there are worth
knowing. The variable list uses gcloud's ^@^ custom delimiter because
LANGUAGES=en,es contains a comma, and under the default comma delimiter that
would set LANGUAGES=en and quietly turn bilingual mode off. And only secrets that
actually exist in Secret Manager are attached, so a project missing an optional one deploys
with a note instead of an error.
--update-env-vars touches only the variables the script lists; everything else
already on the service survives, which is why one-time settings like the calendar id and the
project id are deliberately not in it.
Cloud Build compiles from source and swaps the revision with zero downtime;
min-instances=1 keeps one instance warm so calls never hit a cold start. Secrets
attach with --update-secrets (Secret Manager references), plain configuration
with --update-env-vars. The process fails fast at boot if the selected voice
mode's credentials are missing, so a bad config dies at deploy time, visibly.
Observe
The log vocabulary is a contract, one grep per question:
| Prefix | Answers |
|---|---|
[timing] | Where each turn's milliseconds went: utterance end path, LLM first token, first sentence, first audio to caller. |
[voice] | Conversation events: utterances, barge-in triggers, echo suppressions, language switches, merged turns, turn failures. |
[call] | Call boundaries with caller number and abuse tier. |
[llm] | Token accounting per call: input, cache read, cache write, output, the live proof caching works. |
[sms] · [telegram] | Channel traffic, including compliance events (consent confirmations, outreach sends) and gate rejections. |
Self-checks: the admin surface
Two token-guarded owner views close the loop between "configured" and "actually working"
(src/admin.ts):
/admin/statusprobes integrations live rather than echoing config: a real Calendar free/busy call proves calendar access; the intake bot's API is queried and its registered webhook compared against this service's URL, with pending-update counts and the platform's own last-error surfaced; booking, SMS, recording, and the active voice architecture each get a row (with a warning when the pricier demo architecture is live). JSON twin at/admin/status.json./admin/calendar-checkruns the booking calendar end to end as the service's own account: reads free/busy, creates a tentative hold, confirms it reads as busy, moves it, deletes it. This is the authoritative answer to "can Elle actually book", because it is the only check that runs as the identity that does the booking. The command-line equivalent runs as whoever is at the terminal./admin/callslists recent calls with caller number, duration, and expandable full transcripts, how an unknown caller gets identified without log spelunking.
Verify offline
npm run typecheck npm run verify:bridge # barge-in flush/truncate/cancel, audio both ways npm run verify:factory # runtime architecture switch + demo deadline npm run verify:audio # phone audio conversion across chunk boundaries npm run verify:gemini # Vertex duplex adapter, against a fake session npm run verify:intake # the owner's call report, against a seeded DB
Those fake their dependencies, which is the point: they prove the orchestration without a phone, a key, or a network. What they cannot prove is that this deployment can reach the things it needs. Two live checks cover that gap:
./tools/smoke-calendar.sh # real calendar: read, create, busy, move, delete ./tools/smoke-gemini-live.sh # real Vertex duplex session, writes a WAV npm run verify:cascade # streaming, tool loop, re-framing, bilingual voices npm run verify:knowledge # chunking + BM25 retrieval npm run verify:booking # tentative holds, busy slots, lead capture npm run verify:calendar # Calendar REST client, incl. the guest fallback npm run verify:policy # abuse tier escalation npm run verify:recording # WAV framing + sidecar
Every suite fakes its network dependencies (Twilio socket, speech streams, the LLM) and exercises the real orchestration logic. The discipline: anything tricky enough to have broken once gets an offline assertion so it can't silently break twice.
Configuration reference
Everything tunable is an environment variable, documented in .env.example.
Names and purposes only, secrets are set via Secret Manager, never written down:
Identity and transport
| Variable | Purpose |
|---|---|
PUBLIC_URL | This service's own base URL (webhooks, media stream, signature validation). |
SITE_URL · ASK_PATH | The human-facing site and the single page carrying both chat and call pairing. Everything visitor-facing points here; legacy paths on the service origin redirect to it. |
VOICE_MODE | realtime (barge-in pipeline) or cascaded (Gather/Say fallback). |
REALTIME_PROVIDER | Default voice architecture: deepgram (production cascade), gemini (fused, on Vertex AI, no API key), or openai (fused, separate vendor). Runtime-overridable by the owner; a fused override reverts on its own after its deadline. |
PROFILE_NAME · ASSISTANT_NAME | Who Elle represents, and what she's called. |
CORS_ORIGIN | Origins allowed to call the public API. |
Speech and language
| Variable | Purpose |
|---|---|
DEEPGRAM_API_KEY | SECRET One key covers STT and TTS. |
DEEPGRAM_STT_MODEL · DEEPGRAM_STT_MULTI_MODEL | English-only vs. code-switching STT models. |
DEEPGRAM_TTS_MODEL · DEEPGRAM_TTS_MODEL_ES | Elle's English and Spanish voices. |
LANGUAGES | Conversation languages; more than one enables bilingual mode. |
DEEPGRAM_ENDPOINTING_MS | Trailing silence before a caller's turn is considered finished. |
DEEPGRAM_BARGE_IN_MIN_CHARS · DEEPGRAM_BARGE_IN_GRACE_MS | How much recognized speech interrupts, and the grace window after Elle starts talking. |
Reasoning
| Variable | Purpose |
|---|---|
GEMINI_LIVE_MODEL · GEMINI_LIVE_LOCATION · GEMINI_LIVE_VOICE | The Vertex duplex model, its region (the Live API is not offered everywhere, so it is set separately from VERTEX_REGION), and its voice. No credential: auth is ADC. Confirm the model is enabled for the project with tools/smoke-gemini-live.sh before relying on it. |
LLM_PROVIDER | anthropic (default), vertex (same models inside a GCP BAA), or openai-compatible endpoints. |
ANTHROPIC_API_KEY | SECRET When using the Anthropic API directly. |
CLAUDE_MODEL · VERTEX_* · OPENAI_* | Model and endpoint selection per provider. |
Telephony, booking, and channels
| Variable | Purpose |
|---|---|
TWILIO_AUTH_TOKEN | SECRET Webhook signature validation, and the key for media-stream tokens. |
TWILIO_ACCOUNT_SID · TWILIO_FROM_NUMBER | Outbound SMS (consent confirmations, outreach). |
BOOKING_CALENDAR_ID · BOOKING_TIMEZONE · BOOKING_DURATION_MINUTES | Where tentative holds land and how meetings are shaped. |
TELEGRAM_BOT_TOKEN · TELEGRAM_WEBHOOK_SECRET | SECRET The owner intake bot and its webhook gate. |
TELEGRAM_OWNER_ID | The single account the intake channel obeys. |
ADMIN_TOKEN | SECRET Enables the admin views; absent = they 404. |
ABUSE_* | Repeat-caller thresholds, window, and the limited-tier duration cap. |
RECORDING_* | Off-by-default call recording: enable flag, GCS bucket, prefix. |
Known operational habits
- SQLite rides the instance. Facts and transcripts persist across calls but not across revision replacements; the documented upgrade is a managed database when that trade stops being acceptable.
- Webhook registration is stateful. The intake bot's webhook and secret
are registered against the platform once per secret rotation;
/admin/statusis the fastest way to see whether registration and the deployed config agree. - Everything optional degrades. No calendar → bookings become leads. No outbound SMS credentials → consent texts are skipped and logged. No bot token → the intake route stays dark. The phone always answers.