# Preventing a voice agent from promising a call-back it cannot keep

A model will say *someone will ring you at four* whether or not anything was recorded. Connect refuses the record rather than trusting the sentence: `record_event` rejects a time that shares no time word with the caller's most recent time-bearing line, unless the caller agreed to a time the voice said aloud. The tool answers **NOT recorded**, so the model cannot report a booking it does not hold.

- **Status:** Available
- **Audience:** both, developer
- **Channels:** phone
- **In the app:** #/calls, #/follow-ups
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/research/callback-promises/

## The failure this rule was written against

One reviewed call looped three times: the voice offered a call-back, the caller accepted, the tool recorded something that did not match, the voice asked again. Underneath was a simpler problem than a loop. A model asked to book a time will produce a plausible time, and a tool that accepts whatever it is handed turns that plausibility into a row. The customer then waits for a call at a time nobody agreed.

A promise a business does not keep is more expensive than no promise. The design question is therefore not *how do we get the model to be careful* but *what does the tool refuse*.

## The time must come from the caller's own words

`record_event` compares the `when` the model supplies against the caller's latest time-bearing line. If the two share no time word, `conversation.when_mismatch` refuses the record. The comparison runs in English, Telugu and Hindi.

- **The caller named a time** — the words overlap, the follow-up is recorded.
- **The caller agreed to a time the voice said aloud** (`when_agreed`) — recorded, because the words are on the wire even though the caller did not originate them.
- **Neither** — refused, and the tool returns *NOT recorded* rather than a success the model can paraphrase into a promise.
- **Neither, and the call is still live** — a read-back is asked for, so the caller hears the time and can correct it. A read-back is only asked when it is needed, not on every booking.

> **Note** The refusal text matters as much as the refusal. A tool that fails silently leaves the model free to narrate success; a tool that says *NOT recorded* gives it something true to say.

## Turning agreed words into a due time

`voice_engine.due_at` resolves the promised phrase in the line's own timezone. *In 10 minutes*, *tomorrow 11am*, *Friday 3pm* and *today evening* all resolve; anything that resolves to nothing usable becomes 11:00 tomorrow rather than now or never. `follow_up_channel` then decides who owns it: a call-back goes on `phone`, and work only a person can do — *send the quotation*, *review the recording* — goes on the `task` channel, which no drain ever sends. *No follow-up needed* schedules nothing at all.

Every follow-up carries a reason, because a dated commitment with no reason is not useful to whoever inherits it. A second follow-up with the same *when* for the same person is treated as a duplicate rather than a second promise.

## What the queue does with it

| Condition | What happens |
|---|---|
| Due, line ready | `runner.chase_phone` places it — two per tick, through the same gates as any outbound call |
| Held by autonomy | Recorded as *awaiting approval*, and it appears in Needs You |
| Gate refuses | Recorded as `refused:` with the reason — budget, consent, a block, a suppression |
| Line not ready | Retried in an hour rather than burned |
| More than 24 hours late | Closed as `missed:` and never rung — a very late call-back is worse than none |

## Measuring what is left, and what the rule cannot do

The rule prevents the false record; it does not stop the sentence being spoken. `promised_unbooked` is the review finding for exactly that residue — a promise on the wire with nothing behind it — so the gap between what was said and what exists is counted rather than assumed to be zero.

- The overlap test is lexical. A caller who agrees in a way that shares no time word is refused, and the read-back is what recovers that case.
- Three languages are covered. Another language on the line falls back to asking rather than to guessing.
- A call with fewer than two caller lines and fewer than five caller words is never sent to the summariser at all: ten one-turn rows once carried an invented afternoon at a sales desk.
- No figure exists here for how often a promise is spoken and refused. That rate is UNKNOWN and would be worth measuring per line.

## Questions

### Why not simply prompt the model to only book times the caller said?

That instruction was already present. A prompt shapes the average case; it does not make an error impossible, and the error here writes a row that a person later relies on. Putting the check in the tool means the worst case is a missing follow-up rather than a fictional one.

### What happens if the caller genuinely named a time and the check refuses it?

The voice asks for a read-back, which is the same thing a person does when a line is unclear. The cost of the false refusal is one extra sentence; the cost of the false acceptance is a customer waiting for a call that was never scheduled.

### Does this apply to follow-ups created outside a call?

The time-word rule is specific to what a live voice claims, because that is where the words and the record can diverge without anybody seeing it. A follow-up created from the follow-ups screen or by the Assistant is a deliberate act with a visible form, and carries the same reason and duplicate rules.

## Related

- [Follow-ups in Connect](https://connectbyjbrh.com/docs/follow-ups/)
- [Why uncertainty is a valid answer from a business agent](https://connectbyjbrh.com/research/uncertainty-is-an-answer/)
- [Why a sales agent should refuse to answer](https://connectbyjbrh.com/research/safe-refusals-in-sales/)
- [Phone and voice in Connect](https://connectbyjbrh.com/docs/phone/)
- [When a call is answered in silence](https://connectbyjbrh.com/research/silent-failure/)

## What this page is based on

- `docs-source/sources/PHONE.md` §10 — follow-up integrity, `when_mismatch` and `due_at`
- `docs-source/sources/GENERAL.md` §7 — the follow-up queue and `chase_phone`
- `docs-source/sources/PHONE.md` §9 — the summariser guard on very short calls
- Connect capability registry (`docs-source/facts.py`) — `MEASURED`
