Skip to content

Delivery receipts: tell "she ignored it" apart from "it never arrived" - #1

Merged
kapoordeepanshu merged 1 commit into
mainfrom
feat/delivery-receipts
Sep 4, 2026
Merged

Delivery receipts: tell "she ignored it" apart from "it never arrived"#1
kapoordeepanshu merged 1 commit into
mainfrom
feat/delivery-receipts

Conversation

@kapoordeepanshu

Copy link
Copy Markdown
Owner

Prompted by a cold email that read the repo properly rather than the README.

The finding

message_log.status had four states and none of them was delivered:

status TEXT NOT NULL DEFAULT 'claimed' CHECK (status IN ('claimed','sent','failed','blocked'))

Two lines below sat provider_message_id — the exact handle a delivery receipt
is matched against — and nothing ever came back to match it. sent_at meant a
provider accepted the payload. settled_at meant the dispatcher finished with
the row. v_stuck_sends found sends we abandoned. None of them answered
whether the T-24h reminder reached the handset.

That matters more here than in most repos. A no-show engine exists to separate
"she read it and didn't reply" from "it never arrived", and in the log
both were status = 'sent' with no row in inbound_messages. T-3h with
onlyIfUnconfirmed fired identically for both. So did the digest's unconfirmed
list.

Two things the email couldn't see from outside made it worse:

  • Workflow 03 already set StatusCallback to /webhook/deskbell/message-status
    on every SMS, and no workflow served that path — Twilio had been posting
    every receipt at a 404, and retrying it.
  • provider_message_id was written by exactly one node. Workflows 07 and 08
    settled to sent without it, so follow-ups, recalls and waitlist offers were
    unreconcilable even in principle. Workflow 05 created no message_log row at
    all.

What changed

Recording it

  • message_log.status carries claimedsentdelivered/read, plus
    undelivered and failed, with delivered_at, provider_status and
    provider_status_at alongside. data/schema.sql is idempotent and
    re-applying it is the upgrade.
  • New workflow 11 receives Twilio receipts on the URL 03 was already
    advertising. WhatsApp Cloud allows one webhook URL per app, so 04 stops
    dropping its status payloads and forwards them here instead.
  • Receipts arrive out of order and more than once — Twilio's sent and
    delivered webhooks routinely land the wrong way round. deskbell.delivery_rank()
    makes every status write monotonic, in workflow 11 and in all four settles, so
    a late sent can't undo a delivered and a duplicate updates nothing.
  • 05, 07 and 08 now record provider_message_id; the missed-call text-back
    claims a message_log row before it sends, like every other send does.

Acting on it, because a receipt nothing reads is decoration

  • A stage whose last attempt did not reach the customer is retried on a
    different channel
    . This also fixes a silent dead end that predates receipts:
    a refused send left sent_stages untouched and looked due forever, while the
    claim it already held dropped every retry via ON CONFLICT DO NOTHING. The
    retry claims appointment|stage|r<n>, capped by
    reliability.maxRedeliveryAttempts (default 1).
  • The daily digest splits unconfirmed into reached, no reply and never
    reached
    . The second is a wrong number in the booking system, not a reluctant
    customer.
  • deskbell.v_undelivered is the resulting fault list.
  • A stage whose claim is held by an unsettled attempt reports
    attempt_in_flight rather than due — a decision the database silently
    overrules is a log that lies.

What it deliberately does not claim. Channels differ in what they report:
SMS gives delivered/undelivered, WhatsApp adds read, email reports nothing. A
message on a channel with no receipts stays sent, and sent means unknown
everywhere it's read — not delivered, not failed.

Also

.env.example now documents the four sender identities. Twilio, WhatsApp, VAPI
and email are configured separately, so out of the box the reminder, the SMS
fallback and the voice escalation can each arrive from a different number — and
only one of them has replies routed anywhere, which makes the others a dead end
that still costs money.

Verification

12 workflows, 162 nodes, 35 Code nodes validated — 0 error(s), 0 warning(s).
12 tables, 4 views, 1 functions in schema; 37 SQL nodes checked — 0 error(s).
# tests 78 · pass 78 · fail 0

Docker wasn't running locally, so schema.sql has not been applied to a real
Postgres here — CI does that twice on every commit and is the check to watch on
this PR.

message_log.status had four states and none of them was delivered. Two lines
below it sat provider_message_id — the exact handle a receipt is matched
against — and nothing ever came back to match. sent_at meant a provider
accepted the payload; settled_at meant the dispatcher finished; neither meant
the handset showed anything.

That matters more here than in most systems. A no-show engine exists to
separate "she read it and didn't reply" from "it never arrived", and in the log
both were status = 'sent' with no inbound row. T-3h with onlyIfUnconfirmed
fired identically for both. So did the digest's unconfirmed list.

Worse, workflow 03 already set StatusCallback to /webhook/deskbell/message-status
on every SMS and no workflow served that path, so Twilio had been posting every
receipt at a 404.

  * message_log.status now carries claimed -> sent -> delivered/read, with
    undelivered and failed, plus delivered_at, provider_status and
    provider_status_at. Applying schema.sql again is the upgrade path.
  * New workflow 11 receives Twilio receipts on the URL 03 was already
    advertising. WhatsApp Cloud allows one webhook per app, so 04 stops
    dropping its status payloads and forwards them instead.
  * Receipts arrive out of order and more than once. deskbell.delivery_rank()
    makes every status write monotonic — in 11 and in all four settles — so a
    late 'sent' cannot undo a 'delivered' and a duplicate updates nothing.
  * 05, 07 and 08 record provider_message_id. Follow-ups, recalls and waitlist
    offers were unreconcilable even in principle; the missed-call text-back had
    no message_log row at all, so it now claims one before it sends.

Acting on it, since a receipt nothing reads is decoration:

  * A stage whose last attempt did not reach the customer is retried on a
    different channel. This also fixes a silent dead end that predates
    receipts: a refused send left sent_stages untouched and looked due forever,
    while the claim it already held dropped every retry via ON CONFLICT DO
    NOTHING. The retry claims appointment|stage|r<n> instead, capped by
    reliability.maxRedeliveryAttempts.
  * The daily digest splits "unconfirmed" into reached-no-reply and
    never-reached. The second is a wrong number, not a reluctant customer.
  * deskbell.v_undelivered is the fault list. It answers a different question
    from v_stuck_sends, which finds sends we abandoned rather than sends a
    provider refused.
  * A stage whose claim is held by an unsettled attempt reports
    attempt_in_flight rather than due, because a decision the database
    silently overrules is a log that lies.

A channel with no receipts still reports nothing, and 'sent' means unknown
everywhere it is read — not delivered, and not failed.

Also documents the four sender identities in .env.example: Twilio, WhatsApp,
VAPI and email are configured separately and will otherwise chase one person
about one appointment from three different numbers, only one of which has
replies routed anywhere.

78 tests, up from 63.
@kapoordeepanshu
kapoordeepanshu merged commit 719e466 into main Sep 4, 2026
2 checks passed
@kapoordeepanshu
kapoordeepanshu deleted the feat/delivery-receipts branch September 4, 2026 10:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant