fix: prevent multi-channel response routing race condition (#4530)

## Problem

When multiple channels (WhatsApp, iMessage, Telegram, etc.) are active,
responses intended for one channel could leak to another channel if
messages arrived simultaneously. This created serious privacy concerns.

**Root Cause:**
All channels were updating the SAME main session key with their delivery
context (channel, to, accountId). When messages arrived concurrently:

1. WhatsApp message arrives → updates session['agent:main:main'].lastChannel = 'whatsapp'
2. Agent starts processing (takes time)
3. iMessage message arrives → OVERWRITES session['agent:main:main'].lastChannel = 'imessage'
4. WhatsApp response delivers → reads session lastChannel = 'imessage'
5. Response goes to wrong channel!

## Solution

Changed all channel monitors to use the channel-specific session key
(route.sessionKey) instead of the shared main session key
(route.mainSessionKey) when updating delivery context.

Now each channel updates its own isolated session:
- WhatsApp: session['agent:main:whatsapp:dm:+1234']
- iMessage: session['agent:main:imessage:dm:alice']
- Telegram: session['agent:main:telegram:dm:12345']

This prevents cross-channel state clobbering.

## Changes

- src/discord/monitor/message-handler.process.ts
- src/imessage/monitor/monitor-provider.ts
- src/line/bot-message-context.ts
- src/signal/monitor/event-handler.ts
- src/slack/monitor/message-handler/dispatch.ts
- src/slack/monitor/message-handler/prepare.ts
- src/telegram/bot-message-context.ts
- src/web/auto-reply/monitor/process-message.ts

All changed from:
  sessionKey: route.mainSessionKey

To:
  sessionKey: route.sessionKey

## Testing

Manual testing needed:
1. Configure 2+ channels (e.g., WhatsApp + iMessage)
2. Send message to channel A
3. Immediately send message to channel B (within 100ms)
4. Verify responses go to correct channels

Fixes #4530
This commit is contained in:
spiceoogway 2026-01-30 08:42:22 -05:00
parent d2c1fd3c61
commit e31764fed1
8 changed files with 9 additions and 9 deletions

View File

@ -302,7 +302,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
ctx: ctxPayload,
updateLastRoute: isDirectMessage
? {
sessionKey: route.mainSessionKey,
sessionKey: route.sessionKey,
channel: "discord",
to: `user:${author.id}`,
accountId: route.accountId,

View File

@ -510,7 +510,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
updateLastRoute:
!isGroup && updateTarget
? {
sessionKey: route.mainSessionKey,
sessionKey: route.sessionKey,
channel: "imessage",
to: updateTarget,
accountId: route.accountId,

View File

@ -282,7 +282,7 @@ export async function buildLineMessageContext(params: BuildLineMessageContextPar
if (!isGroup) {
await updateLastRoute({
storePath,
sessionKey: route.mainSessionKey,
sessionKey: route.sessionKey,
deliveryContext: {
channel: "line",
to: userId ?? peerId,
@ -432,7 +432,7 @@ export async function buildLinePostbackContext(params: {
if (!isGroup) {
await updateLastRoute({
storePath,
sessionKey: route.mainSessionKey,
sessionKey: route.sessionKey,
deliveryContext: {
channel: "line",
to: userId ?? peerId,

View File

@ -156,7 +156,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
ctx: ctxPayload,
updateLastRoute: !entry.isGroup
? {
sessionKey: route.mainSessionKey,
sessionKey: route.sessionKey,
channel: "signal",
to: entry.senderRecipient,
accountId: route.accountId,

View File

@ -27,7 +27,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
});
await updateLastRoute({
storePath,
sessionKey: route.mainSessionKey,
sessionKey: route.sessionKey,
deliveryContext: {
channel: "slack",
to: `user:${message.user}`,

View File

@ -533,7 +533,7 @@ export async function prepareSlackMessage(params: {
ctx: ctxPayload,
updateLastRoute: isDirectMessage
? {
sessionKey: route.mainSessionKey,
sessionKey: route.sessionKey,
channel: "slack",
to: `user:${message.user}`,
accountId: route.accountId,

View File

@ -617,7 +617,7 @@ export const buildTelegramMessageContext = async ({
ctx: ctxPayload,
updateLastRoute: !isGroup
? {
sessionKey: route.mainSessionKey,
sessionKey: route.sessionKey,
channel: "telegram",
to: String(chatId),
accountId: route.accountId,

View File

@ -295,7 +295,7 @@ export async function processMessage(params: {
cfg: params.cfg,
backgroundTasks: params.backgroundTasks,
storeAgentId: params.route.agentId,
sessionKey: params.route.mainSessionKey,
sessionKey: params.route.sessionKey,
channel: "whatsapp",
to: dmRouteTarget,
accountId: params.route.accountId,