Created
March 12, 2026 13:28
-
-
Save hypnguyen1209/46d5677b49b8bfe022031b78a34448ac to your computer and use it in GitHub Desktop.
[PATCH] fix: cron tools and panic slice string utf-8 - zeroclaw v0.1.9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| From 289cd41c289efa98e40ee715314af43bdc62a198 Mon Sep 17 00:00:00 2001 | |
| From: hypnguyen1209 <haha@troller.vn> | |
| Date: Thu, 12 Mar 2026 11:41:28 +0000 | |
| Subject: [PATCH] fix: cron tools and panic slice string utf-8 | |
| --- | |
| src/channels/mod.rs | 11 +++++++++-- | |
| src/config/schema.rs | 5 +++++ | |
| src/cron/mod.rs | 2 +- | |
| src/cron/scheduler.rs | 1 + | |
| src/tools/cron_add.rs | 36 +++++++++++++++++++++++++++++------- | |
| src/tools/cron_remove.rs | 1 + | |
| src/tools/schedule.rs | 1 + | |
| 7 files changed, 47 insertions(+), 10 deletions(-) | |
| diff --git a/src/channels/mod.rs b/src/channels/mod.rs | |
| index 610fe715..f0984dff 100644 | |
| --- a/src/channels/mod.rs | |
| +++ b/src/channels/mod.rs | |
| @@ -128,7 +128,12 @@ impl Observer for ChannelNotifyObserver { | |
| } else { | |
| let s = args.to_string(); | |
| if s.len() > 120 { | |
| - format!(": {}…", &s[..120]) | |
| + let truncated = s.char_indices() | |
| + .take_while(|(i, _)| *i < 120) | |
| + .last() | |
| + .map(|(i, c)| &s[..i + c.len_utf8()]) | |
| + .unwrap_or(""); | |
| + format!(": {}…", truncated) | |
| } else { | |
| format!(": {s}") | |
| } | |
| @@ -286,6 +291,7 @@ struct ChannelRuntimeContext { | |
| provider_runtime_options: providers::ProviderRuntimeOptions, | |
| workspace_dir: Arc<PathBuf>, | |
| message_timeout_secs: u64, | |
| + show_tool_notifications: bool, | |
| interrupt_on_new_message: bool, | |
| multimodal: crate::config::MultimodalConfig, | |
| hooks: Option<Arc<crate::hooks::HookRunner>>, | |
| @@ -1864,7 +1870,7 @@ async fn process_channel_message( | |
| let notify_channel = target_channel.clone(); | |
| let notify_reply_target = msg.reply_target.clone(); | |
| let notify_thread_root = msg.id.clone(); | |
| - let notify_task = if msg.channel == "cli" { | |
| + let notify_task = if msg.channel == "cli" || !ctx.show_tool_notifications { | |
| Some(tokio::spawn(async move { | |
| while notify_rx.recv().await.is_some() {} | |
| })) | |
| @@ -3497,6 +3503,7 @@ pub async fn start_channels(config: Config) -> Result<()> { | |
| provider_runtime_options, | |
| workspace_dir: Arc::new(config.workspace_dir.clone()), | |
| message_timeout_secs, | |
| + show_tool_notifications: config.channels_config.show_tool_notifications, | |
| interrupt_on_new_message, | |
| multimodal: config.multimodal.clone(), | |
| hooks: if config.hooks.enabled { | |
| diff --git a/src/config/schema.rs b/src/config/schema.rs | |
| index c0f7f6d0..311a0bca 100644 | |
| --- a/src/config/schema.rs | |
| +++ b/src/config/schema.rs | |
| @@ -2827,6 +2827,10 @@ pub struct ChannelsConfig { | |
| /// 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, | |
| + /// Send tool-call notifications as live messages in the channel thread. | |
| + /// Defaults to false to avoid cluttering chats with internal bookkeeping. | |
| + #[serde(default)] | |
| + pub show_tool_notifications: bool, | |
| } | |
| impl ChannelsConfig { | |
| @@ -2954,6 +2958,7 @@ impl Default for ChannelsConfig { | |
| nostr: None, | |
| clawdtalk: None, | |
| message_timeout_secs: default_channel_message_timeout_secs(), | |
| + show_tool_notifications: false, | |
| } | |
| } | |
| } | |
| diff --git a/src/cron/mod.rs b/src/cron/mod.rs | |
| index 49db429d..b10a4276 100644 | |
| --- a/src/cron/mod.rs | |
| +++ b/src/cron/mod.rs | |
| @@ -200,7 +200,7 @@ pub fn resume_job(config: &Config, id: &str) -> Result<CronJob> { | |
| ) | |
| } | |
| -fn parse_delay(input: &str) -> Result<chrono::Duration> { | |
| +pub fn parse_delay(input: &str) -> Result<chrono::Duration> { | |
| let input = input.trim(); | |
| if input.is_empty() { | |
| anyhow::bail!("delay must not be empty"); | |
| diff --git a/src/cron/scheduler.rs b/src/cron/scheduler.rs | |
| index 024be18b..b880c415 100644 | |
| --- a/src/cron/scheduler.rs | |
| +++ b/src/cron/scheduler.rs | |
| @@ -127,6 +127,7 @@ async fn execute_and_persist_job( | |
| crate::health::mark_component_ok(component); | |
| warn_if_high_frequency_agent_job(job); | |
| + eprintln!("[DEBUG scheduler] firing job id={} delivery={}", job.id, serde_json::to_string(&job.delivery).unwrap_or_default()); | |
| let started_at = Utc::now(); | |
| let (success, output) = execute_job_with_retry(config, security, job).await; | |
| let finished_at = Utc::now(); | |
| diff --git a/src/tools/cron_add.rs b/src/tools/cron_add.rs | |
| index b13979e9..ae701fb5 100644 | |
| --- a/src/tools/cron_add.rs | |
| +++ b/src/tools/cron_add.rs | |
| @@ -1,6 +1,7 @@ | |
| use super::traits::{Tool, ToolResult}; | |
| use crate::config::Config; | |
| use crate::cron::{self, DeliveryConfig, JobType, Schedule, SessionTarget}; | |
| +use chrono::Utc; | |
| use crate::security::SecurityPolicy; | |
| use async_trait::async_trait; | |
| use serde_json::json; | |
| @@ -68,7 +69,11 @@ impl Tool for CronAddTool { | |
| "name": { "type": "string" }, | |
| "schedule": { | |
| "type": "object", | |
| - "description": "Schedule object: {kind:'cron',expr,tz?} | {kind:'at',at} | {kind:'every',every_ms}" | |
| + "description": "Schedule: {\"kind\":\"at\",\"at\":\"2026-03-12T07:00:00Z\"} | {\"kind\":\"cron\",\"expr\":\"0 9 * * *\"} | {\"kind\":\"every\",\"every_ms\":60000}. Use ISO 8601 UTC for 'at'." | |
| + }, | |
| + "delay": { | |
| + "type": "string", | |
| + "description": "Human-readable delay from now, e.g. '1m', '30s', '2h', '1d'. Use this OR schedule.kind=at, not both." | |
| }, | |
| "job_type": { "type": "string", "enum": ["shell", "agent"] }, | |
| "command": { "type": "string" }, | |
| @@ -97,6 +102,8 @@ impl Tool for CronAddTool { | |
| } | |
| async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> { | |
| + tracing::info!(args = %args, "cron_add called"); | |
| + eprintln!("[DEBUG cron_add] args = {}", args); | |
| if !self.config.cron.enabled { | |
| return Ok(ToolResult { | |
| success: false, | |
| @@ -112,16 +119,31 @@ impl Tool for CronAddTool { | |
| return Ok(ToolResult { | |
| success: false, | |
| output: String::new(), | |
| - error: Some(format!("Invalid schedule: {e}")), | |
| + error: Some(format!("Invalid schedule: {e}. Use {{\"kind\":\"at\",\"at\":\"<RFC3339>\"}} or set the 'delay' field instead.")), | |
| }); | |
| } | |
| }, | |
| None => { | |
| - return Ok(ToolResult { | |
| - success: false, | |
| - output: String::new(), | |
| - error: Some("Missing 'schedule' parameter".to_string()), | |
| - }); | |
| + // Fall back to top-level `delay` field (e.g. "1m", "30s", "2h") | |
| + match args.get("delay").and_then(|v| v.as_str()) { | |
| + Some(delay_str) => match cron::parse_delay(delay_str) { | |
| + Ok(duration) => Schedule::At { at: Utc::now() + duration }, | |
| + Err(e) => { | |
| + return Ok(ToolResult { | |
| + success: false, | |
| + output: String::new(), | |
| + error: Some(format!("Invalid delay '{delay_str}': {e}")), | |
| + }); | |
| + } | |
| + }, | |
| + None => { | |
| + return Ok(ToolResult { | |
| + success: false, | |
| + output: String::new(), | |
| + error: Some("Missing 'schedule' or 'delay' parameter. Use delay='1m' for a one-minute reminder.".to_string()), | |
| + }); | |
| + } | |
| + } | |
| } | |
| }; | |
| diff --git a/src/tools/cron_remove.rs b/src/tools/cron_remove.rs | |
| index b4dc110c..c3c24b0e 100644 | |
| --- a/src/tools/cron_remove.rs | |
| +++ b/src/tools/cron_remove.rs | |
| @@ -68,6 +68,7 @@ impl Tool for CronRemoveTool { | |
| } | |
| async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> { | |
| + eprintln!("[DEBUG cron_remove] args = {}", args); | |
| if !self.config.cron.enabled { | |
| return Ok(ToolResult { | |
| success: false, | |
| diff --git a/src/tools/schedule.rs b/src/tools/schedule.rs | |
| index 88b824c5..ff17b8f2 100644 | |
| --- a/src/tools/schedule.rs | |
| +++ b/src/tools/schedule.rs | |
| @@ -73,6 +73,7 @@ impl Tool for ScheduleTool { | |
| } | |
| async fn execute(&self, args: serde_json::Value) -> Result<ToolResult> { | |
| + eprintln!("[DEBUG schedule] args = {}", args); | |
| let action = args | |
| .get("action") | |
| .and_then(|value| value.as_str()) | |
| -- | |
| 2.43.0 | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment