feat(channel): add interrupt_on_new_message support for Mattermost

This commit is contained in:
Nim G 2026-03-18 16:40:25 -03:00 committed by Roman Tataurov
parent c1095de1ac
commit 2aaf916185
No known key found for this signature in database
GPG Key ID: 70A51EF3185C334B
4 changed files with 152 additions and 0 deletions

View File

@ -291,6 +291,7 @@ const OPENRC_RESTART_ARGS: [&str; 2] = ["zeroclaw", "restart"];
struct InterruptOnNewMessageConfig {
telegram: bool,
slack: bool,
mattermost: bool,
}
impl InterruptOnNewMessageConfig {
@ -298,6 +299,7 @@ impl InterruptOnNewMessageConfig {
match channel {
"telegram" => self.telegram,
"slack" => self.slack,
"mattermost" => self.mattermost,
_ => false,
}
}
@ -4383,6 +4385,11 @@ pub async fn start_channels(config: Config) -> Result<()> {
.slack
.as_ref()
.is_some_and(|sl| sl.interrupt_on_new_message);
let interrupt_on_new_message_mattermost = config
.channels_config
.mattermost
.as_ref()
.is_some_and(|mm| mm.interrupt_on_new_message);
let runtime_ctx = Arc::new(ChannelRuntimeContext {
channels_by_name,
@ -4411,6 +4418,7 @@ pub async fn start_channels(config: Config) -> Result<()> {
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: interrupt_on_new_message,
slack: interrupt_on_new_message_slack,
mattermost: interrupt_on_new_message_mattermost,
},
multimodal: config.multimodal.clone(),
hooks: if config.hooks.enabled {
@ -4727,6 +4735,7 @@ mod tests {
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -4839,6 +4848,7 @@ mod tests {
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -4907,6 +4917,7 @@ mod tests {
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -4994,6 +5005,7 @@ mod tests {
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -5502,6 +5514,80 @@ BTC is currently around $65,000 based on latest tool output."#
#[tokio::test]
async fn process_channel_message_executes_tool_calls_instead_of_sending_raw_json() {
let channel_impl = Arc::new(RecordingChannel::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(ToolCallingProvider),
default_provider: Arc::new("test-provider".to_string()),
memory: Arc::new(NoopMemory),
tools_registry: Arc::new(vec![Box::new(MockPriceTool)]),
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())),
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()),
message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS,
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
non_cli_excluded_tools: Arc::new(Vec::new()),
tool_call_dedup_exempt: Arc::new(Vec::new()),
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
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,
});
process_channel_message(
runtime_ctx,
traits::ChannelMessage {
id: "msg-1".to_string(),
sender: "alice".to_string(),
reply_target: "chat-42".to_string(),
content: "What is the BTC price now?".to_string(),
channel: "test-channel".to_string(),
timestamp: 1,
thread_ts: None,
},
CancellationToken::new(),
)
.await;
let sent_messages = channel_impl.sent_messages.lock().await;
assert!(!sent_messages.is_empty());
let reply = sent_messages.last().unwrap();
assert!(reply.starts_with("chat-42:"));
assert!(reply.contains("BTC is currently around"));
assert!(!reply.contains("\"tool_calls\""));
assert!(!reply.contains("mock_price"));
}
#[tokio::test]
async fn process_channel_message_telegram_does_not_persist_tool_summary_prefix() {
let channel_impl = Arc::new(TelegramRecordingChannel::default());
let channel: Arc<dyn Channel> = channel_impl.clone();
@ -5535,6 +5621,7 @@ BTC is currently around $65,000 based on latest tool output."#
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
non_cli_excluded_tools: Arc::new(Vec::new()),
autonomy_level: AutonomyLevel::default(),
@ -5625,6 +5712,7 @@ BTC is currently around $65,000 based on latest tool output."#
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -5700,6 +5788,7 @@ BTC is currently around $65,000 based on latest tool output."#
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -5785,6 +5874,7 @@ BTC is currently around $65,000 based on latest tool output."#
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -5891,6 +5981,7 @@ BTC is currently around $65,000 based on latest tool output."#
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -5978,6 +6069,7 @@ BTC is currently around $65,000 based on latest tool output."#
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -6080,6 +6172,7 @@ BTC is currently around $65,000 based on latest tool output."#
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -6167,6 +6260,7 @@ BTC is currently around $65,000 based on latest tool output."#
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -6244,6 +6338,7 @@ BTC is currently around $65,000 based on latest tool output."#
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -6432,6 +6527,7 @@ BTC is currently around $65,000 based on latest tool output."#
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -6528,6 +6624,7 @@ BTC is currently around $65,000 based on latest tool output."#
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: true,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -6639,6 +6736,7 @@ BTC is currently around $65,000 based on latest tool output."#
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: true,
mattermost: false,
},
ack_reactions: true,
show_tool_calls: true,
@ -6747,6 +6845,7 @@ BTC is currently around $65,000 based on latest tool output."#
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: true,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -6837,6 +6936,7 @@ BTC is currently around $65,000 based on latest tool output."#
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -6912,6 +7012,7 @@ BTC is currently around $65,000 based on latest tool output."#
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -7663,6 +7764,7 @@ BTC is currently around $65,000 based on latest tool output."#
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -7952,6 +8054,7 @@ BTC is currently around $65,000 based on latest tool output."#
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -8053,6 +8156,7 @@ BTC is currently around $65,000 based on latest tool output."#
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -8383,6 +8487,7 @@ This is an example JSON object for profile settings."#;
allowed_users: vec![],
thread_replies: Some(true),
mention_only: Some(false),
interrupt_on_new_message: false,
});
let channels = collect_configured_channels(&config, "test");
@ -8618,6 +8723,7 @@ This is an example JSON object for profile settings."#;
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -8700,6 +8806,7 @@ This is an example JSON object for profile settings."#;
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -8856,6 +8963,7 @@ This is an example JSON object for profile settings."#;
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -8962,6 +9070,7 @@ This is an example JSON object for profile settings."#;
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -9060,6 +9169,7 @@ This is an example JSON object for profile settings."#;
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -9178,6 +9288,7 @@ This is an example JSON object for profile settings."#;
interrupt_on_new_message: InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
},
multimodal: crate::config::MultimodalConfig::default(),
hooks: None,
@ -9299,4 +9410,24 @@ This is an example JSON object for profile settings."#;
assert!(!is_stop_command("/stopwatch"));
assert!(!is_stop_command("/stop-all"));
}
#[test]
fn interrupt_on_new_message_enabled_for_mattermost_when_true() {
let cfg = InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: true,
};
assert!(cfg.enabled_for_channel("mattermost"));
}
#[test]
fn interrupt_on_new_message_disabled_for_mattermost_by_default() {
let cfg = InterruptOnNewMessageConfig {
telegram: false,
slack: false,
mattermost: false,
};
assert!(!cfg.enabled_for_channel("mattermost"));
}
}

View File

@ -4731,6 +4731,10 @@ pub struct MattermostConfig {
/// Other messages in the channel are silently ignored.
#[serde(default)]
pub mention_only: Option<bool>,
/// When true, a newer Mattermost message from the same sender in the same channel
/// cancels the in-flight request and starts a fresh response with preserved history.
#[serde(default)]
pub interrupt_on_new_message: bool,
}
impl ChannelConfig for MattermostConfig {
@ -9713,6 +9717,21 @@ channel_id = "C123"
assert_eq!(parsed.channel_id.as_deref(), Some("C123"));
}
#[test]
async fn mattermost_config_default_interrupt_on_new_message_is_false() {
let json = r#"{"url":"https://mm.example.com","bot_token":"tok"}"#;
let parsed: MattermostConfig = serde_json::from_str(json).unwrap();
assert!(!parsed.interrupt_on_new_message);
}
#[test]
async fn mattermost_config_deserializes_interrupt_on_new_message_true() {
let json =
r#"{"url":"https://mm.example.com","bot_token":"tok","interrupt_on_new_message":true}"#;
let parsed: MattermostConfig = serde_json::from_str(json).unwrap();
assert!(parsed.interrupt_on_new_message);
}
#[test]
async fn webhook_config_with_secret() {
let json = r#"{"port":8080,"secret":"my-secret-key"}"#;

View File

@ -671,6 +671,7 @@ mod tests {
allowed_users: vec!["*".into()],
thread_replies: Some(true),
mention_only: Some(false),
interrupt_on_new_message: false,
});
assert!(has_supervised_channels(&config));
}

View File

@ -7371,6 +7371,7 @@ mod tests {
allowed_users: vec!["*".into()],
thread_replies: Some(true),
mention_only: Some(false),
interrupt_on_new_message: false,
});
assert!(has_launchable_channels(&channels));