# When a call is answered in silence

A call that connects and then plays nothing leaves the same trace as a caller who hung up immediately. On 2026-09-07 five inbound calls sat silent and were recorded as abandonment. The cause was a greeting the process could not synthesise. The call now ends deliberately, posts `engine_error {at: greeting}`, and is written down as something that was never a conversation.

- **Status:** Available
- **Audience:** both, developer
- **Channels:** phone
- **In the app:** #/calls, #/needs-you
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/research/silent-failure/

## Two very different calls, one indistinguishable record

A caller who rings, hears nothing and hangs up after four seconds produces a short answered call with no caller speech. A caller who rings, is answered by a process that cannot speak, and hangs up after four seconds produces exactly the same thing. One is ordinary; the other is a total failure of the product, and for a while both were summarised as *the caller hung up before speaking*.

A failure that files itself under a benign heading is worse than a loud one. Nobody investigates a hang-up. The five calls were only found because a restart pattern was being looked at for another reason.

## What was actually happening

The SDK runs each call in its own process and calls the prewarm hook in every idle process. Each of those processes asked the text-to-speech model for the same greeting sentence, under the same cache key, in the same millisecond. The model's per-minute quota answered one of them **429** — and a refused synthesis was treated as a permanent answer, so that process had no greeting for the rest of its life. Whichever call landed on it was answered in silence.

A second, quieter contributor sat underneath: process initialisation had a default bound of 10 s, which was *shorter* than the warm-up's own budget. The warm-up could be killed for being slow before it had a chance to fail for being refused.

## The fix has two halves

1. Stop the stampede. Warm-up now spreads processes over `WARM_STAGGER_S` and retries a refused synthesis `WARM_RETRIES` times with backoff when the refusal is one that passes — 429 and 5xx, never a bad key.
   - Result: `refused` is counted separately from `failed`, so a quota problem and a broken credential no longer look alike. The worker asks for `initialize_process_timeout` 45 s against a `WARM_TIMEOUT_S` of 30 s.
2. Make an unsayable greeting end the call rather than start one. `_say` asks once more when the refusal passes; if the line still cannot be spoken on a model that takes no generated line, the worker posts `engine_error {at: greeting}` and ends with `hangup_by=engine`.
   - Result: The engine writes `facts.silent_failure` from that event, `voice.no_conversation` says so, and the outcome is `not_reached` — a call that never rang a conversation into existence.

The warm-up itself is the reason the greeting is worth this much trouble: a cached line starts in **0 ms** against **3.0 s** for live synthesis, and `greeting.prewarmed` on the event says which of the two the caller got.

## The vocabulary that keeps the cases apart

**`no_answer`** — It rang and nobody picked up. An ordinary outcome.
**`not_reached`** — It never rang — the dial, the dispatch or the worker failed. Not the caller's doing.
**`silent_failure`** — It was answered and nothing could be said. Written from the `engine_error` event, not inferred.
**`line_off` / `after_hours`** — A deliberate refusal: the closed-line message was spoken in the business's own name, then the call ended.
**`hangup_by`** — One of caller, model, owner, engine, carrier, agent. Before this, every ending that was not the caller's read *agent*.

`_capture_lead` creates no lead from a call that was never a conversation, which stops a run of failures from quietly manufacturing prospects. The refusal path is equally deliberate: a switched-off line used to be a bare 409 that the worker answered by deleting the room, so the caller heard the call connect and drop and no `Call` row existed at all. It now returns a refusal brief carrying `closed_line_text` — deterministic, warmed alongside the greetings, and with no opening hours in it.

## What is still only partly covered

- `voice_engine.line_health` catches the adjacent failure — a worker that has not checked in, so an inbound call reaches nothing at all — which the Phone screen once reported as *ready*. It drains by itself when the cause clears.
- Silence with a cause outside the process, such as a one-way audio path on the carrier leg, is not covered by this event and would still read as a short call.
- The five calls are the only counted instance. No ongoing rate of silent answers is measured here: UNKNOWN.
- The guard depends on the greeting being a known utterance. A model that could generate its own opening would need a different signal that speech began.

## Questions

### Why end the call instead of continuing without a greeting?

Because the caller has no way to know a conversation started. Continuing produces a call where the first thing a stranger hears is silence, then possibly a reply to something they said into that silence. Ending immediately with the failure on the record is honest, and it makes the fault countable.

### How would I notice this on my own line?

Compare the carrier's own call log against the call records. A completed, charged call on their side with nothing or a four-second nothing on yours is the signature. That comparison is the fastest audit available on any telephony integration and it has found more than one defect here.

### Does a refused greeting count against the caller?

No. The outcome is `not_reached`, no lead is captured, and the ending is attributed to the engine rather than to the caller — which is the whole point of `hangup_by` having six possible values instead of defaulting to one.

## Related

- [Barge-in without cutting the greeting in half](https://connectbyjbrh.com/research/barge-in-without-cutting-the-greeting/)
- [Measuring conversational latency correctly](https://connectbyjbrh.com/research/measuring-voice-latency/)
- [Phone and voice in Connect](https://connectbyjbrh.com/docs/phone/)
- [Troubleshooting](https://connectbyjbrh.com/docs/troubleshooting/)
- [Why uncertainty is a valid answer from a business agent](https://connectbyjbrh.com/research/uncertainty-is-an-answer/)

## What this page is based on

- `docs-source/sources/PHONE.md` §8 — greeting warm-up, the 2026-09-07 failure and the retry policy
- `docs-source/sources/PHONE.md` §9 — refusal, attribution and `NOT_A_CONVERSATION`
- `docs-source/sources/PHONE.md` §5 — line health and worker check-in
- Connect capability registry (`docs-source/facts.py`) — `MEASURED`
