Skip to content

fix(chatwoot): find existing conversation by contact and inbox - #2756

Open
kauecostax wants to merge 1 commit into
evolution-foundation:developfrom
Sinapses-AI:fix/chatwoot-find-conversation-by-inbox
Open

kauecostax wants to merge 1 commit into
evolution-foundation:developfrom
Sinapses-AI:fix/chatwoot-find-conversation-by-inbox

Conversation

@kauecostax

@kauecostax kauecostax commented Oct 15, 2026 •

Copy link
Copy Markdown

📋 Description

createConversation and getOpenConversationByContact look for the contact's conversation in the current inbox by calling GET /api/v1/accounts/:account_id/contacts/:id/conversations and filtering the result by inbox_id. Chatwoot returns at most 20 conversations from that endpoint, across all inboxes, ordered by last activity (limit(20) in Api::V1::Accounts::Contacts::ConversationsController#index).

When the same contact exists in more than 20 inboxes of one Chatwoot account, the conversation for the current inbox is often missing from those 20. Evolution then assumes there is no conversation and creates a new one. This happens on every cache miss (the conversation ID is cached for 30 minutes). Typical cases are a WhatsApp group that many connected numbers belong to, a person who talks to many of those numbers, and the EvolutionAPI bot contact (123456) that posts connection notices in every inbox.

In production, with about 50 instances connected to one Chatwoot account, one group contact accumulated more than 14,000 open conversations, and others had thousands. Besides cluttering the conversation lists, this made Chatwoot unusable. Updating such a contact (PUT /contacts/:id, which Evolution sends when the group picture changes) renders every contact_inbox in the response and went past Chatwoot's 60 s request timeout, holding the Rails threads for all accounts.

This PR asks Chatwoot only for the conversations of that contact in that inbox, through POST /api/v1/accounts/:account_id/conversations/filter with contact_id and inbox_id. The result has the same shape as before (payload sorted by last activity), so the logic after the lookup is unchanged, including reopenConversation and conversationPending.

The contact_id attribute was added to Chatwoot's conversation filter in chatwoot/chatwoot#14629 (June 2026). Older Chatwoot versions reject it with 400/422. In that case the service falls back to the previous endpoint and remembers it per Chatwoot URL, so it does not try the filter again on every message. Other errors (e.g. a timeout) fall back for that call only.

🔗 Related Issue

No existing issue. The root cause is described above.

🧪 Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🔧 Refactoring (no functional changes)
  • ⚡ Performance improvement
  • 🧹 Code cleanup
  • 🔒 Security fix

🧪 Testing

  • Manual testing completed
  • Functionality verified in development environment
  • No breaking changes introduced
  • Tested with different connection types (if applicable)

The same change was applied on top of 2.3.7 and tested against Chatwoot v4.17 before running in production:

  • Scenario: a contact with 26 conversations, 25 of them in other inboxes with more recent activity, so the one in the instance's inbox was outside the 20 returned by the contact endpoint. Cache cleared before each message.
  • Official 2.3.7 image: the incoming message created a duplicate conversation (Chatwoot log: GET /contacts/:id/conversations followed by POST /conversations).
  • Patched image: the message went to the existing conversation (Chatwoot log: POST /conversations/filter only). Text and audio messages verified.
  • The fallback path was checked against Chatwoot: an unknown filter attribute returns 422.
  • In production, after the switch, no new duplicate conversations were created and Chatwoot only received conversations/filter lookups.

On develop, the change compiles (tsc --noEmit) and passes ESLint.

✅ Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have manually tested my changes thoroughly
  • I have verified the changes work with different scenarios
  • Any dependent changes have been merged and published

📝 Additional Notes

Duplicates already created are not removed by this PR. They stop growing once it is deployed.

🤖 Generated with Claude Code

Summary by Sourcery

Prevent duplicate Chatwoot conversations by using inbox-scoped contact lookups with compatibility fallback for older Chatwoot versions.

Bug Fixes:

  • Find existing Chatwoot conversations by both contact and inbox to prevent duplicate conversations when the contact has more than 20 conversations across inboxes.
  • Fall back to the legacy contact-conversations lookup for older Chatwoot versions that do not support filtering by contact ID.

Enhancements:

  • Cache unsupported Chatwoot filter capability per provider URL and retain per-call fallback behavior for other lookup errors.

The contact conversations endpoint returns at most 20 conversations across
all inboxes. When the same contact is present in more than 20 inboxes of an
account (a WhatsApp group shared by many instances, or a person who talks to
many of them), the conversation for the current inbox is often missing from
that list, so createConversation opens a new one every time the cache
expires. One group reached 14k open conversations, and contact updates that
render all contact_inboxes started timing out in Chatwoot.

Query the conversation filter by contact_id and inbox_id instead, in both
createConversation and getOpenConversationByContact. Chatwoot versions whose
filter does not accept contact_id answer 400/422; for those, fall back to the
previous endpoint and remember it per Chatwoot URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

The PR fixes duplicate Chatwoot conversations by querying conversations directly by contact and inbox, while retaining compatibility with older Chatwoot versions through a per-URL capability cache and fallback to the legacy endpoint.

Sequence diagram for inbox-scoped conversation lookup

sequenceDiagram
    participant Service as ChatwootService
    participant Chatwoot as Chatwoot
    participant Cache as contactFilterUnsupported

    Service->>Cache: has(providerUrl)
    alt filter supported
        Service->>Chatwoot: conversations.filter(contact_id, inbox_id)
        Chatwoot-->>Service: payload
    else filter returns 400 or 422
        Chatwoot-->>Service: error
        Service->>Cache: add(providerUrl)
        Service->>Chatwoot: contacts.listConversations(contactId)
        Chatwoot-->>Service: payload
    else filter has another error
        Chatwoot-->>Service: error
        Service->>Chatwoot: contacts.listConversations(contactId)
        Chatwoot-->>Service: payload
    end
    Service-->>Service: find open conversation in inbox
Loading

File-Level Changes

Change Details Files
Replace broad contact conversation lookups with a contact-and-inbox conversation filter to prevent duplicate conversations when the contact has more than 20 conversations.
  • Query Chatwoot's conversation filter using contact_id and inbox_id.
  • Use the filtered payload in both conversation creation and open-conversation lookup paths while preserving existing downstream behavior.
src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts
Add compatibility fallback for Chatwoot versions that do not support filtering by contact.
  • Fall back to the existing contact-conversations endpoint on 400/422 responses.
  • Cache unsupported Chatwoot URLs to avoid retrying the unsupported filter on every message.
  • Fall back only for the current call on other filter errors and log the failure.
src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!

Sourcery assessment

Needs a human reviewer. If the new filter misses an existing conversation, the integration may create a duplicate conversation record; reverting restores the old lookup but does not remove records already created. The likely impact is bounded and can be repaired in Chatwoot.


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

This branch has not been deployed

No deployments
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.

1 participant