Carry an arriving text across the boundary the dock sits above

Otherkamo-internal
Shipped
September 8, 2026 at 12:23 AM UTC
Author
Kamo
Commit
290dd54

An inbound SMS never reached an open SMS window, and the reason was not the transport everyone went looking at. SmsMessageProvider was mounted in AuthenticatedToolWrapper, which is inside ToolProviders' {children}. Every SMS window mounts under <ToolDock />, which ToolProviders renders as that block's SIBLING — deliberately, because it is what keeps a tool window's state alive across a navigation. So SmsThread's useSmsMessages() resolved to the context DEFAULT: registerHandler was `() => {}` and the app-wide listener's dispatch searched a map that could never contain anything. Nothing said so, and the one visible effect actively misled: the window still popped open, because that travels as a `tool:open` CustomEvent on window and a CustomEvent has no React tree. A window opening and then showing nothing new reads as a broken socket, which is exactly where this sent every investigation. This is the third crossing of that boundary — smsUnreadStore and toolWindowUnreadStore are the other two — so it is built the same way: a module-level store, which has no tree either. It also replays the last few seconds to a window that is still mounting. An inbound text is what RAISES the window it belongs in, so the message always precedes the thread's first effect; without that buffer a window opened by an arrival opened without the arrival in it, every time, for the one message the member was being shown the window for. Two more things in the same path: The window was titled with the member's OWN number. The frame's fromPhoneNumber carried the conversation's from-number, which is the org's line; the far side is externalPhoneNumber. So an inbound text opened a window headed with the member's own number, and the contact lookup beside it searched for that and found nobody — every text from a known customer showed as an unknown one. Now reads the explicit field, falling back to the old one for a VOIPService pod that has not rolled yet. And the TEXT conversation list rode nothing at all. The chat buckets are kept live by ACTIVITY frames on the unread socket, but a text conversation is not a MediaSession and comes from VOIPService's own endpoint, so the Texts tab was a snapshot taken on mount: an arriving message moved no row, changed no preview and added no conversation until the member switched tabs and back. mergeSmsIntoRows now folds arrivals in — synthesizing the row for a first text from a new number, keeping what only the server knows, and refusing to walk a row backwards on an out-of-order frame. The tests render the consumer with NO provider ancestor, which is where a tool window actually lives. A test that wraps both halves in a provider passes whether or not the bridge works, and that is why this class of bug keeps shipping here.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing