Repository navigation
Conversation
Baileys answers a retry receipt with whatever getMessage returns. A message deleted for everyone keeps its content in the Message table, so when another device asks for a retry (for example after a group session is re-established on reconnection) the deleted text was relayed again and reappeared for the other members. getMessage now returns undefined on the non-full path when the message is marked deleted: on the row (status DELETED or key.deleted, set by deleteMessage with logical delete) or by a DELETED MessageUpdate for this instance and key id. The messages.update revoke branch now records its MessageUpdate as DELETED instead of the SERVER_ACK fallback, so revokes that arrive as events are covered too. The full path is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideStops Baileys retry receipts from resurrecting deleted message content by filtering deleted records in the retry-specific getMessage path and recording revoke events as DELETED, while leaving full-message retrieval unchanged. Sequence diagram for preventing deleted message retry resendssequenceDiagram
participant Device as GroupDevice
participant Baileys
participant Service as BaileysStartupService
participant Messages as MessageRepository
participant Updates as MessageUpdateRepository
Device->>Baileys: retry receipt
Baileys->>Service: getMessage(key, false)
Service->>Messages: find message by key
Messages-->>Service: message row
Service->>Updates: findFirst(instanceId, keyId, status=DELETED)
Updates-->>Service: deletion match or none
alt message is deleted
Service-->>Baileys: undefined
Baileys-->>Device: no resend
else message is available
Service-->>Baileys: message content
Baileys-->>Device: resend message
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts" line_range="703-706" />
<code_context>
+ return true;
+ }
+
+ const deletion = await this.prismaRepository.messageUpdate.findFirst({
+ where: { instanceId: this.instanceId, keyId, status: 'DELETED' },
+ select: { id: true },
+ });
+
+ return !!deletion;
</code_context>
<issue_to_address>
**Another instance’s message is deleted**
When a shared-group deletion selects a different instance’s copy, `deleteMessage` marks the wrong instance’s row, leaving the requesting instance’s row live. Its deletion marker is absent or is later removed with the other row, so `isMessageDeleted` finds no deletion and a retry returns the content.
Scope the `deleteMessage` lookup to the current instance so it marks that instance’s message row.
Also at `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:1893-1925`.
</issue_to_address>
### Comment 2
<location path="src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts" line_range="703-706" />
<code_context>
+ return true;
+ }
+
+ const deletion = await this.prismaRepository.messageUpdate.findFirst({
+ where: { instanceId: this.instanceId, keyId, status: 'DELETED' },
+ select: { id: true },
+ });
+
+ return !!deletion;
</code_context>
<issue_to_address>
**Deletion checks scan update history**
When an instance has accumulated many `MessageUpdate` rows, the lookup filters by `instanceId`, `keyId`, and `status` without a matching index, so retry checks scan the instance’s update rows and add increasing database work.
Add an index that supports the deletion lookup’s filter fields.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 2 findings to address first, and if the deletion check is wrong or misses a recorded deletion, a retry receipt can resend deleted message content to other devices, and reverting cannot undo that disclosure. If it incorrectly suppresses a live message, the delivery failure is bounded and can be repaired by retrying after a fix.
Blocking findings: src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:706, src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:706
| const deletion = await this.prismaRepository.messageUpdate.findFirst({ | ||
| where: { instanceId: this.instanceId, keyId, status: 'DELETED' }, | ||
| select: { id: true }, | ||
| }); |
There was a problem hiding this comment.
🟠 High · Another instance’s message is deleted
When a shared-group deletion selects a different instance’s copy, deleteMessage marks the wrong instance’s row, leaving the requesting instance’s row live. Its deletion marker is absent or is later removed with the other row, so isMessageDeleted finds no deletion and a retry returns the content.
Scope the deleteMessage lookup to the current instance so it marks that instance’s message row.
Also at src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:1893-1925.
Prompt for AI agents
In `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts` at lines 703-706:
**Another instance’s message is deleted**
When a shared-group deletion selects a different instance’s copy, `deleteMessage` marks the wrong instance’s row, leaving the requesting instance’s row live. Its deletion marker is absent or is later removed with the other row, so `isMessageDeleted` finds no deletion and a retry returns the content.
Scope the `deleteMessage` lookup to the current instance so it marks that instance’s message row.
Also at `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:1893-1925`.| const deletion = await this.prismaRepository.messageUpdate.findFirst({ | ||
| where: { instanceId: this.instanceId, keyId, status: 'DELETED' }, | ||
| select: { id: true }, | ||
| }); |
There was a problem hiding this comment.
🟡 Medium · Deletion checks scan update history
When an instance has accumulated many MessageUpdate rows, the lookup filters by instanceId, keyId, and status without a matching index, so retry checks scan the instance’s update rows and add increasing database work.
Add an index that supports the deletion lookup’s filter fields.
Prompt for AI agents
In `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts` at lines 703-706:
**Deletion checks scan update history**
When an instance has accumulated many `MessageUpdate` rows, the lookup filters by `instanceId`, `keyId`, and `status` without a matching index, so retry checks scan the instance’s update rows and add increasing database work.
Add an index that supports the deletion lookup’s filter fields.
📋 Description
A message deleted for everyone no longer comes back. Before this change, when a group member's device sent a retry receipt (common after a group session is re-established, for example when an instance reconnects or re-pairs), Evolution answered with the original content of the deleted message, and the text reappeared for the other members, sometimes days after it was deleted.
Why it happened. Baileys answers a retry receipt with whatever
getMessagereturns, and skips the resend only when it returns a falsy value (sendMessagesAgaininSocket/messages-recv). Deleting a message keeps its content in theMessagetable, andgetMessagenever looked at the deletion, so it returned the deleted payload and Baileys relayed it.What changed
getMessage(non-full path only) returnsundefinedwhen the message is marked deleted, so Baileys logsrecv retry request, but message not availableand sends nothing. Thefull = truepath used by edits, quoted replies, media download andupdateMessageis unchanged.status = 'DELETED'orkey.deleted = true(set bydeleteMessagewithLOGICAL_MESSAGE_DELETE), or when aDELETEDMessageUpdateexists for this instance and key id. The lookup is by instance and key id, not bymessageId: the row lookup indeleteMessageis not scoped to the instance, so in a group shared by several instances the deletion can be linked to another instance's copy (about 4% of deletions in the deployment we looked at).messages.updaterevoke branch (message === null) now stores itsMessageUpdateasDELETEDinstead of theSERVER_ACKfallback, so revokes that arrive as events (for example, deleted from the phone) are also honored. TheMESSAGES_DELETEwebhook already reported this status.How to reproduce (before this change)
DELETE /chat/deleteMessageForEveryone/{instance}). The phone shows "This message was deleted".Messagerows show up for the deleted key id with the full content.Checking it without a phone (PostgreSQL), messages of instance A that were deleted and later re-delivered:
Rows received after the deletion with
text_len > 0are deleted messages that were resent.To see who asked for the retry, run with
LOG_BAILEYS=debugand look forrecv retry request.Known limitation
Baileys keeps a recent-message cache (
enableRecentMessageCache, 512 messages, 5 minute TTL) and checks it before callinggetMessage. A retry that arrives within 5 minutes of the original send can still be answered from that cache, and Evolution does not control that path. The cases we observed were days old and are covered by this change.🔗 Related Issue
No open issue. Complements #2728, which stops
getMessagefrom returning{ conversation: '' }when the message is not found. The two changes do not overlap: #2728 covers a missing row, this PR covers a row that exists but was deleted.🧪 Type of Change
🧪 Testing
What was verified:
status = 'DELETED',key.deleted, deletion recorded only as an instance-scopedMessageUpdate, and aDELETEDupdate from another instance (must not count). All pass.tsc --noEmit,eslintandprettier --checkare clean.✅ Checklist
📝 Additional Notes
Trade-off: a device that asks for a retry of a deleted message receives nothing. That matches what "delete for everyone" promises; previously it received the deleted content.
🤖 Generated with Claude Code
Summary by Sourcery
Stop Baileys retry handling from restoring messages that were deleted for everyone.
Bug Fixes: