A newly bound number now hears from Mira
When counsel completes the enrollment ceremony — they send their one-time code on WhatsApp and their number binds to their PRAMAAN account — the channel now answers with a welcome. Before, the ceremony minted the binding and then either went silent or (after Bug-2987) sent a terse masked one-liner. Silence after a security ceremony reads as failure; the welcome is the first thing that tells counsel it worked and shows them how to start.
post_bind_ping row and dispatch_recorded_whatsapp_text.
This PR replaces that masked path with the welcome, and
deletes the old mechanism in the same change (R1) — the queued-row
INSERT, the ping_delivery_id field, the recorded-dispatch
helper, and build_post_bind_confirmation all go.
The message (founder-locked copy, EPIC-3034 D1)
*Mira here. Welcome to PRAMAAN* 👋
You are connected: {number} → {firm}
I work on one matter at a time.
Reply *matters* to see yours and pick one. Then just ask — in plain English.
To move to another, say *switch to {first_matter_code}*.
/help · /matters · /disconnect
{number}is the counsel’s real bound number, unmasked (D2). Bindings are visible only to their owner and their own firm; masking protects nobody and stops counsel recognising their own number. The rawprovider_subjectis already in hand at bind time.{firm}is the firm name and{first_matter_code}is the counsel’s own first accessible matter code — never a placeholder.- The
*…*render bold on WhatsApp and are deliberate.
Which case gets which message (founder ruling, D-rebind)
| Case | Signal | Message |
|---|---|---|
| First bind | new binding row | full welcome |
| Reconnect after disconnecting | rebound=False (a new row — disconnect revoked the old one) |
full welcome |
| Re-enrolled while still connected | rebound=True |
Still connected — nothing changed. |
The reconnect case deliberately gets the full welcome, not a
shortened “welcome back”: the thing most likely to have changed
while someone was disconnected is their matter list, which is exactly what
the welcome shows. rebound was already computed by the
ceremony — no new query.
The zero-matter empty state
A member with zero accessible matters has no
first_matter_code to offer, so the full welcome would print an
empty switch to example. Instead they get the same greeting and
connection line, then the existing disambiguation constant
NO_ACCESS_MATTER_TEXT (“You don’t have any active
matters I can access here. Check your matter access in the app.”), and
the command footer — no switch to line, no crash:
*Mira here. Welcome to PRAMAAN* 👋
You are connected: +91… → Fictional Chambers
You don't have any active matters I can access here. Check your matter access in the app.
/help · /matters · /disconnect
There is exactly one spelling of the matter read (Finding 3): the welcome
reuses disambiguation.load_post_bind_welcome_context, a thin
public wrapper over the same RLS-bounded
read_channel_binding_contexts read the disambiguation menus
use. _load_firm_contexts stays private — nothing imports
it across modules. Because that read LEFT JOINs matters, a
matterless binding still returns its firm row, so the firm name always
resolves.
Sent after the bind commits, best-effort (Finding 2)
The ceremony (consume_enrollment_code) runs inside the ingress
consumer’s transaction and now mints the binding only. The
welcome is dispatched after that transaction commits, from
a fresh session re-scoped to the binding’s firm — the exact
command_broker._dispatch_refusal precedent. This guarantees a
message is never delivered for a bind that then rolls back. It is
best-effort: a failed send stamps its own honest terminal
failed delivery record (R5) and never unwinds the binding, and
one welcome failure never aborts the others or the webhook’s 200.
_intercept_enrollment -> consume + mark inbound processed (in the ingress txn)
returns the EnrollmentResult, sends nothing
[ ingress transaction commits ]
dispatch_post_bind_welcome -> fresh session, scope to firm, resolve firm+matter,
pick body, dispatch_whatsapp_text (post_bind_ping)
Static, one message, zero model
The welcome and the re-enrol ack are static constants beside
STATIC_ACK_TEXT in src/shared/whatsapp/formatter.py
— zero LLM, zero agent run, zero per-bind variance beyond the three
interpolated values. It fits in one WhatsApp message: at the deployed
4096-char cap the body is ~280 characters, so
chunk_whatsapp_text returns a single chunk (unit-pinned). The
shared formatter takes the empty-state sentence as an argument so it never
imports a domain module.
What is proven, and what is not
- Unit: the four founder cases (first bind, reconnect,
re-enrol ack, zero-matter empty state) via the pure
select_post_bind_body; the welcome fits one message; the number is unmasked; noswitch toexample when there are no matters. - Integration (CI DB gate): the ceremony records no
delivery row; the signed ingress produces one
post_bind_ping→sentcarrying the unmasked number and firm. - Deploy canary (R4): the enrollment smoke now asserts the welcome text and the unmasked number actually interpolated — a reverted or masked body fails the deploy, not the first real bind.
- Not yet proven — live UAT is outstanding. The
final proof is a real handset receiving the welcome as a single message
after a real bind: the bold renders, the emoji renders, the unmasked
number and firm are correct, and the
switch to {code}example names one of that counsel’s own matters. That is a manual post-merge step and is not claimed done here.