Retry unacknowledged DMs after Noise replacement (#1462)

DMs sent through an established Noise session are retained in the durable MessageRouter outbox until an authenticated delivery/read ack, and retried under the same message ID when the peer's replacement handshake authenticates — fixing DMs silently lost into stale local sessions after a remote app restart. Mesh ack handling is peer-scoped end-to-end (alias-scoped delivery status, peer-bound markDelivered, PeerMessageKey retry state), so a colliding message ID from another conversation can no longer clear or promote foreign state; bridge-drop dedup keys are recipient-scoped with hashed persistence.

Includes review fix: acks arriving after a relaunch (durable outbox restored, conversation not) now clear the peer-scoped router entry unconditionally — only the UI status transition is gated on conversation presence — so a delivered message can no longer re-send to the attempt cap and be marked failed despite delivery. Regression test simulates the relaunch through real store persistence; the dead allowedPeerIDs parameter (unscoped-tombstone trap) is removed.
This commit is contained in:
jack
2026-07-26 15:37:50 +02:00
committed by GitHub
parent a4d294015a
commit a1711bd399
17 changed files with 1519 additions and 126 deletions
+15
View File
@@ -65,11 +65,15 @@ final class MockTransport: Transport, PrivateMediaDeletionPersisting {
var peerFingerprints: [PeerID: String] = [:]
var peerNoiseStates: [PeerID: LazyHandshakeState] = [:]
var privateMediaPolicies: [PeerID: PrivateMediaSendPolicy] = [:]
var privateMediaReceiptSessionGenerations: [PeerID: UUID] = [:]
var persistDeletedPrivateMediaResult = true
var deferDeletedPrivateMediaPersistence = false
private var pendingDeletedPrivateMediaCompletions: [
@MainActor (Bool) -> Void
] = []
/// Optional synchronous hook for send-ordering tests (for example, an ack
/// arriving before the router's send call returns).
var onSendPrivateMessage: (@MainActor (_ messageID: String) -> Void)?
private let mockKeychain = MockKeychain()
// MARK: - Transport Protocol Implementation
@@ -174,6 +178,11 @@ final class MockTransport: Transport, PrivateMediaDeletionPersisting {
func sendPrivateMessage(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String) {
sentPrivateMessages.append((content, peerID, recipientNickname, messageID))
if let onSendPrivateMessage {
MainActor.assumeIsolated {
onSendPrivateMessage(messageID)
}
}
}
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID) {
@@ -215,6 +224,12 @@ final class MockTransport: Transport, PrivateMediaDeletionPersisting {
privateMediaPolicies[peerID] ?? .encrypted
}
func authenticatedPrivateMediaReceiptSessionGeneration(
to peerID: PeerID
) -> UUID? {
privateMediaReceiptSessionGenerations[peerID]
}
func resolvePrivateMediaSendPolicy(
to peerID: PeerID,
completion: @escaping @MainActor (PrivateMediaSendPolicy) -> Void