From 82ebc72b9e22ae9cb5edd2533f86a8bc4268d2c7 Mon Sep 17 00:00:00 2001 From: Ciocanel Razvan Date: Fri, 27 Feb 2026 00:11:53 +0200 Subject: [PATCH 1/3] Fix scheduler token quota to treat 0 as unlimited The scheduler's check_quota() would reject all agent turns when max_llm_tokens_per_hour was set to 0, unlike the metering engine which correctly skips the cost check when max_cost_per_hour_usd is 0.0. Add a guard to skip the token check when the limit is 0, making the behavior consistent with metering.rs. --- crates/openfang-kernel/src/scheduler.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/openfang-kernel/src/scheduler.rs b/crates/openfang-kernel/src/scheduler.rs index 6a4a298..31c2b07 100644 --- a/crates/openfang-kernel/src/scheduler.rs +++ b/crates/openfang-kernel/src/scheduler.rs @@ -88,7 +88,9 @@ impl AgentScheduler { // Reset the window if an hour has passed tracker.reset_if_expired(); - if tracker.total_tokens > quota.max_llm_tokens_per_hour { + if quota.max_llm_tokens_per_hour > 0 + && tracker.total_tokens > quota.max_llm_tokens_per_hour + { return Err(OpenFangError::QuotaExceeded(format!( "Token limit exceeded: {} / {}", tracker.total_tokens, quota.max_llm_tokens_per_hour From c034d9b4880ec9129acfe4c9f7608e5fb6626643 Mon Sep 17 00:00:00 2001 From: Ciocanel Razvan Date: Fri, 27 Feb 2026 00:32:13 +0200 Subject: [PATCH 2/3] Implement real cron expression parser (was placeholder firing every 60s) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compute_next_run for Cron schedules was a placeholder that returned now + 60 seconds regardless of the cron expression. This caused all cron jobs to fire every minute, burning through token quotas rapidly (5 jobs × 60/hour = 300 LLM calls/hour). Added the `cron` crate (0.15) and implemented proper 5-field to 7-field conversion for standard cron expressions. Jobs now fire at their correct scheduled times (e.g., "0 9 * * 1-5" fires at 9 AM UTC weekdays only). --- crates/openfang-kernel/Cargo.toml | 1 + crates/openfang-kernel/src/cron.rs | 65 ++++++++++++++++++++++++------ 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/crates/openfang-kernel/Cargo.toml b/crates/openfang-kernel/Cargo.toml index 72cfe78..b7074a8 100644 --- a/crates/openfang-kernel/Cargo.toml +++ b/crates/openfang-kernel/Cargo.toml @@ -32,6 +32,7 @@ subtle = { workspace = true } rand = { workspace = true } hex = { workspace = true } reqwest = { workspace = true } +cron = "0.15" [target.'cfg(unix)'.dependencies] libc = "0.2" diff --git a/crates/openfang-kernel/src/cron.rs b/crates/openfang-kernel/src/cron.rs index 0605453..b29d655 100644 --- a/crates/openfang-kernel/src/cron.rs +++ b/crates/openfang-kernel/src/cron.rs @@ -293,16 +293,36 @@ impl CronScheduler { /// /// - `At { at }` — returns `at` directly. /// - `Every { every_secs }` — returns `now + every_secs`. -/// - `Cron { .. }` — returns 60 seconds from now (placeholder until a cron -/// expression parser is added). +/// - `Cron { expr, tz }` — parses the cron expression and computes the next +/// matching time. Supports standard 5-field (`min hour dom month dow`) and +/// 6-field (`sec min hour dom month dow`) formats by converting to the +/// 7-field format required by the `cron` crate. pub fn compute_next_run(schedule: &CronSchedule) -> chrono::DateTime { match schedule { CronSchedule::At { at } => *at, CronSchedule::Every { every_secs } => Utc::now() + Duration::seconds(*every_secs as i64), - CronSchedule::Cron { .. } => { - // Placeholder: real cron parsing will be added when the `cron` - // crate is brought in. For now, fire 60 seconds from now. - Utc::now() + Duration::seconds(60) + CronSchedule::Cron { expr, tz: _ } => { + // Convert standard 5/6-field cron to 7-field for the `cron` crate. + // Standard 5-field: min hour dom month dow + // 6-field: sec min hour dom month dow + // cron crate: sec min hour dom month dow year + let fields: Vec<&str> = expr.trim().split_whitespace().collect(); + let seven_field = match fields.len() { + 5 => format!("0 {} *", expr.trim()), + 6 => format!("{} *", expr.trim()), + _ => expr.clone(), + }; + + match seven_field.parse::() { + Ok(sched) => sched + .upcoming(Utc) + .next() + .unwrap_or_else(|| Utc::now() + Duration::hours(1)), + Err(e) => { + warn!("Failed to parse cron expression '{}': {}", expr, e); + Utc::now() + Duration::hours(1) + } + } } } } @@ -655,18 +675,39 @@ mod tests { } #[test] - fn test_compute_next_run_cron_placeholder() { - let before = Utc::now(); + fn test_compute_next_run_cron_daily() { + let now = Utc::now(); let schedule = CronSchedule::Cron { expr: "0 9 * * *".into(), tz: None, }; let next = compute_next_run(&schedule); - let after = Utc::now(); - // Placeholder returns ~60s from now - assert!(next >= before + Duration::seconds(59)); - assert!(next <= after + Duration::seconds(61)); + // Should be within the next 24 hours (next 09:00 UTC) + assert!(next > now); + assert!(next <= now + Duration::hours(24)); + // Should fire at minute 0 of hour 9 + assert_eq!(next.format("%M").to_string(), "00"); + assert_eq!(next.format("%H").to_string(), "09"); + } + + #[test] + fn test_compute_next_run_cron_weekday() { + let now = Utc::now(); + let schedule = CronSchedule::Cron { + expr: "30 14 * * 1-5".into(), + tz: None, + }; + let next = compute_next_run(&schedule); + + // Should be within the next 7 days + assert!(next > now); + assert!(next <= now + Duration::days(7)); + // Should fire at 14:30 + assert_eq!(next.format("%H:%M").to_string(), "14:30"); + // Should be a weekday (Mon=1 .. Fri=5 in chrono) + let weekday = next.weekday().num_days_from_monday(); // 0=Mon, 4=Fri + assert!(weekday <= 4, "Expected weekday, got {}", next.weekday()); } // -- error message truncation in record_failure ------------------------- From fd52529aceb4bfea9e6d2d564459cc8dc576c9ad Mon Sep 17 00:00:00 2001 From: Ciocanel Razvan Date: Fri, 27 Feb 2026 00:39:26 +0200 Subject: [PATCH 3/3] Wire scheduler dashboard to real CronScheduler API The scheduler page was calling /api/schedules (empty KV store) instead of /api/cron/jobs (the real CronScheduler engine). Jobs created by agents via cron_create were invisible in the dashboard. Changed scheduler.js to use /api/cron/jobs for all CRUD operations. Normalized the nested response format (schedule.expr, action.message) to flat fields the UI expects. Replaced "Runs" column with "Next Run" which shows when each job will fire next. Added friendly day-of-week labels to describeCron() (e.g., "Weekdays at 5:00 AM"). --- crates/openfang-api/static/index_body.html | 4 +- .../openfang-api/static/js/pages/scheduler.js | 91 ++++++++++++------- 2 files changed, 62 insertions(+), 33 deletions(-) diff --git a/crates/openfang-api/static/index_body.html b/crates/openfang-api/static/index_body.html index 7d2a2a8..08ee3d7 100644 --- a/crates/openfang-api/static/index_body.html +++ b/crates/openfang-api/static/index_body.html @@ -1523,7 +1523,7 @@

Scheduler

Agent Status Last Run - Runs + Next Run Actions @@ -1543,7 +1543,7 @@

Scheduler

- +