From 6ad522b52bff4bc71a60dc4553fac21339de462d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Apr 2026 19:50:50 +0000 Subject: [PATCH] fix(marmot): surface inbound app messages even when LocalCache says duplicate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The B->Amethyst kind:445 reply was decrypting fine — the harness's new outbound-side diagnostics + the Amethyst logcat showed a clean ApplicationMessage path right up to the chatroom-add step: GroupEventHandler.add: kind:445 id=988221d0… groupId=ee27bc48… GroupEventHandler.add: ApplicationMessage decrypted innerKind=9 innerId=ebf35373… author=170df8d1… GroupEventHandler.add: inner event already in cache (duplicate) `cache.justConsume` returned false because the inner event id was already in LocalCache (a prior session persisted the same plaintext via persistDecryptedMessage and the startup restore loop in Account.kt:3034-3044 reloaded it into the cache before this kind:445 arrived). The live-receive branch in DecryptAndIndexProcessor was gated on `isNew` and silently dropped the message — never calling marmotGroupList.addMessage — so the chatroom never saw B's reply even though decryption succeeded. Mirror what the restore path does: always surface the inner note in the chatroom. `MarmotGroupChatroom.addMessageSync` is itself idempotent (dedupes by Note identity), so the call is safe even when the message was already added through another path. `persistDecryptedMessage` stays guarded on `isNew` because the message store is append-only and a re-persist would grow the on-disk log unboundedly across decrypt retries. --- .../loggedIn/DecryptAndIndexProcessor.kt | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index 100760c5e..49b91b287 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -592,20 +592,41 @@ class GroupEventHandler( "GroupEventHandler.add: ApplicationMessage decrypted innerKind=${innerEvent.kind} " + "innerId=${innerEvent.id.take(8)}… author=${innerEvent.pubKey.take(8)}…" } - if (cache.justConsume(innerEvent, null, false)) { - val innerNote = cache.getOrCreateNote(innerEvent.id) + // `cache.justConsume` returns false if LocalCache already + // has this inner event id — typically because a previous + // session persisted the inner JSON and the startup + // restore loop in Account reloaded it into the cache + // before this kind:445 arrived. The chatroom may still + // not have it (e.g. the user's chatroom snapshot was + // dropped, or the restore-from-disk path raced ahead and + // the cache populated before the chatroom was hydrated). + // `addMessage` is itself idempotent (`addMessageSync` + // dedupes by Note identity), so always surface the note + // in the chatroom — otherwise the message is silently + // dropped and the operator sees only the "duplicate" log. + val isNew = cache.justConsume(innerEvent, null, false) + val innerNote = cache.getOrCreateNote(innerEvent.id) + if (isNew) { innerNote.event = innerEvent - - // Track the message in the Marmot group chatroom - account.marmotGroupList.addMessage(result.groupId, innerNote) - - // Persist the decrypted plaintext so the message - // survives an app restart. Marmot/MLS application - // messages cannot be re-decrypted once the ratchet - // has advanced, so we must capture them here. - manager.persistDecryptedMessage(result.groupId, result.innerEventJson) } else { - Log.d("MarmotDbg") { "GroupEventHandler.add: inner event already in cache (duplicate)" } + Log.d("MarmotDbg") { + "GroupEventHandler.add: inner event already in cache — surfacing in chatroom anyway" + } + } + + // Track the message in the Marmot group chatroom + account.marmotGroupList.addMessage(result.groupId, innerNote) + + // Persist the decrypted plaintext so the message + // survives an app restart. Marmot/MLS application + // messages cannot be re-decrypted once the ratchet + // has advanced, so we must capture them here. Skip + // when the cache reported duplicate — the message + // was already persisted on the run that originally + // decrypted it (the message store appends, so a + // re-persist would silently grow the on-disk log). + if (isNew) { + manager.persistDecryptedMessage(result.groupId, result.innerEventJson) } }