Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save hypnguyen1209/46d5677b49b8bfe022031b78a34448ac to your computer and use it in GitHub Desktop.

Select an option

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
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