fix(channel): add interruption_scope_id for thread-aware cancellation scoping (#4017)

Add interruption_scope_id to ChannelMessage for thread-aware cancellation. Slack genuine thread replies and Matrix threads get scoped keys, preventing cross-thread cancellation. All other channels preserve existing behavior.

Supersedes #3900. Depends on #3891.
This commit is contained in:
Argenis
2026-03-19 21:34:04 -04:00
committed by Roman Tataurov
parent 5c898246ff
commit a198b26594
30 changed files with 281 additions and 2 deletions
+1
View File
@@ -251,6 +251,7 @@ impl BlueskyChannel {
channel: "bluesky".to_string(),
timestamp,
thread_ts: Some(notif.uri.clone()),
interruption_scope_id: None,
})
}
+3
View File
@@ -48,6 +48,7 @@ impl Channel for CliChannel {
.unwrap_or_default()
.as_secs(),
thread_ts: None,
interruption_scope_id: None,
};
if tx.send(msg).await.is_err() {
@@ -111,6 +112,7 @@ mod tests {
channel: "cli".into(),
timestamp: 1_234_567_890,
thread_ts: None,
interruption_scope_id: None,
};
assert_eq!(msg.id, "test-id");
assert_eq!(msg.sender, "user");
@@ -130,6 +132,7 @@ mod tests {
channel: "ch".into(),
timestamp: 0,
thread_ts: None,
interruption_scope_id: None,
};
let cloned = msg.clone();
assert_eq!(cloned.id, msg.id);
+1
View File
@@ -275,6 +275,7 @@ impl Channel for DingTalkChannel {
.unwrap_or_default()
.as_secs(),
thread_ts: None,
interruption_scope_id: None,
};
if tx.send(channel_msg).await.is_err() {
+1
View File
@@ -789,6 +789,7 @@ impl Channel for DiscordChannel {
.unwrap_or_default()
.as_secs(),
thread_ts: None,
interruption_scope_id: None,
};
if tx.send(channel_msg).await.is_err() {
+1
View File
@@ -467,6 +467,7 @@ impl EmailChannel {
channel: "email".to_string(),
timestamp: email.timestamp,
thread_ts: None,
interruption_scope_id: None,
};
if tx.send(msg).await.is_err() {
+1
View File
@@ -294,6 +294,7 @@ end tell"#
.unwrap_or_default()
.as_secs(),
thread_ts: None,
interruption_scope_id: None,
};
if tx.send(msg).await.is_err() {
+1
View File
@@ -580,6 +580,7 @@ impl Channel for IrcChannel {
.unwrap_or_default()
.as_secs(),
thread_ts: None,
interruption_scope_id: None,
};
if tx.send(channel_msg).await.is_err() {
+2
View File
@@ -823,6 +823,7 @@ impl LarkChannel {
.unwrap_or_default()
.as_secs(),
thread_ts: None,
interruption_scope_id: None,
};
tracing::debug!("Lark WS: message in {}", lark_msg.chat_id);
@@ -1120,6 +1121,7 @@ impl LarkChannel {
channel: self.channel_name().to_string(),
timestamp,
thread_ts: None,
interruption_scope_id: None,
});
messages
+1
View File
@@ -267,6 +267,7 @@ impl LinqChannel {
channel: "linq".to_string(),
timestamp,
thread_ts: None,
interruption_scope_id: None,
});
messages
+2 -1
View File
@@ -893,7 +893,8 @@ impl Channel for MatrixChannel {
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
thread_ts,
thread_ts: thread_ts.clone(),
interruption_scope_id: thread_ts,
};
let _ = tx.send(msg).await;
+1
View File
@@ -322,6 +322,7 @@ impl MattermostChannel {
#[allow(clippy::cast_sign_loss)]
timestamp: (create_at / 1000) as u64,
thread_ts: None,
interruption_scope_id: None,
})
}
}
+1
View File
@@ -198,6 +198,7 @@ impl Channel for MochatChannel {
.unwrap_or_default()
.as_secs(),
thread_ts: None,
interruption_scope_id: None,
};
if tx.send(channel_msg).await.is_err() {
+195 -1
View File
@@ -406,7 +406,13 @@ fn followup_thread_id(msg: &traits::ChannelMessage) -> Option<String> {
}
fn interruption_scope_key(msg: &traits::ChannelMessage) -> String {
format!("{}_{}_{}", msg.channel, msg.reply_target, msg.sender)
match &msg.interruption_scope_id {
Some(scope) => format!(
"{}_{}_{}_{}",
msg.channel, msg.reply_target, msg.sender, scope
),
None => format!("{}_{}_{}", msg.channel, msg.reply_target, msg.sender),
}
}
/// Returns `true` when `content` is a `/stop` command (with optional `@botname` suffix).
@@ -5586,6 +5592,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "test-channel".to_string(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -5664,6 +5671,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "telegram".to_string(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -5756,6 +5764,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "test-channel".to_string(),
timestamp: 3,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -5833,6 +5842,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "test-channel".to_string(),
timestamp: 2,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -5920,6 +5930,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "telegram".to_string(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -6028,6 +6039,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "telegram".to_string(),
timestamp: 2,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -6117,6 +6129,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "telegram".to_string(),
timestamp: 3,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -6221,6 +6234,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "telegram".to_string(),
timestamp: 4,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -6310,6 +6324,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "test-channel".to_string(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -6389,6 +6404,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "test-channel".to_string(),
timestamp: 2,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -6578,6 +6594,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "test-channel".to_string(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
})
.await
.unwrap();
@@ -6589,6 +6606,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "test-channel".to_string(),
timestamp: 2,
thread_ts: None,
interruption_scope_id: None,
})
.await
.unwrap();
@@ -6677,6 +6695,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "telegram".to_string(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
})
.await
.unwrap();
@@ -6689,6 +6708,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "telegram".to_string(),
timestamp: 2,
thread_ts: None,
interruption_scope_id: None,
})
.await
.unwrap();
@@ -6790,6 +6810,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "slack".to_string(),
timestamp: 1,
thread_ts: Some("1741234567.100001".to_string()),
interruption_scope_id: Some("1741234567.100001".to_string()),
})
.await
.unwrap();
@@ -6802,6 +6823,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "slack".to_string(),
timestamp: 2,
thread_ts: Some("1741234567.100001".to_string()),
interruption_scope_id: Some("1741234567.100001".to_string()),
})
.await
.unwrap();
@@ -6900,6 +6922,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "telegram".to_string(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
})
.await
.unwrap();
@@ -6912,6 +6935,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "telegram".to_string(),
timestamp: 2,
thread_ts: None,
interruption_scope_id: None,
})
.await
.unwrap();
@@ -6992,6 +7016,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "test-channel".to_string(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -7069,6 +7094,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "test-channel".to_string(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -7590,6 +7616,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "slack".into(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
};
assert_eq!(conversation_memory_key(&msg), "slack_U123_msg_abc123");
@@ -7605,6 +7632,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "slack".into(),
timestamp: 1,
thread_ts: Some("1741234567.123456".into()),
interruption_scope_id: None,
};
assert_eq!(
@@ -7623,6 +7651,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "cli".into(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
};
assert_eq!(followup_thread_id(&msg).as_deref(), Some("msg_abc123"));
@@ -7638,6 +7667,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "slack".into(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
};
let msg2 = traits::ChannelMessage {
id: "msg_2".into(),
@@ -7647,6 +7677,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "slack".into(),
timestamp: 2,
thread_ts: None,
interruption_scope_id: None,
};
assert_ne!(
@@ -7668,6 +7699,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "slack".into(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
};
let msg2 = traits::ChannelMessage {
id: "msg_2".into(),
@@ -7677,6 +7709,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "slack".into(),
timestamp: 2,
thread_ts: None,
interruption_scope_id: None,
};
mem.store(
@@ -7822,6 +7855,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "test-channel".to_string(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -7837,6 +7871,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "test-channel".to_string(),
timestamp: 2,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -7949,6 +7984,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "telegram".to_string(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -7980,6 +8016,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "telegram".to_string(),
timestamp: 2,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -8017,6 +8054,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "telegram".to_string(),
timestamp: 3,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -8115,6 +8153,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "test-channel".to_string(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -8218,6 +8257,7 @@ BTC is currently around $65,000 based on latest tool output."#
channel: "telegram".to_string(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -8787,6 +8827,7 @@ This is an example JSON object for profile settings."#;
channel: "test-channel".to_string(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -8870,6 +8911,7 @@ This is an example JSON object for profile settings."#;
channel: "test-channel".to_string(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -8885,6 +8927,7 @@ This is an example JSON object for profile settings."#;
channel: "test-channel".to_string(),
timestamp: 2,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -9028,6 +9071,7 @@ This is an example JSON object for profile settings."#;
channel: "telegram".to_string(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -9136,6 +9180,7 @@ This is an example JSON object for profile settings."#;
channel: "telegram".to_string(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -9236,6 +9281,7 @@ This is an example JSON object for profile settings."#;
channel: "telegram".to_string(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -9356,6 +9402,7 @@ This is an example JSON object for profile settings."#;
channel: "telegram".to_string(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
},
CancellationToken::new(),
)
@@ -9494,4 +9541,151 @@ This is an example JSON object for profile settings."#;
};
assert!(!cfg.enabled_for_channel("discord"));
}
// ── interruption_scope_key tests ──────────────────────────────────────
#[test]
fn interruption_scope_key_without_scope_id_is_three_component() {
let msg = traits::ChannelMessage {
id: "1".into(),
sender: "alice".into(),
reply_target: "room".into(),
content: "hi".into(),
channel: "matrix".into(),
timestamp: 0,
thread_ts: None,
interruption_scope_id: None,
};
assert_eq!(interruption_scope_key(&msg), "matrix_room_alice");
}
#[test]
fn interruption_scope_key_with_scope_id_is_four_component() {
let msg = traits::ChannelMessage {
id: "1".into(),
sender: "alice".into(),
reply_target: "room".into(),
content: "hi".into(),
channel: "matrix".into(),
timestamp: 0,
thread_ts: Some("$thread1".into()),
interruption_scope_id: Some("$thread1".into()),
};
assert_eq!(interruption_scope_key(&msg), "matrix_room_alice_$thread1");
}
#[test]
fn interruption_scope_key_thread_ts_alone_does_not_affect_key() {
// thread_ts used for reply anchoring should not bleed into scope key
let msg = traits::ChannelMessage {
id: "1".into(),
sender: "alice".into(),
reply_target: "C123".into(),
content: "hi".into(),
channel: "slack".into(),
timestamp: 0,
thread_ts: Some("1234567890.000100".into()), // Slack top-level fallback
interruption_scope_id: None, // but NOT a thread reply
};
assert_eq!(interruption_scope_key(&msg), "slack_C123_alice");
}
#[tokio::test]
async fn message_dispatch_different_threads_do_not_cancel_each_other() {
let channel_impl = Arc::new(SlackRecordingChannel::default());
let channel: Arc<dyn Channel> = channel_impl.clone();
let mut channels_by_name = HashMap::new();
channels_by_name.insert(channel.name().to_string(), channel);
let runtime_ctx = Arc::new(ChannelRuntimeContext {
channels_by_name: Arc::new(channels_by_name),
provider: Arc::new(SlowProvider {
delay: Duration::from_millis(150),
}),
default_provider: Arc::new("test-provider".to_string()),
memory: Arc::new(NoopMemory),
tools_registry: Arc::new(vec![]),
observer: Arc::new(NoopObserver),
system_prompt: Arc::new("test-system-prompt".to_string()),
model: Arc::new("test-model".to_string()),
temperature: 0.0,
auto_save_memory: false,
max_tool_iterations: 10,
min_relevance_score: 0.0,
conversation_histories: Arc::new(Mutex::new(HashMap::new())),
pending_new_sessions: Arc::new(Mutex::new(HashSet::new())),
provider_cache: Arc::new(Mutex::new(HashMap::new())),
route_overrides: Arc::new(Mutex::new(HashMap::new())),
api_key: None,
api_url: None,
reliability: Arc::new(crate::config::ReliabilityConfig::default()),
provider_runtime_options: providers::ProviderRuntimeOptions::default(),
workspace_dir: Arc::new(std::env::temp_dir()),
prompt_config: Arc::new(crate::config::Config::default()),
message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS,
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: true,
discord: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
non_cli_excluded_tools: Arc::new(Vec::new()),
autonomy_level: AutonomyLevel::default(),
tool_call_dedup_exempt: Arc::new(Vec::new()),
model_routes: Arc::new(Vec::new()),
query_classification: crate::config::QueryClassificationConfig::default(),
ack_reactions: true,
show_tool_calls: true,
session_store: None,
approval_manager: Arc::new(ApprovalManager::for_non_interactive(
&crate::config::AutonomyConfig::default(),
)),
activated_tools: None,
});
let (tx, rx) = tokio::sync::mpsc::channel::<traits::ChannelMessage>(8);
let send_task = tokio::spawn(async move {
// Two messages from same sender but in different Slack threads —
// they must NOT cancel each other.
tx.send(traits::ChannelMessage {
id: "1741234567.100001".to_string(),
sender: "alice".to_string(),
reply_target: "C123".to_string(),
content: "thread-a question".to_string(),
channel: "slack".to_string(),
timestamp: 1,
thread_ts: Some("1741234567.100001".to_string()),
interruption_scope_id: Some("1741234567.100001".to_string()),
})
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(30)).await;
tx.send(traits::ChannelMessage {
id: "1741234567.200002".to_string(),
sender: "alice".to_string(),
reply_target: "C123".to_string(),
content: "thread-b question".to_string(),
channel: "slack".to_string(),
timestamp: 2,
thread_ts: Some("1741234567.200002".to_string()),
interruption_scope_id: Some("1741234567.200002".to_string()),
})
.await
.unwrap();
});
run_message_dispatch_loop(rx, runtime_ctx, 4).await;
send_task.await.unwrap();
// Both tasks should have completed — different threads, no cancellation.
let sent_messages = channel_impl.sent_messages.lock().await;
assert_eq!(
sent_messages.len(),
2,
"both Slack thread messages should complete, got: {sent_messages:?}"
);
}
}
+2
View File
@@ -193,6 +193,7 @@ impl NextcloudTalkChannel {
channel: "nextcloud_talk".to_string(),
timestamp: Self::now_unix_secs(),
thread_ts: None,
interruption_scope_id: None,
});
messages
@@ -294,6 +295,7 @@ impl NextcloudTalkChannel {
channel: "nextcloud_talk".to_string(),
timestamp,
thread_ts: None,
interruption_scope_id: None,
});
messages
+1
View File
@@ -253,6 +253,7 @@ impl Channel for NostrChannel {
channel: "nostr".to_string(),
timestamp,
thread_ts: None,
interruption_scope_id: None,
};
if tx.send(msg).await.is_err() {
tracing::info!("Nostr listener: message bus closed, stopping");
+1
View File
@@ -360,6 +360,7 @@ impl Channel for NotionChannel {
channel: "notion".into(),
timestamp,
thread_ts: None,
interruption_scope_id: None,
})
.await
.is_err()
+2
View File
@@ -465,6 +465,7 @@ impl Channel for QQChannel {
.unwrap_or_default()
.as_secs(),
thread_ts: None,
interruption_scope_id: None,
};
if tx.send(channel_msg).await.is_err() {
@@ -503,6 +504,7 @@ impl Channel for QQChannel {
.unwrap_or_default()
.as_secs(),
thread_ts: None,
interruption_scope_id: None,
};
if tx.send(channel_msg).await.is_err() {
+1
View File
@@ -225,6 +225,7 @@ impl RedditChannel {
channel: "reddit".to_string(),
timestamp,
thread_ts: item.parent_id.clone(),
interruption_scope_id: None,
})
}
}
+1
View File
@@ -266,6 +266,7 @@ impl SignalChannel {
channel: "signal".to_string(),
timestamp: timestamp / 1000, // millis → secs
thread_ts: None,
interruption_scope_id: None,
})
}
}
+20
View File
@@ -165,6 +165,23 @@ impl SlackChannel {
.map(str::to_string)
}
/// Returns the interruption scope identifier for a Slack message.
///
/// Returns `Some(thread_ts)` only when the message is a genuine thread reply
/// (Slack's `thread_ts` field is present and differs from the message's own `ts`).
/// Returns `None` for top-level messages and thread parent messages (where
/// `thread_ts == ts`), placing them in the 3-component scope key
/// (`channel_reply_target_sender`).
///
/// Intentional: top-level messages and threaded replies are separate conversational
/// scopes and should not cancel each other's in-flight tasks.
fn inbound_interruption_scope_id(msg: &serde_json::Value, ts: &str) -> Option<String> {
msg.get("thread_ts")
.and_then(|t| t.as_str())
.filter(|&t| t != ts)
.map(str::to_string)
}
fn normalized_channel_id(input: Option<&str>) -> Option<String> {
input
.map(str::trim)
@@ -1792,6 +1809,7 @@ impl SlackChannel {
.unwrap_or_default()
.as_secs(),
thread_ts: Self::inbound_thread_ts(event, ts),
interruption_scope_id: Self::inbound_interruption_scope_id(event, ts),
};
if tx.send(channel_msg).await.is_err() {
@@ -2356,6 +2374,7 @@ impl Channel for SlackChannel {
.unwrap_or_default()
.as_secs(),
thread_ts: Self::inbound_thread_ts(msg, ts),
interruption_scope_id: Self::inbound_interruption_scope_id(msg, ts),
};
if tx.send(channel_msg).await.is_err() {
@@ -2440,6 +2459,7 @@ impl Channel for SlackChannel {
.unwrap_or_default()
.as_secs(),
thread_ts: Some(thread_ts.clone()),
interruption_scope_id: Some(thread_ts.clone()),
};
if tx.send(channel_msg).await.is_err() {
+3
View File
@@ -1142,6 +1142,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
.unwrap_or_default()
.as_secs(),
thread_ts: thread_id,
interruption_scope_id: None,
})
}
@@ -1264,6 +1265,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
.unwrap_or_default()
.as_secs(),
thread_ts: thread_id,
interruption_scope_id: None,
})
}
@@ -1425,6 +1427,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
.unwrap_or_default()
.as_secs(),
thread_ts: thread_id,
interruption_scope_id: None,
})
}
+7
View File
@@ -12,6 +12,11 @@ pub struct ChannelMessage {
/// Platform thread identifier (e.g. Slack `ts`, Discord thread ID).
/// When set, replies should be posted as threaded responses.
pub thread_ts: Option<String>,
/// Thread scope identifier for interruption/cancellation grouping.
/// Distinct from `thread_ts` (reply anchor): this is `Some` only when the message
/// is genuinely inside a reply thread and should be isolated from other threads.
/// `None` means top-level — scope is sender+channel only.
pub interruption_scope_id: Option<String>,
}
/// Message to send through a channel
@@ -182,6 +187,7 @@ mod tests {
channel: "dummy".into(),
timestamp: 123,
thread_ts: None,
interruption_scope_id: None,
})
.await
.map_err(|e| anyhow::anyhow!(e.to_string()))
@@ -198,6 +204,7 @@ mod tests {
channel: "dummy".into(),
timestamp: 999,
thread_ts: None,
interruption_scope_id: None,
};
let cloned = message.clone();
+1
View File
@@ -288,6 +288,7 @@ impl Channel for TwitterChannel {
.get("conversation_id")
.and_then(|c| c.as_str())
.map(|s| s.to_string()),
interruption_scope_id: None,
};
if tx.send(channel_msg).await.is_err() {
+1
View File
@@ -163,6 +163,7 @@ impl WatiChannel {
channel: "wati".to_string(),
timestamp,
thread_ts: None,
interruption_scope_id: None,
});
messages
+1
View File
@@ -237,6 +237,7 @@ impl Channel for WebhookChannel {
channel: "webhook".to_string(),
timestamp,
thread_ts: payload.thread_id,
interruption_scope_id: None,
};
if state.tx.send(msg).await.is_err() {
+1
View File
@@ -142,6 +142,7 @@ impl WhatsAppChannel {
channel: "whatsapp".to_string(),
timestamp,
thread_ts: None,
interruption_scope_id: None,
});
}
}
+1
View File
@@ -741,6 +741,7 @@ impl Channel for WhatsAppWebChannel {
content,
timestamp: chrono::Utc::now().timestamp() as u64,
thread_ts: None,
interruption_scope_id: None,
})
.await
{
+1
View File
@@ -2173,6 +2173,7 @@ mod tests {
channel: "whatsapp".into(),
timestamp: 1,
thread_ts: None,
interruption_scope_id: None,
};
let key = whatsapp_memory_key(&msg);