Skip to content

[Bug]: WhatsApp DM updateLastRoute overwrites mainSessionKey regardless of dmScope #24912

Description

@longevityboris

Version

2026.2.22-2

OS

macOS (Darwin 25.2.0)

Description

When session.dmScope is set to "per-channel-peer", WhatsApp DMs correctly resolve isolated session keys (e.g. agent:main:whatsapp:direct:+447513451349), but updateLastRouteInBackground unconditionally writes lastTo to the main session key (agent:main:main). This causes:

  1. Reply routing corruption: Every WhatsApp DM sender overwrites the main session's lastTo — subsequent auto-replies and heartbeats route to whoever messaged last ("last writer wins"), not the session owner
  2. Privacy violation: Agent replies intended for User A get delivered to User B if User B messaged more recently
  3. Heartbeat/cron misrouting: Heartbeats and cron announcements using channel: "last" resolve against the corrupted main session

This is the exact same bug that PR #13580 fixes for Telegram, but in the WhatsApp channel handler.

Expected Behavior

With dmScope: "per-channel-peer":

  • Each WhatsApp DM sender should get their own isolated session
  • updateLastRoute should not write to mainSessionKey when the DM session is isolated
  • Replies should route back to the original sender, not the last person who messaged

Actual Behavior

  • DM from User A → updateLastRoute writes lastTo=+userA to agent:main:main
  • DM from User B → updateLastRoute writes lastTo=+userB to agent:main:main
  • Agent reply to User A → resolves lastTo from main session → sends to User B (WRONG)
  • Heartbeat/cron → uses lastTo from main session → goes to User B instead of owner

Root Cause

In src/web/auto-reply/monitor/process-dm.ts (compiled: channel-web-pz2P6JD5.js line 1618-1628):

if (dmRouteTarget) updateLastRouteInBackground({
    cfg: params.cfg,
    backgroundTasks: params.backgroundTasks,
    storeAgentId: params.route.agentId,
    sessionKey: params.route.mainSessionKey,  // ← BUG: always writes to main session
    channel: "whatsapp",
    to: dmRouteTarget,
    accountId: params.route.accountId,
    ctx: ctxPayload,
    warn: params.replyLogger.warn.bind(params.replyLogger)
});

params.route.mainSessionKey is always agent:main:main. When dmScope isolates DMs, params.route.sessionKey differs from mainSessionKey, but updateLastRoute still writes to the main session unconditionally.

Proposed Fix

Same pattern as PR #13580 (Telegram fix). Gate updateLastRoute on session key equality:

// Only update main session's lastRoute when DM actually IS the main session
if (dmRouteTarget && params.route.sessionKey === params.route.mainSessionKey) {
    updateLastRouteInBackground({
        cfg: params.cfg,
        backgroundTasks: params.backgroundTasks,
        storeAgentId: params.route.agentId,
        sessionKey: params.route.mainSessionKey,
        channel: "whatsapp",
        to: dmRouteTarget,
        accountId: params.route.accountId,
        ctx: ctxPayload,
        warn: params.replyLogger.warn.bind(params.replyLogger)
    });
}

When dmScope="main" (default): sessionKey === mainSessionKeyupdateLastRoute fires as before. No behavior change.
When dmScope="per-channel-peer": sessionKey !== mainSessionKeyupdateLastRoute is skipped, preventing main session corruption.

Additional Context

There may also be additional updateLastRoute call sites in the broader codebase (subagent-registry-BCf5fWt9.js has 10+ calls) that need the same guard. A comprehensive audit of all updateLastRoute callers for dmScope-awareness would prevent the same bug in other channel handlers.

Reproduction

  1. Set session.dmScope: "per-channel-peer" in openclaw.json
  2. Have two different WhatsApp numbers DM the agent
  3. Observe that sessions.json shows lastTo on agent:main:main flipping between senders
  4. Observe replies routing to the wrong sender

Steps Already Verified

  • Config hot-reload applies session.dmScope successfully (confirmed in gateway logs)
  • resolveAgentRoute correctly reads dmScope and passes it to buildAgentPeerSessionKey
  • buildAgentPeerSessionKey correctly produces per-peer session keys when dmScope="per-channel-peer" and peerId is set
  • The bug is specifically in updateLastRouteInBackground unconditionally targeting mainSessionKey

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions