[iOS · Delivery Status] Message remains "Not sent" with "Retry" after a read receipt is received #20
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
[iOS · Delivery Status] Message remains "Not sent" with "Retry" after a read receipt is received
Status: Confirmed behavior defect.
Component: Swift delivery model / failure-state UI.
Related investigation: INVESTIGATION_96_offline_queue_rejection_then_delivery.zip
Summary
A message can receive a delivery failure (resource-constraint) and later be successfully received and read by the recipient. However, after the sender receives remote_status=read for the same message, the UI still shows "Not sent" and "Retry".
This creates an inconsistent state: the recipient has read the message, but the sender is still encouraged to resend it.
Environment
A (sender): iPhone 13 Pro Max, iOS 26.7.1, testman421@another.im
B (recipient): iPhone 17 Pro Max, iOS 27.2, testman435@another.im
App: v1.1.0 build 96 (feat/ui-redesign@e945d7cc)
libtacky: v1.2.0-rc4
1:1 OMEMO chat
Steps to reproduce
Send a message from A to B while B is suspended/locked.
The message receives a resource-constraint failure indicating that the offline message queue is full.
Resume B, open the chat, and read the message.
Verify that A receives remote_status=read for the same message ID.
Check the message status on A.
Expected result
Once the same message has been received and read, the UI should show a final state consistent with the actual delivery. A previous delivery-stage failure should not leave the message marked as "Not sent" with an unconditional "Retry" action.
Actual result
Both affected messages received remote_status=read at 15:55:06 UTC, and the messages were present on B. However, at 15:58 UTC, A still displayed both messages in red as "Not sent" with "Retry".
No retry was performed during this test.
Relevant diagnostics
For both affected messages:
A recorded server_status=failed with fail_reason=resource-constraint.
The failure referred to the same message IDs/timestamps that later received remote_status=read.
B received both messages after reconnecting and later marked the conversation as displayed.
Code observation
In revision e945d7cc:
DeliveryChange.applied preserves the previous failed server state when an update contains only a remote status.
TranscriptWindow clears failReason only when delivery.server is updated.
As a result, a read update without a new server status does not clear the previous failure state in the current transcript.
This behavior is consistent with the observed logs and UI, although an independent reproduction and diagnostic stack trace are not available.
Retest criteria
Reproduce a failure followed by successful delivery and read for the same message ID.
Verify that the final UI state no longer shows the stale "Not sent" / "Retry" state.
Repeat after navigating away and back and after restarting the app.
Verify that genuinely undelivered messages without a receipt still retain the appropriate failure and retry behavior.
Analysis (anotherim-swift, 2026-11-07)
The row's state comes from libtacky, and the app shows what it stores:
BounceWindowUs(10 min) of the send fails the row even after our server's ack (message.tcl:310, :1510-1534);markFailedsetsserver_status='failed',fail_reason=<condition>(messagestore.tcl:3162).remote_status(markRemoteStatus,messagestore.tcl:1047-1083), andmessage <Status>carries only-remote_status(message.tcl:1895). So the row staysfailedwithremote_status=read, live and after a relaunch.markRemoteStatushas noserver_statusfilter), so a message that never left (encrypt, upload, too large) would show as read once the peer reads a later one.Proposed fix in libtacky
markRemoteStatus: the read walk skips rows withserver_status='failed', except the row the receipt/marker names.server_statusto''(orsent) andfail_reason/fail_text, and emit<Status -server_status … -remote_status …>.MarkOutgoingFailedon an acked row: ignore the bounce whenremote_status != 'none'(receipt first, bounce second; see another-im/anotherim-server#3).failedwith a bounce condition andremote_statusin received/displayed/read.The app needs no change once this lands: a
<Status>with a server status already clears the failure and the Retry (TranscriptWindow.swift). The related server side (why the bounce came at all) is another-im/anotherim-server#3.Fixed in libtacky v1.2.1 (https://dev.another.im/another-im/libtacky/releases/tag/v1.2.1): a receipt or read marker that names a failed (bounced) row now clears its failure (
<Status>setsserver_status ''andfail_reason ''). A read marker no longer passes over a failed row without clearing it, and a bounce that arrives after the peer has already receipted the message is ignored instead of marking it failed again. Already-stuck rows clear as soon as a new receipt or marker names them, so nothing needs a manual fix-up.The bounce itself (why a delivered, read message could be marked failed in the first place) was another-im/anotherim-server#3; see that issue for the server-side fix.
Please check with v1.2.1 or newer.