0057 — Clara's voice promises must be backed by real, verified delivery (unknown-caller path)
- Status: Proposed — fix implemented on branch
fede/unknown-caller-always-notify; the live ElevenLabs cutover of the transfer tools + prod merge are gated on the sandbox sign-off (see Decision 2). - Date: 2026-06-15
- Deciders: Fede
- Reproduction:
src/__tests__/unknown-caller-promise-delivery.repro.test.ts(+ escalate sibling); see "Steps to reproduce" below.
Context
When an inbound voice caller can't be matched to a resident, prospect, or vendor on file, Triage routes them to the unknown_caller voice sibling (ADR-context: PR #1993). That sibling has exactly two webhook tools, and both are bookkeeping that perform no real-world action:
capture_unknown_caller_notewritesConversation.unknownCaller = {status:'new', …}and returns a message that Clara reads aloud — "I've passed your message along to the team and someone will follow up." No email, no SMS, no page fires. The note lands on a dashboard queue that, in practice, nobody watches (of 153 such notes, 150 are stillstatus:'new'; the overwhelming majority are PR-D backfill, but the live ones sit untouched too).escalate_to_humanon voice places no telephony. For a "get me a person" request it stampsconversation.transferredToand makes Clara say "Transferring call to…", but no call leg is dialed. It only emails the PM forurgency ∈ {high, gas_emergency}— so amediumrequest notifies no one. The prompt instructs the sibling to usetransfer_to_number, but that native tool is not wired onto this agent (unknown-caller.config.jsonis empty; only the two webhook tools tagunknown_caller).
Net: on this path, every "someone will follow up" / "connecting you now" is a promise the system cannot keep. Verified across four independent layers:
- Code/config — the two-tool surface above;
PM_PAGE_URGENCY_TIERS = {gas_emergency, high}. - Production data (real, test-excluded) — e.g. a utility's water-shutoff notice and a billing complaint, same property, same day (2026-06-15), each captured/escalated with zero delivery signals; a vendor's two attempts to deliver a resident's rent checks (2026-04-21 transferred to an empty office; 2026-05-20 the message disappeared with
escalatedAt/transferredTo/workOrderId/unknownCallerall null). - Telephony (other session, Twilio) — a
mediumescalate returnedtransferred:truewith 0 child legs dialed. This disproved an earlier read of ours:transferredTobeing set is bookkeeping, not proof of a transfer. (The April re-greet loop is separately fixed by #2113/#2341 — that is a different bug from "the caller never reaches a human.") - Human/operator — the community manager emailed a resident's vendor a ledger with "Sorry if we've missed your calls… [the AI] does have an option to connect with a person. Next time, if we don't answer, please send us an email" — the PM is absorbing the trust cost and routing people off the phone line because the "connect to a person" option does not actually connect.
A wider audit of 1,377 voice calls found the same promise-without-delivery class leaking into other flows (lost work orders, unsent renewal texts, a tour_inquiry vs billing misclassification). This ADR scopes only the unknown_caller path; the others get their own ADRs/tickets so this one stays shippable.
Decision
Adopt one invariant and rebuild the unknown_caller action surface to satisfy it.
Invariant (binding): Clara may not speak a promise of human follow-up, notification, or transfer unless the corresponding action has actually been performed and the tool result confirms it. Her spoken wording is derived from the tool's delivery result, never hardcoded ahead of it.
Concretely:
Taking a message delivers it.
capture_unknown_caller_notemust, as part of taking the message, send a real notification to the property team's escalation inbox (reusing thesendEmail/pagePropertyManagerleg with a calm, non-life-safety template). Its return value carriesdelivered: true|false. Clara only says "I've let the team know / someone will follow up" whendelivered === true. Onfalse(no inbox configured, send failed) she does not claim a human was reached, and the failure is logged loud (Sentry) — never a silent no-op.A transfer must actually transfer. When Clara hands a caller to a person it must be a real telephony transfer.
transfer_to_numberis now declared on theunknown_calleragent (unknown-caller.config.jsoninline_tools[]— theconferencetransfer to{{pm_phone_number}}) and pushed to the live agent by a dedicated, gated cutover script (scripts/cutover-unknown-caller-transfers.ts, run with--applyonly after a Test Property clone + 3-channel pass). That script is deliberately not insync-specialists.yml, so merging this branch can't ship the live transfer by itself.escalate_to_humanmust never emit "Transferring call…" on a path that places no call. Known limitation: EL's nativeconferencetransfer gives the app no no-answer signal, so a transfer that rings out is not yet auto-detected. The guarantee "a transfer that can't connect degrades to a delivered message + PM page" is the target; the implementation is tracked indocs/planning/unknown-caller-transfer-no-answer-detection.md(recommended Phase 1: page the PM at transfer-time incall-ended, a one-file change shippable separately). Until that lands, the prompt closes the gap behaviorally — transfer is reserved for callers who insist, the bias is to a delivered message, andescalate_to_humanalready pages the PM on every tier. The transfer tools were verified against a real ElevenLabs agent on 2026-06-16 (clone + Simulate-Conversation,scripts/voice-harness/verify-unknown-caller-transfers.ts): EL accepts the config, and Clara firestransfer_to_numberon insist,transfer_to_agent→leasing on revealed intent, and a delivered message otherwise. That harness also caught a residual wording bug —escalate_to_humanreturned "Transferring call…" on voice (Clara echoed "connecting you now") while only paging, never placing a call. Two complementary fixes: the handler no longer narrates a transfer (delivery-conditional, honest on every channel — landed independently via ADR-0058 / PR #2411), and the unknown_caller prompt adds a hard honesty rule: "connecting/transferring you" is true only in the same turn a real transfer tool fires; paging and message-taking speak in future tense (re-verified on the clone — an insister with no transfer tool now hears "someone will follow up shortly", not "connecting you now"). The no-answer follow-up is decided (notify the PM only on a no-answer, and forward the caller's voicemail to them — seedocs/planning/unknown-caller-transfer-no-answer-detection.md), not "page on every transfer."Bias to taking a message; transfer is the exception. Because a captured message now actually reaches the PM, it is the default for anything non-urgent. Reserve a live transfer for the narrow cases that need a person now — a safety/urgent situation, or a caller who explicitly insists on a human. Routing: can Clara handle it herself? → handle it; is it urgent, or does the caller insist on a person? → transfer (for real); otherwise → take a message that is actually delivered to the PM. Minimizing transfers also shrinks the surface where a "connecting you" promise can break — fewer transfer attempts, fewer ways to fail one.
Revealed intent routes to the right specialist. The
unknown_callersibling now carries atransfer_to_agentsystem tool, so when a caller's real need becomes clear mid-call Clara hands them to the specialist who can actually help instead of parking a message: a brand-new leasing prospect → leasing (agent_8601…); a current resident with a maintenance issue → maintenance intake (agent_8301…). Routing is high-precision and narrow — when identity or intent is unclear, the default (a delivered message) wins over a wrong transfer, and a failed handoff falls back to a delivered message rather than a dead end. Same gated cutover as Decision 2 (declared inunknown-caller.config.json, applied by the cutover script after the sandbox pass).A guard test makes regression impossible. A unit test asserts that no
unknown_callertool may return follow-up/notification/transfer wording without a corresponding delivery side-effect; a second test (unknown-caller-transfer-wiring.test.ts) fences the declared transfer-tool shape and the cutover merge. The reproduction specs (below) are the first guard in its initial, RED state.
Entity classification
No new entities. This ADR changes the behavior of existing handlers and the unknown_caller prompt/tooling; the Conversation.unknownCaller field is unchanged in shape.
Consequences
- Easier: every taken message reaches a human; Clara's spoken promises become true by construction; PMs stop discovering dropped calls via angry follow-ups.
- Harder / commits us to: a gated live cutover of the sibling's system tools (
transfer_to_number+transfer_to_agent) — a customer-facing voice change run via Test Property clone + 3-channel pass, never straight to a live property (the cutover script sits outside the merge workflow so a merge can't ship it); a calm "message for the team" email template distinct from the life-safety page; building the no-answer detection so a rung-out transfer degrades to a delivered message + page (design doc linked in Decision 2). The property escalation inbox is already configured (Camellia →camelliaapts@jp-co.com), so it is a prerequisite we already meet, not new work. - Follow-up work: the live cutover of the transfer tools (gated on the sandbox sign-off, via
scripts/cutover-unknown-caller-transfers.ts --apply); the no-answer detection (docs/planning/unknown-caller-transfer-no-answer-detection.md, Phase 1 shippable separately); manual remediation emails to the verified affected callers (Tier 1, off-repo for PII); separate ADRs/tickets for the lost-work-order, unsent-renewal-text, andissueTypemisclassification gaps surfaced by the audit.
Alternatives considered
- Kill the tool, always transfer/escalate. Rejected — it discards genuinely useful async message-taking (vendor/utility heads-ups), and the escalate path is itself broken at
medium, so it wouldn't fix the promise. - Bolt an email onto the capture tool only. Rejected — it leaves the "connecting you now" lie and the urgency-gating intact, and doesn't make Clara's wording conditional on delivery. The invariant, not a patch, is the point.
- Keep dashboard-only, train PMs to watch the queue. Rejected — that is today's design, and the operator email is direct evidence it fails in production.
Steps to reproduce
In production (observed, real callers, test data excluded): an inbound voice call that Triage can't match routes to unknown_caller; the caller leaves a routine message or asks for a person; the conversation row ends with unknownCaller.status:'new' and/or transferredTo stamped while escalatedAt/workOrderId are null and no email/SMS is sent (Twilio shows 0 child legs for the "transfer"). No human is notified, though Clara's spoken close promised one.
In the test suite (deterministic, ships with this ADR):
npx vitest run src/__tests__/unknown-caller-promise-delivery.repro.test.ts \
src/__tests__/unknown-caller-escalate-no-transfer.repro.test.ts
These specs encoded the broken invariant in their initial it.fails (green-while-bug-exists) form; with the fix landed they are now plain it() assertions that lock it in — a human is notified before Clara promises follow-up, and escalate_to_human pages the PM on every tier. The transfer wiring itself is fenced by src/__tests__/unknown-caller-transfer-wiring.test.ts (config shape + the cutover merge function).
Multi-turn, prompt level (coverage hole filled):
npx tsx evals/voice-simulation/run-scenarios.ts uc-adv-RI-01 --probe # revealed-intent → leasing
npx tsx evals/voice-simulation/run-scenarios.ts uc-adv-IH-01 --probe # insists on a person → transfer
# live (cutover only — hits ElevenLabs, tests the DEPLOYED agent): drop --probe
Out of scope here (separate tickets): the parallel promise-without-delivery gaps the same audit surfaced — lost work orders (workOrderId null after "your work order is in"), unsent renewal texts ("I'll text you the details"), and the issueType: tour_inquiry vs topics: billing misclassification.