# Why 'sent' must require provider evidence

`outbound.py` is the single send boundary, shared by the engine and by a person, and it reports a message as **sent** only once the provider has acknowledged it. Everything else is **failed** or **uncertain** — a real third state, displayed as itself rather than resolved by optimism, because re-sending on a maybe is how a customer receives the same reply twice.

- **Status:** Available
- **Audience:** both, developer
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/research/provider-evidence-for-sent/

## Three states

| State | Evidence | What it means for the customer | What happens next |
|---|---|---|---|
| Sent | The provider acknowledged the message | It is with the provider; delivery is a separate question | Recorded against the thread with the acknowledgement |
| Failed | The provider refused it, or a gate did | Nothing was sent | The reason is shown — a refusal, a suppression, a spent allowance |
| Uncertain | None either way — the handoff completed without an answer | Unknown: it may be in flight, it may never have left | Shown as uncertain, resolved by evidence rather than by a retry |

Most systems have two states because the third is uncomfortable. It appears anyway — a connection dropped after the request left and before the response arrived is an ordinary event on any network — and the choice is only whether to represent it or to pick a side and be wrong some of the time.

## Why guessing costs more than admitting

Guessing *failed* and re-sending duplicates the message. The customer gets the same reply twice, which reads as carelessness in a way a delay does not, and on a thread with a quotation or a commitment it is worse than that.

Guessing *sent* loses the message. Nobody follows up, because the record says the work is done, and the failure is discovered when the customer asks why nobody replied — the one moment at which the answer cannot be recovered gracefully.

Keeping the state honest costs one thing: somebody has to look. That is the trade, and it is the right way round, because the looking is bounded and the alternatives are not.

## Resolving an uncertain send

1. Read the provider's own record for that window and compare it with Connect's.
   - Result: This is the fastest audit available and it generalises: on the phone side, a call the carrier logs as completed with no matching row on our side is a defect every time. The same comparison settles a message.
2. Treat a 4xx as an answer.
   - Result: A 4xx is the provider saying no. It is never retried, on any path here, because retrying a refusal produces the same refusal and obscures the original one.
3. Treat a timeout as an unknown, and retry it at most as the path's rule allows.
   - Result: On the call path, a dial the carrier answered with 408, 503 or 504 is dialled once more, and a leg that never joins is written off after a bounded wait rather than waited on indefinitely.
4. If evidence says the message went, mark it and move on; if it says nothing did, send once, deliberately.
   - Result: The decision is recorded with the person who made it, which is what the audit trail is for.

> **Note** Idempotency is what makes any of this safe to automate later. Every normalised provider event on the call path is stored once, with the signature nonce under a partial unique index, so a replayed callback cannot create a second record of one real event.

## What is not evidence of a send

- **A model saying it sent something.** Tool output is a claim about an intention. The record is what the boundary wrote after the provider answered.
- **A draft that exists.** A held draft has not been handed to any provider: there is no message in flight, no read receipt and nothing in a sent folder. It is not a slow reply, it is no reply.
- **A successful write-back.** Marking a message read or labelling it in Gmail is a separate operation, and a write-back failure never blocks the reply — which also means its success says nothing about the reply.
- **Delivery.** Acknowledgement is the provider accepting the message. A later bounce, a spam placement or a silent drop are delivery outcomes, and they do not retroactively make the send a failure.

## Questions

### Why does the screen say uncertain instead of just retrying?

Because a retry on a maybe is a duplicate on the customer's side, and there is no evidence yet that would distinguish the two cases. Showing the state as it is puts the decision in front of a person along with the means to settle it — the provider's own record.

### Does a bounce mean the message was never sent?

No. A bounce happens after the provider accepted the message, so the send was real and the delivery failed. The two are recorded separately because the remedies are different: a bounce points at the address or the receiving server, not at the send.

### Is the send boundary the same for the engine and for a person?

The same one. `outbound.py` is where every outbound crosses, whether the engine wrote it or somebody approved it from the queue, which is why the evidence rule holds uniformly and there is no second path with weaker bookkeeping.

## Related

- [Email in Connect](https://connectbyjbrh.com/docs/email/)
- [Why uncertainty is a valid answer from a business agent](https://connectbyjbrh.com/research/uncertainty-is-an-answer/)
- [How a mail cursor loses messages, and how to stop it](https://connectbyjbrh.com/research/cursor-loss/)
- [Idempotency for retried telephony webhooks](https://connectbyjbrh.com/research/idempotent-telephony-webhooks/)
- [Why 'connected' is not enough to prove mailbox health](https://connectbyjbrh.com/research/mailbox-health-beyond-connected/)
- [Troubleshooting](https://connectbyjbrh.com/docs/troubleshooting/)

## What this page is based on

- `docs-source/sources/CHANNELS.md` §1 — the send boundary, the uncertain state and the write-back rule
- `docs-source/sources/PHONE.md` §2 and §9 — comparing the carrier's log with ours, and the retry rules
- Connect capability registry (docs-source/facts.py) — `send_evidence`, `gmail_writeback`
