Skip to content

fix(whatsapp): reopen conversation across a contact's coexistence identities - #15098

Merged
tds-1 merged 1 commit into
developfrom
fix/cw-7651-whatsapp-coexistence-duplicate-conversation
Jul 21, 2026
Merged

fix(whatsapp): reopen conversation across a contact's coexistence identities#15098
tds-1 merged 1 commit into
developfrom
fix/cw-7651-whatsapp-coexistence-duplicate-conversation

Conversation

@tds-1

@tds-1 tds-1 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

WhatsApp contacts using coexistence are identified by more than one source ID (a phone wa_id and a BR./BSUID identity), so a single contact ends up owning multiple contact_inbox records. The "reopen the same conversation" feature scoped conversation reuse to a single contact_inbox, so messages arriving under a different identity of the same contact started a brand-new conversation — even with reopen enabled — producing duplicate conversations.

This scopes reuse to the contact across all of its contact_inbox records in the inbox instead of a single contact_inbox.

Closes

How to reproduce

  1. On a WhatsApp Cloud inbox with "reopen the same conversation" (lock to single conversation) enabled.
  2. Have a coexistence contact whose webhooks alternate between carrying the phone wa_id and only the BSUID identity.
  3. Before: each identity opens its own conversation → duplicates. After: incoming messages reopen the contact's existing conversation regardless of which identity the webhook carried.

What changed

  • Whatsapp::IncomingMessageBaseService#set_conversation now looks up reusable conversations via @contact.conversations.where(inbox_id: @inbox.id) instead of @contact_inbox.conversations.
  • Updated existing specs to wire the conversation's contact to the contact_inbox's contact, mirroring production data.

@itoqa

itoqa Bot commented Jul 21, 2026

Copy link
Copy Markdown

Ito QA test results
Commit: 32aa35d: 11 test cases ran, 10 passed ✅, 1 additional finding ⚠️.

Summary

Coverage spans normal and edge-case message handling across phone and alternate WhatsApp identities, including conversation reuse, inbox and contact isolation, duplicate delivery protection, resolved-thread behavior, rollback safety, and signed inbound message processing. The application behavior is healthy across sequential, identity-variant, status, and failure-recovery paths, with one concurrency-specific fragmentation issue identified.

Safe to merge — the sole failure is a medium-severity concurrency bug explicitly determined to be pre-existing and not attributable to this PR, while the PR’s targeted sequential identity-reuse behavior and related safeguards passed. The concurrency issue is a flag for later rather than a merge blocker, with no PR regressions or newly introduced failures identified.

Tests run by Ito

View full run

Result Severity Type Description
Conversation Sequential phone and BSUID WhatsApp webhook deliveries were accepted locally and processed into one conversation for the shared Contact. The persisted conversation contained both messages and two identities, and the inbox showed a single conversation with continuous history.
Conversation BSUID and phone follow-up webhooks were accepted locally and persisted in one conversation for the shared contact. The conversation retained both identities and messages, and the inbox UI displayed the continuous history under QA C3 Contact.
Isolation The local service kept the existing conversation in the other inbox at zero messages and stored the inbound message in a conversation belonging to the current inbox.
Isolation Contact 1's existing conversation remained at zero messages, while a message for Contact 2 created a separate conversation with one message and the expected Contact 2 association. The service implementation scopes reuse through the current Contact and inbox, so a different Contact's conversation is not eligible for reuse.
Message Two identical WhatsApp webhook deliveries returned HTTP 200 and, after asynchronous processing, produced exactly one conversation and one persisted message for source ID wamid.qa.message1.replay. The message was linked to the expected contact and inbox, and the dashboard displayed the single replay conversation.
Message Source review and four focused WhatsApp regression examples confirm that conversation selection and message creation run in one transaction, so a persistence exception rolls back a newly created conversation. The earlier blocked attempt was caused by missing controlled fault injection and contaminated local database state rather than an application failure.
Status Single-lock mode reused the latest contact-scoped resolved conversation and the incoming message reopened it. The initial execution was blocked by missing local WhatsApp fixtures and provider delivery, but the exact targeted service example passed with 1 example and 0 failures.
Status Unlocked WhatsApp conversation handling excludes the resolved conversation and creates a new thread for the incoming message. The targeted service example passed with 1 example and 0 failures, and source review confirms the behavior is scoped to the current contact and inbox.
Webhook A signed WhatsApp Cloud envelope was accepted with HTTP 200, processed asynchronously, and persisted message 16 in the existing conversation for contact 2 and the active WhatsApp inbox. The readback found exactly one eligible conversation.
Webhook The signed Cloud webhook was accepted with HTTP 200 and processed asynchronously. The BSUID identity resolved to the intended Contact, and its message was stored in the existing WhatsApp conversation without creating a duplicate.
⚠️ Medium severity Conversation The persisted readback contained two open conversations for the same Contact and inbox, with one message on each conversation, although the expected result was one conversation containing both messages.
Additional Findings Details

These findings are unrelated to the current changes but were observed during testing.

🟡 Concurrent phone and BSUID deliveries create one conversation
  • Severity: Medium Medium severity
  • Description: The persisted readback contained two open conversations for the same Contact and inbox, with one message on each conversation, although the expected result was one conversation containing both messages.
  • Impact: Concurrent WhatsApp deliveries for the same contact can be split into separate open conversations in one inbox, forcing agents to manage an incorrectly fragmented conversation. Both messages remain persisted, and no evidence indicates data loss or security exposure.
  • Steps to Reproduce:
    1. Prepare one WhatsApp inbox with lock_to_single_conversation enabled, one Contact, and phone and BSUID ContactInbox identities linked to that Contact.
    2. Start two valid incoming deliveries concurrently with distinct source IDs, before either delivery has created a conversation.
    3. Wait for processing and count conversations and messages for the Contact in the inbox.
  • Stub / mock content: Local WhatsApp channel, Contact, phone and BSUID identity fixtures, and worker instrumentation were used to coordinate the concurrent deliveries; no external provider or production data was involved.
  • Code Analysis: At app/services/whatsapp/incoming_message_base_service.rb:118-128, the service queries reusable conversations and, when the result is empty, calls Conversation.create! without a lock or database uniqueness guard covering the one-conversation invariant. Two concurrent transactions can both observe no reusable row and create separate conversations before either commit. The PR's changed lines broaden the query from @contact_inbox.conversations to @contact.conversations.where(inbox_id: @inbox.id), which fixes sequential sibling-identity reuse but does not make this read-and-create operation atomic. The smallest practical fix is to serialize conversation selection/creation for the contact and inbox, or enforce the invariant with a database-safe create/retry path, while retaining the inbox-scoped contact query.
Evidence Package

Tip

Reply with @itoqa to send us feedback on this test run.

@tds-1
tds-1 merged commit 2144de9 into develop Jul 21, 2026
40 checks passed
@tds-1
tds-1 deleted the fix/cw-7651-whatsapp-coexistence-duplicate-conversation branch July 21, 2026 10:51
marcoazcabral added a commit to marcoazcabral/chatwoot that referenced this pull request Aug 12, 2026
Reworked to the direction agreed in chatwoot#14931: the business scoped user id is the
active identity whenever one is present, a message resolves to that exact contact
inbox, and the conversation opened under a previous identity is left where it is.

Three changes carry it. Resolution now lists the identifier before the phone
number, so the phone is the fallback for a payload that carries none. Reuse is
scoped to the contact inbox that resolved the message rather than to the contact,
which is what removes the contact wide lookup: a contact can hold unrelated
identities in one inbox, from coexistence or from a dashboard merge, and that
lookup cannot tell them apart. The repoint this PR carried before is gone, since
the phone conversation is meant to stay reachable under previous conversations.

Worth stating plainly: this changes behaviour released in chatwoot#15098. A contact that
already holds a phone conversation gets a second one on the identifier inbox
instead of continuing the first. That is the agreed trade, and it is what makes a
reply always address the identity that sent the message.
@sony-mathew sony-mathew added this to the v4.17.0 milestone Aug 19, 2026 — with ChatGPT Codex Connector
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants