NousResearch/hermes-agent

WhatsApp bridge cross-chat message contamination fix

Cluster 33360 · gateway. Data as of

Nothing open on GitHub. Fix merged in #54062 on 28 Jun 2026. Check it covers your case.

The short answer

0open issues
0open PRs
4items grouped
Known?
Yes. 4 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #54062. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #54062. That is separate from the fix event.
Still open
Nothing open on GitHub.

These items concern a bug where messages or replies intended for one WhatsApp chat appear in another, and work to serialize sendMessage calls and improve session isolation to prevent this cross-chat contamination.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #54062 fix(whatsapp-bridge): serialize sendMessage to prevent cross-chat contamination (#33360)

    merged to main

    files: infographic/whatsapp-send-queue/infographic.png, scripts/whatsapp-bridge/bridge.js, scripts/whatsapp-bridge/bridge.sendqueue.test.mjs

    Show the diff
    • scripts/whatsapp-bridge/bridge.js
      @@ -83,6 +83,19 @@ const CHUNK_DELAY_MS = parseInt(process.env.WHATSAPP_CHUNK_DELAY_MS || '300', 10
       // fires. Fail fast instead so the gateway can surface a real error and retry.
       const SEND_TIMEOUT_MS = parseInt(process.env.WHATSAPP_SEND_TIMEOUT_MS || '60000', 10);
       
      +// --- Send queue: serialise all sock.sendMessage() calls across concurrent
      +//     HTTP handlers so a single Baileys socket never has overlapping sends.
      +//     Overlapping sends are the root cause of cross-chat contamination
      +//     (#33360) — the WhatsApp protocol-level routing can misdeliver when
      +//     two sendMessage() Promises race on the same socket. ---
      +let _sendQueue = Promise.resolve();
      +
      +function enqueueSend(fn) {
      +  const task = _sendQueue.then(() => fn(), () => fn());
      +  _sendQueue = task.catch(() => {});
      +  return task;
      +}
      +
       function sleep(ms) {
         return new Promise(resolve => setTimeout(resolve, ms));
       }
      @@ -95,8 +108,10 @@ function sendWithTimeout(chatId, payload, timeoutMs = SEND_TIMEOUT_MS) {
             timeoutMs,
           );
         });
      -  return Promise.race([sock.sendMessage(chatId, payload), timeoutPromise])
      -    .finally(() => clearTimeout(timer));
      +  return enqueueSend(() =>
      +    Promise.race([sock.sendMessage(chatId, payload), timeoutPromise])
      +      .finally(() => clearTimeout(timer))
      +  );
       }
       
       function formatOutgoingMessage(message) {
    • scripts/whatsapp-bridge/bridge.sendqueue.test.mjs
      @@ -0,0 +1,112 @@
      +/**
      + * Regression tests for the WhatsApp bridge send queue (#33360).
      + *
      + * The bridge must serialise all sock.sendMessage() calls through a
      + * promise-based queue so that concurrent HTTP /send requests never
      + * produce overlapping Baileys socket writes.  Overlapping writes are
      + * the confirmed root cause of cross-chat contamination.
      + *
      + * These tests exercise the queue itself — they do NOT require a live
      + * WhatsApp socket.
      + */
      +
      +import { strict as assert } from 'node:assert';
      +
      +// ------------------------------------------------------------------
      +// 1.  Unit test for the queue primitives
      +// ------------------------------------------------------------------
      +
      +/**
      + * Replicate the queue logic from bridge.js so we can test it in
      + * isolation without importing the full module (which would trigger
      + * Baileys / express side effects).
      + */
      +function createSendQueue() {
      +  let _sendQueue = Promise.resolve();
      +
      +  function enqueueSend(fn) {
      +    const task = _sendQueue.then(() => fn(), () => fn());
      +    _sendQueue = task.catch(() => {});
      +    return task;
      +  }
      +
      +  return { enqueueSend };
      +}
      +
      +// -- serial ordering -------------------------------------------------
      +{
      +  const { enqueueSend } = createSendQueue();
      +  const order = [];
      +
      +  const a = enqueueSend(async () => {
      +    await new Promise(r => setTimeout(r, 30));
      +    order.push('a');
      +    return 'A';
      +  });
      +  const b = enqueueSend(async () => {
      +    order.push('b');
      +    return 'B';
      +  });
      +  const c = enqueueSend(async () => {
      +    await new Promise(r => setTimeout(r, 10));
      +    order.push('c');
      +    return 'C';
      +  });
      +
      +  const results = await Promise.all([a, b, c]);
      +  assert.deepStrictEqual(results, ['A', 'B', 'C'], 'all tasks resolve');
      +  assert.deepStrictEqual(order, ['a', 'b', 'c'], 'tasks execute in FIFO order');
      +  console.log('  ✓ serial ordering');
      +}
      +
      +// -- error isolation (one rejection does not stall the queue) --------
      +{
      +  const { enqueueSend } = createSendQueue();
      +  const order = [];
      +
      +  const bad = enqueueSend(async () => {
      +    order.push('bad');
      +    throw new Error('boom');
      +  });
      +  const good = enqueueSend(async () => {
      +    order.push('good');
      +    return 'ok';
      +  });
      +
      +  await assert.rejects(() => bad, /boom/, 'bad task rejects');
      +  const g = await good;
      +  assert.strictEqual(g, 'ok', 'good task still resolves');
      +  assert.deepStrictEqual(order, ['bad', 'good'], 'good runs after bad');
      +  console.log('  ✓ error isolation');
      +}
      +
      +// -- timeout still fires (wrapped inside enqueueSend) ----------------
      +{
      +  const { enqueueSend } = createSendQueue();
      +  const timedOut = enqueueSend(async () => {
      +    await new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 20));
      +  });
      +  await assert.rejects(() => timedOut, /timeout/, 'inner timeout propagates');
      +  console.log('  ✓ timeout propagation');
      +}
      +
      +// -- concurrent enqueues maintain single-consumer semantics ----------
      +{
      +  const { enqueueSend } = createSendQueue();
      +  let concurrent = 0;
      +  let maxConcurrent = 0;
      +
      +  async function tracked() {
      +    concurrent += 1;
      +    if (concurrent > maxConcurrent) maxConcurrent = concurrent;
      +    await new Promise(r => setTimeout(r, 5));
      +    concurrent -= 1;
      +  }
      +
      +  await Promise.all(Array.from({ length: 20 }, () => enqueueSend(tracked)));
      +  assert.strictEqual(maxConcurrent, 1, 'never more than one in-flight');
      +  assert.strictEqual(concurrent, 0, 'all finished');
      +  console.log('  ✓ single-consumer concurrency');
      +}
      +
      +console.log('\n✅ All send-queue tests passed.');

2 PRs closed without merging

From the close comments: closed without comment 1 · merged via another change 1

  • #33419 fix(whatsapp-bridge): serialize sendMessage to prevent cross-chat contamination (#33360) · closed without comment
  • #35503 fix(whatsapp-bridge): serialize sendMessage to prevent cross-chat contamination (#33360) · merged via another change → 0800f1c, #54062
Closed issues · 1
  • Closed#33360WhatsApp: Cross-chat response contamination — replies from one chat appear in another
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.50: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
gateway
Merged PRs
#54062
Closed issues
1
Attempts
2 PRs closed without merging
Fix link checked
28 Jun 2026

For your agent

The same facts, typed.