Repository navigation
Description
SMS/MMS state does not synchronize reliably between Android and web clients
Describe the bug
SMS/MMS state can diverge between the Android provider and PlainApp's browser/desktop UI. The visible failures are not one isolated rendering defect: the current implementation has no provider-level SMS/MMS change signal, and several independent request, optimistic-state, identity, and MMS parsing defects can overwrite or conceal fresh data.
Observed symptoms include:
- A message sent from the web UI appears optimistically, then disappears almost immediately.
- An incoming-message notification arrives in the web UI, but the message does not appear until a full page reload.
- A message sent from the web UI does not appear on the phone.
- A thread created on the phone does not appear in the browser/desktop conversation list.
- Group conversations can be labeled as the current user and display the current user's own phone number.
- Incoming MMS attachments can appear as
-or not appear at all.
The browser UI and Tauri desktop application share the plain-desktop Vue frontend. The Android app in plain-app hosts the GraphQL/WebSocket APIs and packages a built copy of that frontend, so a complete correction spans both repositories.
Steps to reproduce
Incoming SMS
- Open Messages in the PlainApp browser or desktop client.
- Keep an existing thread open.
- Receive a new SMS on the connected phone.
- Observe that a notification can arrive while the thread contents, sidebar snippet, timestamp, ordering, and unread/count state remain stale.
- Reload the page and observe the missing message appear.
Phone-created conversation
- Keep the PlainApp Messages page open.
- Create and send a new SMS thread from the phone's native messaging application.
- Wait or switch between existing threads.
- Observe that the new thread does not appear until a page reload.
Web-sent SMS
- Send an SMS from an open PlainApp thread.
- Observe that the optimistic bubble can disappear after an unchanged provider query.
- If Android later reports a send failure, observe that the web client still clears the draft and provides no durable failure state.
Group/MMS identity
- Open a multi-party MMS/SMS conversation.
- Observe that only one canonical recipient is returned and used as the conversation identity.
- If the first address is the active SIM's number, observe that the thread is labeled as the current user.
- Receive an MMS containing an attachment and observe that downloaded MMS content may be absent.
Expected behavior
- SMS/MMS provider changes should update the active thread, conversation list, ordering, snippets, counts, contact display, and read state without a reload.
- A WebSocket reconnect should reconcile potentially missed provider changes.
- An optimistic outgoing message should remain visible until it is matched to a provider row or reaches an explicit failure state.
- Android send failures should be reported to the web client; drafts should not be silently lost.
- Group threads should preserve all participants and exclude the user's own number from the display identity when possible.
- Sent MMS and downloaded incoming MMS should both be queryable and render their attachments.
- Concurrent queries, pagination, filters, and delayed refreshes should not allow an older response to overwrite newer state.
Investigation findings
1. There is no provider-driven SMS/MMS synchronization event
This is the primary cross-client synchronization gap.
plain-appdoes not register aContentObserverfor SMS, MMS, MMS parts/addresses, or conversations. Its existing real-time path is based on Android notification-listener events.- Notification events are not an authoritative change feed: phone-originated sends normally create no qualifying notification, group summaries/ongoing/local notifications are filtered, packages can be absent from the allowlist, and notification delivery can precede the provider commit.
WebSocketEventshas notification events andMMS_SENT, but no event representing an external SMS/MMS provider change.plain-desktoptherefore cannot refresh for phone-originated sends/new threads, and its SMS views do not reconcile on WebSocket reconnect.
Relevant code:
plain-app/shared/src/commonMain/kotlin/com/ismartcoding/plain/events/WebSocketEvents.ktplain-app/app/src/main/java/com/ismartcoding/plain/services/PNotificationListenerService.ktplain-desktop/src/hooks/app-socket.tsplain-desktop/src/hooks/sms-notification-refresh.ts
2. Optimistic outgoing messages are cleared by unrelated or stale queries
plain-desktop/src/hooks/message-thread.tsstores only one module-level pending SMS.- Any successful full fetch clears that pending item, even when the provider returns the same list and has not persisted the send.
- Retry logic compares list length rather than message identity, uses uncancelled timers, and is not scoped to the thread that initiated the send.
- Rapid sends overwrite the singleton pending slot.
- Switching threads while a retry is pending can make the old timer query the newly selected thread.
This directly explains a sent bubble appearing and then disappearing.
3. Android reports send success before receiving the platform result
SmsHelper.sendText()passes no sent/deliveryPendingIntentcallbacks.- The GraphQL mutation returns
trueimmediately after invokingSmsManager. - Radio, service, carrier, rate-limit, and SIM-selection failures are therefore hidden from the web client.
- The composer clears the draft without awaiting the mutation and has no failure restoration path.
Relevant code:
plain-app/shared/src/androidMain/kotlin/com/ismartcoding/plain/features/sms/SmsHelper.ktplain-app/shared/src/commonMain/kotlin/com/ismartcoding/plain/features/sms/SmsGraphQL.ktplain-desktop/src/hooks/message-send.ts
4. In-flight request deduplication and mutable response state can restore stale data
plain-desktop/src/lib/api/gql-client.tsdeduplicates identical in-flight queries. A refresh triggered after a provider notification can join a request that began before the provider commit, so no post-change request occurs.initLazyQueryhas no request generation/cancellation guard; an older request can complete last and overwrite a newer result.- Thread pagination decides append/replace using mutable global state at response time rather than immutable request variables.
- The SMS sidebar cache likewise applies responses using the current page/filter rather than the page/filter that produced the response.
These races explain cases where waiting or switching threads still does not reveal a received message.
5. Send and refresh paths update different parts of the UI
- The in-thread composer refreshes the open thread but does not emit the same event as the new-message modal.
- The sidebar's local
sms_sentlistener therefore misses normal composer sends. - Counts, sidebar ordering, and snippets are not consistently refreshed with the active thread.
- New-thread creation relies on one fixed delayed refresh instead of provider confirmation.
6. Group participants are truncated to one address
- Both conversation queries split Android's
recipient_idsand take only the first canonical address. - The domain and GraphQL models expose only singular
addressdata. - MMS address parsing likewise returns only one candidate.
- The frontend derives conversation display and sometimes send targets from that singular address or the first loaded message.
This makes group identity arbitrary and can select the active SIM's own number. It also risks targeting the wrong participant.
Relevant code:
plain-app/shared/src/androidMain/kotlin/com/ismartcoding/plain/features/sms/SmsConversationHelper.ktplain-app/shared/src/androidMain/kotlin/com/ismartcoding/plain/features/sms/SmsHelper.ktplain-app/shared/src/commonMain/kotlin/com/ismartcoding/plain/features/sms/DMessageConversation.ktplain-desktop/src/lib/interfaces.tsplain-desktop/src/views/messages/MessagesSidebar2.vue
7. Incoming MMS content uses the wrong PDU type filter
The backend uses m_type IN (128, 130) and documents 130 as retrieve-conf. Android/AOSP defines:
128:SEND_REQ130:NOTIFICATION_IND132:RETRIEVE_CONF
The current predicate includes the notification placeholder and excludes downloaded incoming MMS content. This affects message queries, conversation queries, and counts, and directly explains missing MMS bodies/attachments.
The predicate should be centralized using named constants and include (128, 132). Reference: AOSP PduHeaders.
8. MMS completion polling is not correlated to the send
- The Android poller treats any sent MMS row newer than a loose timestamp cutoff as completion.
- Concurrent pending sends can all be completed by one unrelated provider row.
- Timeout emits no terminal failure.
MMS_SENTis mapped by the frontend but is not consumed by the SMS views.
The poller should snapshot the provider ID before launch, match a new row to the operation, and emit exactly one success/failure/cancel/timeout result.
9. Contact resolution is lossy and never invalidated
- Phone numbers are reduced to a ten-digit suffix, with the last contact silently winning collisions.
- International numbers, short codes, ambiguous suffixes, and participant lists are not represented safely.
- The module-level contact map is loaded once, so edits on the phone remain stale until reload.
Resolution should prefer an exact normalized number, use a suffix only when unique, support participant arrays, and invalidate on relevant provider/reconnect events.
10. Secondary list/cache consistency failures
- Failed sidebar load-more increments the page permanently, so retry skips a page.
- Offset pagination appends without ID deduplication and can duplicate/skip conversations when new rows arrive.
- Archive/unarchive is optimistic without rollback and invalidates only the current cache key.
- Refresh and delayed-send timers outlive view deactivation.
- Parsed text/ID filters exclude MMS; SMS count uses raw query-string checks and can disagree with the returned list.
- Filtered conversation IDs are paginated before the combined SMS/MMS results are chronologically sorted.
- Archived snippets inspect SMS only, so an MMS-last thread can show an older snippet.
- SMS ID collection omits MMS, breaking some bulk/tag operations.
Existing issue review
plainhub/plain-desktop#14covered the narrower incoming-notification/sidebar refresh defect and was fixed byplainhub/plain-desktop#15. It did not add an authoritative Android provider change feed or resolve outgoing, reconnect, group identity, MMS, and concurrency failures.plainhub/plain-app#274requests broader MMS/group-text support. It overlaps group presentation and attachments, but it does not describe the provider synchronization, optimistic-message disappearance, stale response, or send-acknowledgement defects here.- No open issue was found that covers this full failure set.
Proposed implementation
plain-app
- Add a lifecycle-owned, descendant Android content observer for SMS/MMS provider URIs and debounce provider write bursts.
- Add a new, additive WebSocket event for SMS/MMS provider changes without renumbering existing events.
- Await correlated Android SMS sent callbacks and surface failure/timeout while preserving GraphQL compatibility where possible.
- Correct and centralize MMS PDU predicates.
- Preserve all group participants through domain/GraphQL models while retaining singular fields for backward compatibility.
- Correlate MMS completion to its own provider row and emit one terminal status.
- Make SMS/MMS list, count, filter, ordering, and archived-snippet behavior consistent.
plain-desktop
- Consume provider-change events in the active thread, conversation list, counts, and contact identity; reconcile on reconnect.
- Replace the singleton pending message with thread-scoped pending operations that persist until matched or failed.
- Await send results, prevent duplicate sends, validate SIM selection, and preserve/restore drafts on failure.
- Add forced post-invalidation fetches and latest-request generation guards.
- Apply responses using captured request variables; repair pagination retry/deduplication and archive rollback.
- Use conversation participant metadata for identity and make contact matching ambiguity-safe.
- Make composer and new-thread send paths update the same domain state.
Because the backend and shared web client live in separate repositories, this issue will require two linked pull requests rather than one cross-repository PR.
Regression and validation plan
Automated tests
- Provider observer registration, unregistration, burst coalescing, event serialization, and read/provider mutations.
- SMS sent-callback success, multipart success, Android error codes, invalid SIM, and timeout.
- MMS PDU
132inclusion and130exclusion. - Group recipient resolution, deduplication, and self-number exclusion.
- MMS completion with simultaneous sends, unrelated rows, cancellation, and timeout.
- SMS/MMS list/count/filter/order/archive-snippet parity.
- Pending-message retention, reconciliation, multiple sends, failures, and thread switching.
- Forced invalidation fetch and reverse-completion latest-result protection.
- Immutable pagination/filter response handling, failed-page retry, and ID deduplication.
- Provider-event and reconnect refresh of thread/sidebar/count/contact state.
- Composer/modal parity, duplicate-submit prevention, SIM validation, and draft restoration.
- Participant display and ambiguous contact-number matching.
Integration/device validation
- Receive SMS while a thread is open and while it is closed.
- Send/create a thread from the phone and verify it appears without reload.
- Send from browser and verify both provider persistence and the phone messaging app.
- Disconnect/reconnect WebSocket and verify reconciliation.
- Receive and send group MMS, including attachments and multiple participants.
- Exercise single- and multi-SIM selection and an intentional send failure.
- Run the full frontend test/typecheck/lint/build/package suite and the required Android
:app:assembleDebugbuild.
No Android device or emulator was connected during the initial source audit, so the device matrix remains a required validation step rather than claimed reproduction evidence.
Environment
- Client: PlainApp browser UI and the shared
plain-desktopfrontend - Android backend audited:
plainhub/plain-appupstream/mainat78b528156d85a51d2fc064031a4fc00c8df64638 - Web frontend audited:
plainhub/plain-desktopupstream/mainat26e28a89cbc4c57e68add46770c4c7a5c071e27f - Exact phone model/Android version: not yet captured
Activity
th317erd commentedon Aug 28, 2026
@ismartcoding, could you please assign this issue to @th317erd and add the
buglabel? I have completed the initial cross-repository source audit and am starting the linkedplain-appandplain-desktopfixes.th317erd commentedon Aug 28, 2026
SMS/MMS state does not synchronize reliably between Android and web clients
Tracking
Describe the bug
SMS/MMS state can diverge between the Android provider and PlainApp's browser/desktop UI. The visible failures are not one isolated rendering defect: the current implementation has no provider-level SMS/MMS change signal, and several independent request, optimistic-state, identity, and MMS parsing defects can overwrite or conceal fresh data.
Observed symptoms include:
-or not appear at all.The browser UI and Tauri desktop application share the
plain-desktopVue frontend. The Android app inplain-apphosts the GraphQL/WebSocket APIs and packages a built copy of that frontend, so a complete correction spans both repositories.Steps to reproduce
Incoming SMS
Phone-created conversation
Web-sent SMS
Group/MMS identity
Expected behavior
Investigation findings
1. There is no provider-driven SMS/MMS synchronization event
This is the primary cross-client synchronization gap.
plain-appdoes not register aContentObserverfor SMS, MMS, MMS parts/addresses, or conversations. Its existing real-time path is based on Android notification-listener events.WebSocketEventshas notification events andMMS_SENT, but no event representing an external SMS/MMS provider change.plain-desktoptherefore cannot refresh for phone-originated sends/new threads, and its SMS views do not reconcile on WebSocket reconnect.Relevant code:
plain-app/shared/src/commonMain/kotlin/com/ismartcoding/plain/events/WebSocketEvents.ktplain-app/app/src/main/java/com/ismartcoding/plain/services/PNotificationListenerService.ktplain-desktop/src/hooks/app-socket.tsplain-desktop/src/hooks/sms-notification-refresh.ts2. Optimistic outgoing messages are cleared by unrelated or stale queries
plain-desktop/src/hooks/message-thread.tsstores only one module-level pending SMS.This directly explains a sent bubble appearing and then disappearing.
3. Android reports send success before receiving the platform result
SmsHelper.sendText()passes no sent/deliveryPendingIntentcallbacks.trueimmediately after invokingSmsManager.Relevant code:
plain-app/shared/src/androidMain/kotlin/com/ismartcoding/plain/features/sms/SmsHelper.ktplain-app/shared/src/commonMain/kotlin/com/ismartcoding/plain/features/sms/SmsGraphQL.ktplain-desktop/src/hooks/message-send.ts4. In-flight request deduplication and mutable response state can restore stale data
plain-desktop/src/lib/api/gql-client.tsdeduplicates identical in-flight queries. A refresh triggered after a provider notification can join a request that began before the provider commit, so no post-change request occurs.initLazyQueryhas no request generation/cancellation guard; an older request can complete last and overwrite a newer result.These races explain cases where waiting or switching threads still does not reveal a received message.
5. Send and refresh paths update different parts of the UI
sms_sentlistener therefore misses normal composer sends.6. Group participants are truncated to one address
recipient_idsand take only the first canonical address.addressdata.This makes group identity arbitrary and can select the active SIM's own number. It also risks targeting the wrong participant.
Relevant code:
plain-app/shared/src/androidMain/kotlin/com/ismartcoding/plain/features/sms/SmsConversationHelper.ktplain-app/shared/src/androidMain/kotlin/com/ismartcoding/plain/features/sms/SmsHelper.ktplain-app/shared/src/commonMain/kotlin/com/ismartcoding/plain/features/sms/DMessageConversation.ktplain-desktop/src/lib/interfaces.tsplain-desktop/src/views/messages/MessagesSidebar2.vue7. Incoming MMS content uses the wrong PDU type filter
The backend uses
m_type IN (128, 130)and documents130as retrieve-conf. Android/AOSP defines:128:SEND_REQ130:NOTIFICATION_IND132:RETRIEVE_CONFThe current predicate includes the notification placeholder and excludes downloaded incoming MMS content. This affects message queries, conversation queries, and counts, and directly explains missing MMS bodies/attachments.
The predicate should be centralized using named constants and include
(128, 132). Reference: AOSPPduHeaders.8. MMS completion polling is not correlated to the send
MMS_SENTis mapped by the frontend but is not consumed by the SMS views.The poller should snapshot the provider ID before launch, match a new row to the operation, and emit exactly one success/failure/cancel/timeout result.
9. Contact resolution is lossy and never invalidated
Resolution should prefer an exact normalized number, use a suffix only when unique, support participant arrays, and invalidate on relevant provider/reconnect events.
10. Secondary list/cache consistency failures
Existing issue review
plainhub/plain-desktop#14covered the narrower incoming-notification/sidebar refresh defect and was fixed byplainhub/plain-desktop#15. It did not add an authoritative Android provider change feed or resolve outgoing, reconnect, group identity, MMS, and concurrency failures.plainhub/plain-app#274requests broader MMS/group-text support. It overlaps group presentation and attachments, but it does not describe the provider synchronization, optimistic-message disappearance, stale response, or send-acknowledgement defects here.Implemented correction
plain-appContentObserverfor SMS, MMS, MMS parts/addresses, and conversations. Provider write bursts are coalesced and published as a new additive WebSocket provider-change event.PendingIntentidentities. Terminal results are persisted, bounded, timestamped, and replayed after a WebSocket reconnect so a disconnect cannot silently lose success or failure.SEND_REQ(128) andRETRIEVE_CONF(132), excluding the notification placeholder (130). The same predicate now drives lists, counts, searches, conversation summaries, and archived data.plain-desktopBecause the backend and shared web client live in separate repositories, this issue will require two linked pull requests rather than one cross-repository PR.
Regression and validation plan
Automated validation completed
plain-app::shared:testAndroidHostTest --tests com.ismartcoding.plain.features.sms.SmsSyncContractTestpasses. The contract suite covers provider burst coalescing; durable/bounded SMS and MMS result state; replay expiry; multipart and terminal states; PDU filtering; concurrent/overlapping MMS correlation; participant/self resolution; MIME-category matching; cancellation; and predicates containing more than 1,000 IDs.plain-app: the repository-requiredPATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:$PATH ./gradlew :app:assembleDebugpasses for the F-Droid, GitHub, and Google debug variants.plain-desktop: 13 focused unit-test files pass with 119 tests passed and 1 skipped. They cover provider/reconnect refresh, newest-query wins, cache behavior, per-thread pending reconciliation, result ordering, send deadlines, draft restoration, pagination, participant/contact resolution, and the result-before-mutation-response race.plain-desktop:vue-tsc --noEmit, ESLint over every changed/new TypeScript and Vue file, and the Vite production build all pass.plain-desktop: the complete repository run finishes with 479 passed and 52 skipped tests, plus 4 failures in existing unchanged local/window-mode suites (cross-window-store,local-mode, andwindow-client). The cross-window failure passes in isolation; the three local/window assertions are repeatable environment/configuration failures outside the SMS/MMS changes.Real-device validation completed
com.ismartcoding.plain.debug) alongside the existing release 3.3.5 on a Pixel 9 Pro. The release package and its data were left untouched.plain-desktopdevelopment build against the corrected Android backend.Remaining carrier/manual matrix
Automated validation deliberately did not send a real carrier SMS/MMS or create a conversation in the user's live message history. The following external-stimulus checks remain appropriate before release: receive SMS while a thread is open/closed, originate a new phone-side thread, send browser-to-phone SMS/MMS, reconnect during an in-flight result, verify a known multi-party thread's display identity, exercise multi-SIM selection, and force a carrier send failure. The new unit/contract coverage exercises the corresponding state transitions without mutating live data.
Environment
plain-desktopfrontendplainhub/plain-appupstream/mainat78b528156d85a51d2fc064031a4fc00c8df64638plainhub/plain-desktopupstream/mainat26e28a89cbc4c57e68add46770c4c7a5c071e27fcaiman), GrapheneOS on Android 17 / API 37, buildCP2A.260805.005th317erd commentedon Sep 1, 2026
Successful browser SMS remains stuck on “Sending…” on Android 17
Tracking
plain-app4ae019ba(v3.3.16, current upstream);plain-desktopd3b1ff1e(current upstream)Priority and scope
This is a high-priority SMS correctness bug because the browser reports an in-progress send indefinitely even after Android confirms that the carrier send succeeded. It affects the browser UI and the shared Tauri desktop frontend when the Android host applies Android 17's restricted-message behavior to SMS sent by a non-default messaging app.
The correction is intentionally limited to truthful terminal status for a correlated browser send while retaining the optimistic item for later provider reconciliation. It does not bypass Android's restricted-message access control, make PlainApp the default SMS application, or attempt to replace Android's system SMS database.
Reported behavior
After upgrading the Android app to PlainApp 3.3.16, an SMS sent from an open browser conversation remains labeled “Sending…” forever. Waiting and switching threads do not settle the status.
Reproduction
Environment
4ae019baSteps
Actual result
sendSmsmutation returnstruewithout errors.SMS_SEND_RESULTWebSocket event withsuccess=trueand Android's successful result code.Expected result
Once the correlated Android sent callback reports success, the outgoing bubble should display “Sent”. It should remain eligible for replacement by the authoritative provider row if that row later becomes readable.
Investigation findings
The carrier send succeeds
The failure is not a GraphQL timeout, WebSocket disconnect, missing Android permission, SIM selection failure, or carrier failure. The mutation succeeds, Android invokes PlainApp's sent
PendingIntent, the result tracker publishes a correlated successful terminal event, and the system SMS provider contains the sent row.Android 17 hides the persisted row from PlainApp
Safe provider metadata comparison showed that ordinary incoming messages and messages sent by the default SMS application have
restricted=0. Every controlled message sent by PlainApp has:type=2(sent)creator=com.ismartcoding.plainrestricted=1Android 17's message-promotion implementation marks messages inserted for a non-default SMS package as read-restricted by default. A restricted row is filtered from provider queries unless the caller has the privileged
READ_RESTRICTED_MESSAGESapp-op. PlainApp does not have that app-op and should not attempt to bypass this platform boundary.The platform path is explicit in the Android 17 sources: the SMS provider applies
ReadRestriction.setReadRestrictionValueOnInsert()during insertion,Telephony.ReadRestrictiondefaults the row to restricted when message promotion is enabled and the caller is not the default SMS package, and provider queries filter restricted rows unless the read-restricted-message app-op is allowed. See GrapheneOSSmsProvider,Telephony.ReadRestriction, andProviderUtil.canReadRestrictedMessages.The access-control diagnosis was verified reversibly: the same exact-ID GraphQL query returned no row with PlainApp's normal app-op state, returned the sent row while
READ_RESTRICTED_MESSAGESwas temporarily allowed through ADB, and returned no row again after the original ignored state was restored.Provider reconciliation would otherwise match
The original optimistic item and Android provider row were compared without exposing message content or destination data. Their thread, body, normalized destination, direction, and timestamp window all match. The row is absent only because the Android provider filters it before PlainApp's query receives results.
A successful terminal result is still rendered as pending
useMessageThread.handleSmsSendResult()settles the failure deadline and starts three forced provider retries when a successful result arrives. If no readable provider row appears, the optimistic item remains inpendingSmsItems.MessageChatBubble.vuedetermines the visible state entirely from thepending_smsID prefix. There is no successful-but-awaiting-reconciliation state, so an item whose send is conclusively successful is still rendered as “Sending…”. The successful result also cancels the only deadline that could remove or fail it, making the label permanent for the lifetime of that page state.Root cause
The bug is the interaction of two valid behaviors and one invalid frontend assumption:
When assumption 3 is false, a successful send remains visually in progress forever.
Implemented correction
The correction is confined to
plain-desktop, which is the shared browser/Tauri frontend:sendingorsentstate.SMS_SEND_RESULTtransitions only its matching operation tosentwhile retaining the optimistic item for provider reconciliation.MessageChatBubblerenders “Sent” for a successful optimistic SMS instead of deriving “Sending…” solely from its temporary ID.Regression coverage
tests/lib/sms-state-sync.test.tstests/hooks/message-thread.test.tsValidation
Before-fix evidence completed
Post-fix validation
corepack yarn test tests/lib/sms-state-sync.test.ts tests/hooks/message-thread.test.ts: 20/20 tests passed.corepack yarn typecheck: passed.corepack yarn build: production Vite build passed.corepack yarn test: 483 passed and 52 skipped. Four pre-existing environment/isolation tests failed in unchangedcross-window-store,local-mode, andwindow-clientsuites; these are the same unrelated baseline failures already recorded in the main issue SMS/MMS state does not synchronize reliably between Android and web clients #345 report.restricted=1with successful provider status/error values.READ_RESTRICTED_MESSAGESapp-op was restored to its original ignored state after diagnosis and again verified after the test.Remaining limitations
Android controls whether and when a restricted provider row becomes readable to PlainApp. The frontend can truthfully display the successful terminal result and retain it for later reconciliation, but it must not grant itself privileged provider access. A full page reload cannot reconstruct a provider-filtered message unless the platform/default messaging app has promoted the row or PlainApp introduces a separate durable message store; that broader storage and privacy decision is outside this focused status fix.