feat(channel): make message timeout configurable via channels_config.message_timeout_secs

Add configurable timeout for processing channel messages (LLM + tools).
Default: 300s (optimized for on-device LLMs like Ollama).
Can be overridden in config.toml:

[channels_config]
message_timeout_secs = 600
This commit is contained in:
ZeroClaw Contributor 2026-02-18 20:37:01 +03:00 committed by Chummy
parent 4ecaf6070c
commit 41a6ed30dd
4 changed files with 38 additions and 24 deletions

View file

@ -50,6 +50,10 @@ Notes:
Top-level channel options are configured under `channels_config`.
| Key | Default | Purpose |
|---|---|---|
| `message_timeout_secs` | `300` | Timeout in seconds for processing a single channel message (LLM + tools) |
Examples:
- `[channels_config.telegram]`
@ -57,6 +61,12 @@ Examples:
- `[channels_config.whatsapp]`
- `[channels_config.email]`
Notes:
- Default `300s` is optimized for on-device LLMs (Ollama) which are slower than cloud APIs.
- If using cloud APIs (OpenAI, Anthropic, etc.), you can reduce this to `60` or lower.
- When a timeout occurs, users receive: `⚠️ Request timed out while waiting for the model. Please try again.`
See detailed channel matrix and allowlist behavior in [channels-reference.md](channels-reference.md).
## Security-Relevant Defaults

View file

@ -60,8 +60,8 @@ const BOOTSTRAP_MAX_CHARS: usize = 20_000;
const DEFAULT_CHANNEL_INITIAL_BACKOFF_SECS: u64 = 2;
const DEFAULT_CHANNEL_MAX_BACKOFF_SECS: u64 = 60;
/// Timeout for processing a single channel message (LLM + tools).
/// 300s for on-device LLMs (Ollama) which are slower than cloud APIs.
/// Default timeout for processing a single channel message (LLM + tools).
/// Used as fallback when not configured in channels_config.message_timeout_secs.
const CHANNEL_MESSAGE_TIMEOUT_SECS: u64 = 300;
const CHANNEL_PARALLELISM_PER_CHANNEL: usize = 4;
const CHANNEL_MIN_IN_FLIGHT_MESSAGES: usize = 8;
@ -120,6 +120,7 @@ struct ChannelRuntimeContext {
reliability: Arc<crate::config::ReliabilityConfig>,
provider_runtime_options: providers::ProviderRuntimeOptions,
workspace_dir: Arc<PathBuf>,
message_timeout_secs: u64,
}
fn conversation_memory_key(msg: &traits::ChannelMessage) -> String {
@ -696,7 +697,7 @@ async fn process_channel_message(ctx: Arc<ChannelRuntimeContext>, msg: traits::C
};
let llm_result = tokio::time::timeout(
Duration::from_secs(CHANNEL_MESSAGE_TIMEOUT_SECS),
Duration::from_secs(ctx.message_timeout_secs),
run_tool_call_loop(
active_provider.as_ref(),
&mut history,
@ -787,10 +788,7 @@ async fn process_channel_message(ctx: Arc<ChannelRuntimeContext>, msg: traits::C
}
}
Err(_) => {
let timeout_msg = format!(
"LLM response timed out after {}s",
CHANNEL_MESSAGE_TIMEOUT_SECS
);
let timeout_msg = format!("LLM response timed out after {}s", ctx.message_timeout_secs);
eprintln!(
" ❌ {} (elapsed: {}ms)",
timeout_msg,
@ -1835,6 +1833,7 @@ pub async fn start_channels(config: Config) -> Result<()> {
reliability: Arc::new(config.reliability.clone()),
provider_runtime_options,
workspace_dir: Arc::new(config.workspace_dir.clone()),
message_timeout_secs: config.channels_config.message_timeout_secs,
});
run_message_dispatch_loop(rx, runtime_ctx, max_in_flight_messages).await;
@ -2225,6 +2224,7 @@ mod tests {
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,
});
process_channel_message(
@ -2277,6 +2277,7 @@ mod tests {
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,
});
process_channel_message(
@ -2338,6 +2339,7 @@ mod tests {
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,
});
process_channel_message(
@ -2420,6 +2422,7 @@ mod tests {
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,
});
process_channel_message(
@ -2478,6 +2481,7 @@ mod tests {
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,
});
process_channel_message(
@ -2531,6 +2535,7 @@ mod tests {
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,
});
process_channel_message(
@ -2635,6 +2640,7 @@ mod tests {
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,
});
let (tx, rx) = tokio::sync::mpsc::channel::<traits::ChannelMessage>(4);
@ -2705,6 +2711,7 @@ mod tests {
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,
});
process_channel_message(
@ -3095,6 +3102,7 @@ mod tests {
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,
});
process_channel_message(

View file

@ -1979,6 +1979,14 @@ pub struct ChannelsConfig {
pub lark: Option<LarkConfig>,
pub dingtalk: Option<DingTalkConfig>,
pub qq: Option<QQConfig>,
/// Timeout in seconds for processing a single channel message (LLM + tools).
/// Default: 300s for on-device LLMs (Ollama) which are slower than cloud APIs.
#[serde(default = "default_channel_message_timeout_secs")]
pub message_timeout_secs: u64,
}
fn default_channel_message_timeout_secs() -> u64 {
300
}
impl Default for ChannelsConfig {
@ -1999,6 +2007,7 @@ impl Default for ChannelsConfig {
lark: None,
dingtalk: None,
qq: None,
message_timeout_secs: default_channel_message_timeout_secs(),
}
}
}
@ -3242,6 +3251,7 @@ default_temperature = 0.7
lark: None,
dingtalk: None,
qq: None,
message_timeout_secs: 300,
},
memory: MemoryConfig::default(),
storage: StorageConfig::default(),
@ -3746,6 +3756,7 @@ allowed_users = ["@ops:matrix.org"]
lark: None,
dingtalk: None,
qq: None,
message_timeout_secs: 300,
};
let toml_str = toml::to_string_pretty(&c).unwrap();
let parsed: ChannelsConfig = toml::from_str(&toml_str).unwrap();
@ -3909,6 +3920,7 @@ channel_id = "C123"
lark: None,
dingtalk: None,
qq: None,
message_timeout_secs: 300,
};
let toml_str = toml::to_string_pretty(&c).unwrap();
let parsed: ChannelsConfig = toml::from_str(&toml_str).unwrap();

View file

@ -2452,23 +2452,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
print_bullet("CLI is always available. Connect more channels now.");
println!();
let mut config = ChannelsConfig {
cli: true,
telegram: None,
discord: None,
slack: None,
mattermost: None,
webhook: None,
imessage: None,
matrix: None,
signal: None,
whatsapp: None,
email: None,
irc: None,
lark: None,
dingtalk: None,
qq: None,
};
let mut config = ChannelsConfig::default();
loop {
let options = vec![