From 04b324b711150050174044518b523702131f456d Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Fri, 13 Mar 2026 22:17:27 -0700 Subject: [PATCH] Don't treat matched-prefix DMs as an ack (as it is an echo-ack for channel messages, not DMs) --- app/AGENTS.md | 2 +- app/services/messages.py | 2 +- tests/test_echo_dedup.py | 22 ++++++++++++++++++---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/app/AGENTS.md b/app/AGENTS.md index 0d2f0f0..f0c6b82 100644 --- a/app/AGENTS.md +++ b/app/AGENTS.md @@ -115,7 +115,7 @@ app/ ### Echo/repeat dedup - Message uniqueness: `(type, conversation_key, text, sender_timestamp)`. -- Duplicate insert is treated as an echo/repeat: the new path (if any) is appended, and the ACK count is incremented **only for outgoing messages**. Incoming repeats add path data but do not change the ACK count. +- Duplicate insert is treated as an echo/repeat: the new path (if any) is appended, and the ACK count is incremented only for outgoing channel messages. Incoming repeats and direct-message duplicates may still add path data, but DM delivery state advances only from real ACK events. ### Raw packet dedup policy diff --git a/app/services/messages.py b/app/services/messages.py index cd9b256..d23169e 100644 --- a/app/services/messages.py +++ b/app/services/messages.py @@ -166,7 +166,7 @@ async def handle_duplicate_message( else: paths = existing_msg.paths or [] - if existing_msg.outgoing: + if existing_msg.outgoing and existing_msg.type == "CHAN": ack_count = await MessageRepository.increment_ack_count(existing_msg.id) else: ack_count = existing_msg.acked diff --git a/tests/test_echo_dedup.py b/tests/test_echo_dedup.py index d4ff0f6..772fc9e 100644 --- a/tests/test_echo_dedup.py +++ b/tests/test_echo_dedup.py @@ -241,8 +241,10 @@ class TestDMEchoDetection: """Test echo detection for direct messages.""" @pytest.mark.asyncio - async def test_outgoing_dm_echo_increments_ack(self, test_db, captured_broadcasts): - """Outgoing DM echo increments ack count.""" + async def test_outgoing_dm_echo_adds_path_without_incrementing_ack( + self, test_db, captured_broadcasts + ): + """Outgoing DM duplicate keeps path updates but does not count as delivery evidence.""" from app.packet_processor import create_dm_message_from_decrypted # Store outgoing DM @@ -283,9 +285,21 @@ class TestDMEchoDetection: ack_broadcasts = [b for b in broadcasts if b["type"] == "message_acked"] assert len(ack_broadcasts) == 1 - assert ack_broadcasts[0]["data"]["ack_count"] == 1 + assert ack_broadcasts[0]["data"]["message_id"] == msg_id + assert ack_broadcasts[0]["data"]["ack_count"] == 0 assert any(p["path"] == "aabb" for p in ack_broadcasts[0]["data"]["paths"]) + msg = await MessageRepository.get_by_content( + msg_type="PRIV", + conversation_key=CONTACT_PUB.lower(), + text="Hello friend", + sender_timestamp=SENDER_TIMESTAMP, + ) + assert msg is not None + assert msg.acked == 0 + assert msg.paths is not None + assert any(p.path == "aabb" for p in msg.paths) + @pytest.mark.asyncio async def test_incoming_dm_duplicate_does_not_increment_ack(self, test_db, captured_broadcasts): """Duplicate of incoming DM does NOT increment ack.""" @@ -1085,4 +1099,4 @@ class TestMessageAckedBroadcastShape: assert payload_keys <= (self.REQUIRED_KEYS | self.OPTIONAL_KEYS) assert isinstance(payload["message_id"], int) assert isinstance(payload["ack_count"], int) - assert payload["ack_count"] == 1 # Outgoing DM echo increments ack + assert payload["ack_count"] == 0 # Outgoing DM duplicates no longer count as delivery