DEV Community

Cover image for A Builder in CDMX: Flu Edition

A Builder in CDMX: Flu Edition

Earl Grey on November 08, 2026

I built my sick wife a digital bell. She pressed the one button I left blank. TL;DR: I caught the flu in Oaxaca and I still have it. By Mexico Ci...
Collapse
 
sinarezaei profile image
Sina Rezaei •

One small edge case worth considering: SMS encoding. Those emoji buttons are cute, but a single emoji can switch the entire message from GSM-7 to UCS-2, dropping the single-segment limit from 160 to 70 characters. It doesn't matter much for these short requests, but it's something to watch if you ever add dynamic messages or longer custom requests. Unicode has a hidden cost here.

Collapse
 
dafili profile image
Renhua Wei •

The quieter cost behind the emoji: GSM 03.38 has no á, í, ó or ú (è é ù ì ò and the tilde set — ñ, ¿, ¡ — are all in the table), so "Necesito más ayuda" is UCS-2 at 70 while "Necesito mas ayuda" still fits 160; the accent, not the emoji, flips a Spanish body. Same budget, second charge: { } [ ] ~ ^ \ | € are escape pairs at 2 septets each, so a pasted snippet caps at 80 characters with no emoji in sight. The check is one assertion — keep the 128-char table plus those 9 escapes in a test and assert set(body) <= GSM7 | EXT for every prewritten string; "Otra cosa" is 9 septets and safe, "días" is not.

Collapse
 
earlgreyhot1701d profile image
Earl Grey •

This is exactly why I love posting builds. I’ll be honest: this was above my head at first. For me, the app either worked or it didn’t, and I trusted what I was told. I had no idea that one accent, “más” instead of “mas,” could quietly cut a text from 160 characters to 70. Your comment sent me digging into what the app is doing under the hood, and that’s the part I’ll carry into the next build. Your test idea is in the v2 notes now. Gracias. 🍵

Thread Thread
 
dafili profile image
Renhua Wei •

Glad it's in the notes, and one sharpening: the 54-character bodies were never the risk. UCS-2 only costs you at the boundary, so a 54-char body fits one part either way. The accent bites when a body crosses 70, and then again on concatenation, where the UDH eats a header: GSM-7 drops 160 to 153 septets per part, UCS-2 drops 70 to 67 UTF-16 units. That is the number that surprises people at 300 characters.

So the assertion is not "is it GSM-7", it is "does every canned body fit one part", run against the real table instead of a guess at it:

GSM7 = "@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà"
ESC  = "\f^{}\\[~]|€"

def parts(s):
    if any(c not in GSM7 and c not in ESC for c in s):
        n, one, multi = len(s.encode("utf-16-be")) // 2, 70, 67
    else:
        n, one, multi = len(s) + sum(s.count(c) for c in ESC), 160, 153
    return 1 if n <= one else -(-n // multi)

assert all(parts(b) == 1 for b in canned)
Enter fullscreen mode Exit fullscreen mode

With that in the send path you don't have to cap the v2 free-text field to keep it single-part. Count the parts before sending and label it, "Necesito... (1 of 2)" is a friendlier failure than a silent split.

Thread Thread
 
earlgreyhot1701d profile image
Earl Grey •

Thank you for coming back to sharpen this! I had to break this one down piece by piece, and here’s what I took away: my short texts were never the problem. The real question is whether a message stays in one piece, because long texts get split, and each piece loses a few characters to a hidden label. I love the “(1 of 2)” idea too. A label beats a silent split. It’s all in the v2 notes now: github.com/earlgreyhot1701D/necesi.... Gracias for teaching in the comments. 🍵

Thread Thread
 
dafili profile image
Renhua Wei •

Right, the split is the whole story, and the wire hands you an exact number instead of a guess. One concatenation header eats 6 octets out of the 140-octet TPDU, so every part except the last carries floor((160*7 - 48)/7) = 153 septets, and the tail gets the full 160. That is why 306 characters still ships as two parts and 307 does not.

One more line for the v2 notes: the escape set. { } [ ] ~ ^ \ | and the euro sign cost two septets each, so a single literal ~ sitting inside a 154-septet body pushes it to 155 and silently bills a third part. Check it by counting parts, not characters — run the longest real template through the encoder at length minus one, at length, and at length plus one, and see whether the part count steps at the same place the character count does. When they disagree by exactly one part, it is an escape, not a length problem.

Collapse
 
onelifevibe profile image
OneLifeVibe •

The blank-button detail is the part that sticks: she skipped every labelled request and tapped "Otra cosa" first, which says a lot about how an open field beats a menu when someone is unwell. The v2 fix is neat too — prefilling "Necesito..." so the cursor lands in a sentence instead of a void, rather than adding more buttons. And the fragment trick for the phone number is a good trade-off to name honestly: keeping the number out of the repo and the commit history, while accepting that anyone holding the link can see it. Hope you both feel better soon.

Collapse
 
earlgreyhot1701d profile image
Earl Grey •

Thank you so much! That blank button surprised me too, and it taught me more than the other six combined. Starting the sentence for her felt like the smallest fix that would make the biggest difference. And thanks for calling out the phone number trade-off, it was the part I most wanted to be honest about. We’re both on the mend, slowly. 🍵

Collapse
 
devkursat profile image
Kürşat Yılmaz •

The obsession with wrapping remote APIs into multi-layered agent abstractions is creating massive hidden latency and failure cascades in production.

If you profile real-world workloads, bare-metal local inference (via direct C++ / llama.cpp or vLLM bindings) consistently beats fragile cloud wrappers on both predictability and cost. Tooling should eliminate abstraction penalties, not compound them.

Collapse
 
earlgreyhot1701d profile image
Earl Grey •

Fair point for AI-heavy apps! This one’s the opposite end: no AI at runtime at all, just one HTML page and a text link. The only “inference” is E deciding between tea and soup. 🍵