Skip to content

fix(baileys): recreate messageSubject when it is stopped, not only closed - #2717

Open
kay0ramon wants to merge 1 commit into
evolution-foundation:developfrom
kay0ramon:fix/message-processor-subject-isstopped
Open

fix(baileys): recreate messageSubject when it is stopped, not only closed#2717
kay0ramon wants to merge 1 commit into
evolution-foundation:developfrom
kay0ramon:fix/message-processor-subject-isstopped

Conversation

@kay0ramon

@kay0ramon kay0ramon commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Summary

The reconnection fix from #2186 never triggers. onDestroy() calls complete() on the RxJS Subject, which sets isStopped — it does not set closed. Only unsubscribe() sets closed.

Since mount() guards the recreation with if (this.messageSubject.closed), the condition is always false after a logout. The Subject is never recreated, and the instance keeps silently dropping every incoming message — the exact bug #2186 set out to fix. The MessageSubject was closed, recreating... warning is never logged, which is why the problem is so hard to spot.

Evidence

Verified against the project's own rxjs (7.8.2):

const s = new Subject();
console.log(s.closed, s.isStopped);  // false false
s.complete();
console.log(s.closed, s.isStopped);  // false true   <- complete() sets isStopped, not closed

let n = 0;
s.subscribe({ next: () => n++ });    // this is what mount() does
s.next({ x: 1 });
console.log(n);                       // 0  -> the Subject is deaf

The guard if (this.messageSubject.closed) evaluates to false here, so the Subject is never replaced.

Reproduced in production

Self-hosted 2.3.6 fork, 8 pm2 instances. Same sequence each time — disconnect via DELETE /instance/logout, reconnect via GET /instance/connect, then send an inbound message:

run version inbound message
1 without the fix lost
2 with #2186 exactly as merged lost
3 with this patch delivered

Run 2 is the important one: the fix was deployed and the message was still dropped, with no warning logged.

With this patch, the log shows the recreation 3 seconds after the logout, and the message is processed normally without restarting the process:

01:10:23  WARN [WAMonitoringService]     Instance "…" - LOGOUT
01:10:26  WARN [BaileysMessageProcessor] MessageSubject was closed, recreating...
01:12:08  LOG  [BaileysMessageProcessor] Processing batch of 1 messages

Worth noting how quiet the failure is: the instance stays open, outgoing messages keep working, and nothing is logged — so it reads as "the customer never replied". In our case a scheduled process restart was masking it three times a day.

Change

-    // Se o Subject foi completado, recriar
-    if (this.messageSubject.closed) {
+    if (this.messageSubject.closed || this.messageSubject.isStopped) {

closed is kept in the condition since either state makes the Subject unusable; isStopped is the one the logout path actually produces.

Affects main, develop and 2.4.0-rc, which all still carry the original guard.

🤖 Generated with Claude Code

Summary by Sourcery

Bug Fixes:

  • Ensure the WhatsApp message subject is recreated after logout so incoming messages continue to be processed.

…osed

The reconnection fix from evolution-foundation#2186 never triggers: `onDestroy()` calls
`complete()` on the RxJS Subject, which sets `isStopped` — it does NOT set
`closed`. Only `unsubscribe()` sets `closed`.

Because `mount()` guards the recreation with `if (this.messageSubject.closed)`,
the condition is always false after a logout, the Subject is never recreated,
and the instance keeps silently dropping every incoming message. The
"MessageSubject was closed, recreating..." warning is therefore never logged.

Verified with the project's own rxjs (7.8.2):

    const s = new Subject();
    s.complete();
    s.closed     // false   <- what the guard checks
    s.isStopped  // true    <- what complete() actually sets
    // a subscriber added afterwards receives nothing

Reproduced and fixed in production (8 instances, ~90 connected numbers):

  | logout -> connect -> incoming message | result  |
  |---------------------------------------|---------|
  | without the fix                       | lost    |
  | with evolution-foundation#2186 as merged                  | lost    |
  | with this patch                       | delivered |

With this change the warning fires 3s after the logout and the message is
processed normally, with no process restart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ph46xTPZzhsm1D9P5FkYmn
@sourcery-ai

sourcery-ai Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Fixes post-logout message loss by recognizing RxJS Subjects completed via complete() (isStopped) as unusable, allowing the processor to recreate the Subject before resuming message handling.

Sequence diagram for post-logout message subject recovery

sequenceDiagram
    participant WAMonitoringService
    participant BaileysMessageProcessor
    participant Subject as RxJS Subject
    participant IncomingMessage

    WAMonitoringService->>BaileysMessageProcessor: onDestroy()
    BaileysMessageProcessor->>Subject: complete()
    Note over Subject: isStopped=true, closed=false
    BaileysMessageProcessor->>BaileysMessageProcessor: mount()
    BaileysMessageProcessor->>Subject: check closed || isStopped
    BaileysMessageProcessor->>BaileysMessageProcessor: warn(processorLogs.warn)
    BaileysMessageProcessor->>Subject: new Subject()
    IncomingMessage->>Subject: next(messages)
    Subject-->>BaileysMessageProcessor: deliver messages
Loading

File-Level Changes

Change Details Files
Recreate the inbound message Subject when it has been completed or unsubscribed.
  • Extend the recreation guard to check both closed and isStopped.
  • Preserve the existing warning and Subject reinitialization flow.
src/api/integrations/channel/whatsapp/baileysMessage.processor.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 found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/api/integrations/channel/whatsapp/baileysMessage.processor.ts" line_range="30" />
<code_context>
+    // A completed Subject silently drops every `next()`, but `complete()` only sets
+    // `isStopped``closed` stays false (it is set by `unsubscribe()`). Checking `closed`
+    // alone never recreates the Subject, so the instance stays deaf after a logout.
+    if (this.messageSubject.closed || this.messageSubject.isStopped) {
       this.processorLogs.warn('MessageSubject was closed, recreating...');
       this.messageSubject = new Subject<{
</code_context>
<issue_to_address>
**issue (broader_impact):** The new recreation check does not affect the active inbound-message path because `eventHandler()` no longer calls `messageProcessor.processMessage`; it directly awaits `messageHandle['messages.upsert']`. Recreating `messageSubject` therefore cannot restore delivery for messages handled through the current event flow.

**Triggers:** When inbound events are processed through `BaileysStartupService.eventHandler()`, which is the current implementation path.

**Suggested fix:** Either route `messages.upsert` events through `messageProcessor.processMessage` again or remove the unused subject-based processor and apply the lifecycle fix to the actual handler path.
</issue_to_address>

Sourcery assessment

Approval pending. 1 finding to address first.

Blocking findings: src/api/integrations/channel/whatsapp/baileysMessage.processor.ts:30


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

// A completed Subject silently drops every `next()`, but `complete()` only sets
// `isStopped` — `closed` stays false (it is set by `unsubscribe()`). Checking `closed`
// alone never recreates the Subject, so the instance stays deaf after a logout.
if (this.messageSubject.closed || this.messageSubject.isStopped) {

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.

issue (broader_impact): The new recreation check does not affect the active inbound-message path because eventHandler() no longer calls messageProcessor.processMessage; it directly awaits messageHandle['messages.upsert']. Recreating messageSubject therefore cannot restore delivery for messages handled through the current event flow.

Triggers: When inbound events are processed through BaileysStartupService.eventHandler(), which is the current implementation path.

Suggested fix: Either route messages.upsert events through messageProcessor.processMessage again or remove the unused subject-based processor and apply the lifecycle fix to the actual handler path.

@wagnerfnds

Copy link
Copy Markdown

my hero!

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.

2 participants