- Shipped
- September 7, 2026 at 4:15 AM UTC
- Author
- Kamo
- Commit
- bf2edd9
The previous commit normalised the sender number where it is stored in the number inventory and on the carrier, and the send still failed with the same carrier error. It was a fourth copy: VOIP_CONVERSATIONS carries its own FROM_PHONE_NUMBER, written when the thread is opened, and /send never consults the resolver at all -- handleOutbound reads that stored copy and hands it straight to the carrier. The live thread had been opened while the inventory still held `9492989960`, so it kept that shape for the life of the thread. upsertConversation's javadoc has claimed "the write is normalised" since it was written, and it was -- for externalPhoneNumber. The sender beside it was stored exactly as handed in. Both go through storableNumber now. Fixing the fourth write is still fixing one write. The sender is stored in four tables, written from a carrier's config, from number discovery, from a member's assignment, and onto the conversation row. So SmsGateway.send -- the one thing every outbound text passes through -- now normalises the sender and the destination itself and trusts no stored copy. E164.normalize refuses rather than guessing, so an unplaceable number keeps its raw text instead of being turned into a different, real, dialable one. Second defect, and the reason this took two rounds to find: every carrier refusal was reported to the member as "The message could not be sent. Try again in a moment." Retrying was hopeless by construction -- the carrier was refusing the sender number, and would refuse it every time. SmsFailureCode.REJECTED already existed with exactly the right meaning; nothing carried the signal to it, because SendTextResult could not tell a refusal from a timeout. It carries the carrier's code and verdict now, taken from the status class rather than a table of per-carrier codes: 4xx is the carrier declining a request it understood, 5xx and a timeout are what a retry is actually for. A refusal is now non-retryable and says so, in words a member can act on; the carrier's own sentence stays in the operator log where it was already going. Live conversation rows were repaired directly, so texting works before this deploys. No non-E.164 sender remains in any of the four tables.