# NousResearch/hermes-agent · WhatsApp bridge cross-chat message contamination fix

## Open now

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

## TL;DR

- 4 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #54062. Verify it addresses your case.
- **Recommended:** read PR #54062 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-54062`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.50`: the share of member 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.

## Closed / duplicate issues

- #33360: WhatsApp: Cross-chat response contamination — replies from one chat appear in another

## Prior & competing PRs

- **#54062: fix(whatsapp-bridge): serialize sendMessage to prevent cross-chat contamination (#33360)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `infographic/whatsapp-send-queue/infographic.png`, `scripts/whatsapp-bridge/bridge.js`, `scripts/whatsapp-bridge/bridge.sendqueue.test.mjs`
  - fix hunks:
  - `scripts/whatsapp-bridge/bridge.js`

    ```diff
    @@ -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));
     }
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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.');
    ```
- **#33419: fix(whatsapp-bridge): serialize sendMessage to prevent cross-chat contamination (#33360)**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#35503: fix(whatsapp-bridge): serialize sendMessage to prevent cross-chat contamination (#33360)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `0800f1c28b80`, `#54062`

## Fix status

- Merged to main in PR #54062 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-28`.
- Subsystem: `gateway`
