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

shell
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:

PrefixAnswers
[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/status probes 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-check runs 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/calls lists recent calls with caller number, duration, and expandable full transcripts, how an unknown caller gets identified without log spelunking.

Verify offline

shell
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:

shell
./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

VariablePurpose
PUBLIC_URLThis service's own base URL (webhooks, media stream, signature validation).
SITE_URL · ASK_PATHThe 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_MODErealtime (barge-in pipeline) or cascaded (Gather/Say fallback).
REALTIME_PROVIDERDefault 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_NAMEWho Elle represents, and what she's called.
CORS_ORIGINOrigins allowed to call the public API.

Speech and language

VariablePurpose
DEEPGRAM_API_KEYSECRET One key covers STT and TTS.
DEEPGRAM_STT_MODEL · DEEPGRAM_STT_MULTI_MODELEnglish-only vs. code-switching STT models.
DEEPGRAM_TTS_MODEL · DEEPGRAM_TTS_MODEL_ESElle's English and Spanish voices.
LANGUAGESConversation languages; more than one enables bilingual mode.
DEEPGRAM_ENDPOINTING_MSTrailing silence before a caller's turn is considered finished.
DEEPGRAM_BARGE_IN_MIN_CHARS · DEEPGRAM_BARGE_IN_GRACE_MSHow much recognized speech interrupts, and the grace window after Elle starts talking.

Reasoning

VariablePurpose
GEMINI_LIVE_MODEL · GEMINI_LIVE_LOCATION · GEMINI_LIVE_VOICEThe 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_PROVIDERanthropic (default), vertex (same models inside a GCP BAA), or openai-compatible endpoints.
ANTHROPIC_API_KEYSECRET When using the Anthropic API directly.
CLAUDE_MODEL · VERTEX_* · OPENAI_*Model and endpoint selection per provider.

Telephony, booking, and channels

VariablePurpose
TWILIO_AUTH_TOKENSECRET Webhook signature validation, and the key for media-stream tokens.
TWILIO_ACCOUNT_SID · TWILIO_FROM_NUMBEROutbound SMS (consent confirmations, outreach).
BOOKING_CALENDAR_ID · BOOKING_TIMEZONE · BOOKING_DURATION_MINUTESWhere tentative holds land and how meetings are shaped.
TELEGRAM_BOT_TOKEN · TELEGRAM_WEBHOOK_SECRETSECRET The owner intake bot and its webhook gate.
TELEGRAM_OWNER_IDThe single account the intake channel obeys.
ADMIN_TOKENSECRET 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/status is 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.