# 04 — Escalation to human (restaurant-v1)

**Binding: `docs/SPEC.md` D8.** *"A config without a tested escalation path cannot pass `RELEASE_CHECKLIST.md`."* All four triggers are always on. Escalation is not a tier feature (`docs/PRICING.md` §1 — present on Starter, Growth and Pro alike) and it is never disabled at a client's request.

`docs/TESTING.md` §3 calls escalation correctness **the one zero-tolerance metric**: any missed trigger is an immediate fix plus a re-run of QA cases 5–8.

---

## The four triggers

| # | Trigger | How it is detected | QA case |
|---|---|---|---|
| 1 | **Caller asks for a human** | Any phrasing: "a real person", "someone who works there", "put me through", "is this a robot?" followed by wanting a person | 6 |
| 2 | **Caller expresses frustration twice** | Two frustration signals in one call, with or without the word "human" — sighing complaints, "you're not understanding me", "this is useless", repeating themselves louder, swearing | 7 |
| 3 | **Sensitive content** — complaint, medical, legal | Illness after eating, injury on premises, allergic reaction, legal threat, press enquiry, anything involving a lawyer, regulator or health inspector · **large party / private event** also routes here (commercial, not sensitive, but human-only) | 5, 13 |
| 4 | **Booking or payment dispute** | "I booked and you had no record", "I was charged", "I want my deposit back", any assertion the restaurant got it wrong | 5 |

**Trigger 2 is the one that gets missed.** A caller who never says "human" but is plainly fed up must still be escalated by their second signal. Configure this explicitly in the prompt; it does not emerge on its own.

## What escalation does

### During staffed hours → **warm transfer**

```
  acknowledge in ONE sentence — apology if warranted, no explanation, no defence
        ↓
  "Let me put you through to someone right now."
        ↓
  transfer_to_human  →  STAFFED_HOURS_TRANSFER_NUMBER
        ↓
  transfer connects?
        ├── YES → assistant leaves the call
        └── NO  → after-hours path below (QA case 8)
```

At most **one** qualifying question ("May I ask what it's regarding?") — and none at all when the trigger is 3 or 4. An angry caller asked to justify themselves gets angrier, and QA case 6 explicitly limits this to one.

### Outside staffed hours, or transfer fails → **structured message + instant alert**

```
  "I'm sorry — I can't put you through right now, but I'll make sure
   the owner has this first thing."
        ↓
  capture: name, phone, what happened, in their words
        ↓
  capture_lead  {"intent": "complaint"}  →  INSTANT delivery (API_CONTRACT §3)
        ↓
  alert to AFTER_HOURS_ALERT_DESTINATION  ← fires even if the lead webhook is down
        ↓
  tell the caller WHEN: the callback window from the knowledge pack
```

The alert is **instant, not batched** — `docs/API_CONTRACT.md` §3 requires instant delivery for `booking` and `complaint` intents. A complaint sitting in a nightly digest is a churned customer.

---

## What the assistant must never do while escalating

| Never | Because |
|---|---|
| Argue, or defend the restaurant | Not its call to make, and it will lose |
| Promise a refund, a free meal, a voucher, or compensation | Commercial authority it does not have. QA case 5 fails on any such promise |
| Explain what went wrong, or speculate | It does not know, and its guess becomes the restaurant's official position |
| Ask the caller to repeat the complaint | It was captured the first time |
| Say "calm down" or any variant | Guarantees escalation of the human kind |
| Apologise more than once | Repeated apology reads as stalling |
| Resist a transfer request | Trigger 1 is unconditional |
| Take a complaint and end the call without capturing a number | A complaint with no callback path is a lost customer with a grievance |

## Wording — scripted, not improvised

These lines are in the system prompt as fixed text. Improvised empathy is where voice agents produce their worst moments.

| Situation | Say |
|---|---|
| Angry caller, staffed hours | "I'm sorry that happened. Let me put you through to someone who can sort it out right now." |
| Angry caller, after hours | "I'm really sorry. I can't reach anyone at this hour, but I'll take the details and {{OWNER_ROLE}} will call you first thing in the morning." |
| Explicit human request | "Of course — one moment." |
| Frustration detected (trigger 2) | "I don't think I'm helping much here — let me get you a person." |
| Transfer failed | "I can't get through to them right now. Let me take your number so they can call you straight back." |
| Large party | "For a group that size I'll get you to someone directly — they'll look after the details." |
| Illness / injury / allergic reaction | "I'm very sorry to hear that. I'm putting you through to a person now." → transfer immediately, capture regardless of outcome, **flag urgent** |
| Legal / press / regulator | "I'll pass this to the owner directly — can I take your name and number?" → **no transfer**, no comment of any kind |

## Configuration checklist (walked at go-live — `docs/RELEASE_CHECKLIST.md` §B)

☐ `STAFFED_HOURS_TRANSFER_NUMBER` set and **verified by a live test call** (not just typed in)
☐ Staffed hours entered separately from opening hours — they usually differ
☐ `AFTER_HOURS_ALERT_DESTINATION` verified by a live test alert
☐ All four triggers present in the prompt, each with its scripted line
☐ Trigger 2 (frustration ×2) explicitly configured, not assumed
☐ Instant-delivery flag set for `complaint` and `booking` intents
☐ Alert path tested with the lead webhook deliberately killed (`docs/RELEASE_CHECKLIST.md` §C)
☐ QA cases 5, 6, 7, 8 passed **on the launch config version**
