From ecbef64a7cd945180ae27d16619affbd4a4ab01b Mon Sep 17 00:00:00 2001 From: Tim <37033036+TimStewartJ@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:58:06 -0700 Subject: [PATCH] fix(channels): include reply_target in conversation history key (#2891) conversation_history_key() now includes reply_target to isolate conversation histories across distinct Discord/Slack/Mattermost channels for the same sender. Previously all channels produced the same key {channel}_{sender}, causing cross-channel context bleed. New key format: {channel}_{reply_target}_{sender} (without thread) or {channel}_{reply_target}_{thread_ts}_{sender} (with thread). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/channels/mod.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/channels/mod.rs b/src/channels/mod.rs index 78dcf411f..be2e84af0 100644 --- a/src/channels/mod.rs +++ b/src/channels/mod.rs @@ -394,10 +394,13 @@ fn conversation_memory_key(msg: &traits::ChannelMessage) -> String { } fn conversation_history_key(msg: &traits::ChannelMessage) -> String { - // Include thread_ts for per-topic session isolation in forum groups + // Include reply_target for per-channel isolation (e.g. distinct Discord/Slack + // channels) and thread_ts for per-topic isolation in forum groups. match &msg.thread_ts { - Some(tid) => format!("{}_{}_{}", msg.channel, tid, msg.sender), - None => format!("{}_{}", msg.channel, msg.sender), + Some(tid) => format!( + "{}_{}_{}_{}", msg.channel, msg.reply_target, tid, msg.sender + ), + None => format!("{}_{}_{}", msg.channel, msg.reply_target, msg.sender), } } @@ -5695,7 +5698,7 @@ BTC is currently around $65,000 based on latest tool output."# .lock() .unwrap_or_else(|e| e.into_inner()); let turns = histories - .get("telegram_alice") + .get("telegram_chat-telegram_alice") .expect("telegram history should be stored"); let assistant_turn = turns .iter() @@ -5948,7 +5951,7 @@ BTC is currently around $65,000 based on latest tool output."# assert_eq!(sent.len(), 1); assert!(sent[0].contains("Provider switched to `openrouter`")); - let route_key = "telegram_alice"; + let route_key = "telegram_chat-1_alice"; let route = runtime_ctx .route_overrides .lock() @@ -5980,7 +5983,7 @@ BTC is currently around $65,000 based on latest tool output."# provider_cache_seed.insert("test-provider".to_string(), Arc::clone(&default_provider)); provider_cache_seed.insert("openrouter".to_string(), routed_provider); - let route_key = "telegram_alice".to_string(); + let route_key = "telegram_chat-1_alice".to_string(); let mut route_overrides = HashMap::new(); route_overrides.insert( route_key, @@ -8183,7 +8186,7 @@ BTC is currently around $65,000 based on latest tool output."# .lock() .unwrap_or_else(|e| e.into_inner()); let turns = histories - .get("test-channel_alice") + .get("test-channel_chat-ctx_alice") .expect("history should be stored for sender"); assert_eq!(turns[0].role, "user"); assert_eq!(turns[0].content, "hello"); @@ -8201,7 +8204,7 @@ BTC is currently around $65,000 based on latest tool output."# let provider_impl = Arc::new(HistoryCaptureProvider::default()); let mut histories = HashMap::new(); histories.insert( - "telegram_alice".to_string(), + "telegram_chat-telegram_alice".to_string(), vec![ ChatMessage::assistant("stale assistant"), ChatMessage::user("earlier user question"), @@ -8960,7 +8963,7 @@ This is an example JSON object for profile settings."#; .lock() .unwrap_or_else(|e| e.into_inner()); let turns = histories - .get("test-channel_zeroclaw_user") + .get("test-channel_chat-photo_zeroclaw_user") .expect("history should exist for sender"); assert_eq!(turns.len(), 2); assert_eq!(turns[0].role, "user");