Hi Kevin,
Thanks a lot for jumping on this — really appreciate it.
I tested the pattern you suggested in a fresh, isolated repro: read isConfiguredForCarrierMessaging first, then delay session creation via a Task.sleep before touching smsService.incomingMessageNotifications. I varied the delay across four values to be thorough.
Code shape (full source available, Swift, ~50 lines):
let session = TelephonyMessagingSession.shared
let configured = session.isConfiguredForCarrierMessaging // PHASE 1
TMKLogger.shared.log("isConfiguredForCarrierMessaging=\(configured)")
Task.detached(priority: .userInitiated) {
try? await Task.sleep(nanoseconds: kDelayMs * 1_000_000) // PHASE 2
let notifications = try session.smsService.incomingMessageNotifications // PHASE 3
for await notification in notifications {
TMKLogger.shared.log("✅ SMS received: \(notification.message.content.body)")
}
}
Test protocol on each delay:
Settings → Apps → Default Messaging → TMKTest
Force-kill the app
Send two SMS in rapid succession from another phone
Wait 30 s without touching the phone
Open TMKTest, read the persistent log
iPhone 14 Plus, iOS 26.4.2, SFR France (IMS-routed SIP).
Empirical results
Delay
1st SMS
2nd SMS
Phase 3 ready at
200 ms
❌ lost
✅ received
T+0.2 s
500 ms
❌ lost
✅ received
T+0.5 s
1000 ms
❌ lost
✅ received
T+1.0 s
2000 ms
❌ lost
✅ received
T+2.0 s
The 1st SMS is dropped identically across all four runs. The 2nd SMS arrives normally. Pattern is rigorously identical regardless of delay length.
A relevant detail
In every test, isConfiguredForCarrierMessaging returns true synchronously before the delay starts (1–19 ms after didFinishLaunchingWithOptions, never failing). If reading that flag is what arms the pending-queue infrastructure you mentioned, that infrastructure should already be in place by the time the delay runs and Phase 3 fires. Yet the message is still flushed to a connection that doesn't have its handlers registered yet — the native CommCenter logs continue to show TMKXPCError Code=2 and Received unhandled request regardless of how long we wait before touching the service.
This suggests the race isn't between "infrastructure armed" and "service touched", but rather inside the XPC handshake itself: messageHandlersByID on the client XPCConnection is populated after Pinging remote end returns, and CommCenter starts flushing pending messages on Received new connection from PID — that ~2–7 ms gap is what we keep losing the 1st message in. Delaying anything before smsService access doesn't move the XPC handshake — it just delays it later.
Question
In the runtime introspection I did earlier on the framework binary, I found a TelephonyMessagingKit.Messaging.Server type (mangled _$s21TelephonyMessagingKit0B0O6ServerC) with what appears to be a synchronous callback method setIncomingMessageHandler<A: Message>(@Sendable (XPCPeerMessage<A>) throws -> ()). A synchronous handler installed at session creation would sidestep the for await race entirely.
Is that an internal-only API for now, or is there any chance it's being considered for @_spi exposure to carrier-messaging-entitled apps in a future iOS 26.x point release? If not, would you have any other client-side angle to try, or is the right path here to wait for a server-side fix in CommCenter (option 1 in my original post: defer flushing until the client confirms handler registration)?
Thanks again for taking the time on this — happy to share the full TMKTest_Workaround repro project (xcodegen + 4 Swift files) if useful.
Best,
Topic:
App & System Services
SubTopic:
General
Tags: