From f50f7622831000118d94189b6aa7c17fc21fd0f5 Mon Sep 17 00:00:00 2001 From: shaun-agent Date: Thu, 14 May 2026 12:49:13 +0000 Subject: [PATCH 1/3] fix(cron): truncate visible trigger messages --- docs/cronjob.md | 1 + src/cron.rs | 62 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/docs/cronjob.md b/docs/cronjob.md index 6c73ffba..15ebcf0d 100644 --- a/docs/cronjob.md +++ b/docs/cronjob.md @@ -275,6 +275,7 @@ usercron_path = "cronjob.toml" - **Isolation**: Cron failures are logged but never block interactive chat traffic. - **Stateless**: No persistence needed. Schedules are re-evaluated from config on restart. - **Graceful shutdown**: In-flight cron tasks are waited on (up to 30 seconds) during shutdown. +- **Long prompts**: The full configured `message` is always sent to the agent. If the visible trigger message would exceed the platform limit, only the visible trigger is truncated before delivery. ## Sender Identity diff --git a/src/cron.rs b/src/cron.rs index a570e96e..36b0423e 100644 --- a/src/cron.rs +++ b/src/cron.rs @@ -12,6 +12,9 @@ use std::time::SystemTime; use tokio::sync::Mutex; use tracing::{debug, error, info, warn}; +const CRON_TRIGGER_TRUNCATED_NOTICE: &str = + "\n\n...\n(full cron prompt sent to agent; visible trigger truncated)"; + /// Parse a 5-field POSIX cron expression into a `Schedule`. /// /// The `cron` crate expects a 6-field expression (with seconds), so we prepend "0". @@ -221,6 +224,36 @@ fn render_run(start: u32, end: u32) -> String { } } +fn take_chars(s: &str, limit: usize) -> String { + s.chars().take(limit).collect() +} + +fn cron_trigger_message(sender_name: &str, message: &str, limit: usize) -> String { + let prefix = format!("🕐 [{}]: ", sender_name); + let full = format!("{prefix}{message}"); + if full.chars().count() <= limit { + return full; + } + + let prefix_len = prefix.chars().count(); + if prefix_len >= limit { + return take_chars(&prefix, limit); + } + + let notice_len = CRON_TRIGGER_TRUNCATED_NOTICE.chars().count(); + if prefix_len + notice_len >= limit { + let available = limit.saturating_sub(prefix_len); + return format!("{prefix}{}", take_chars(message, available)); + } + + let available = limit - prefix_len - notice_len; + format!( + "{prefix}{}{}", + take_chars(message, available), + CRON_TRIGGER_TRUNCATED_NOTICE + ) +} + /// Check whether a cron schedule should fire right now. /// Truncates the current time to the minute boundary and checks if the /// schedule has an event at exactly that minute. @@ -548,11 +581,10 @@ async fn fire_cronjob( origin_event_id: None, }; + let trigger_content = + cron_trigger_message(&job.sender_name, &job.message, adapter.message_limit()); let trigger_msg = match adapter - .send_message( - &thread_channel, - &format!("🕐 [{}]: {}", job.sender_name, job.message), - ) + .send_message(&thread_channel, &trigger_content) .await { Ok(msg) => msg, @@ -935,6 +967,28 @@ mod tests { assert!(should_fire(&schedule, chrono_tz::UTC)); } + #[test] + fn cron_trigger_message_keeps_short_prompt_unchanged() { + assert_eq!( + cron_trigger_message("DailyOps", "summarize yesterday", 2000), + "🕐 [DailyOps]: summarize yesterday" + ); + } + + #[test] + fn cron_trigger_message_truncates_long_prompt_to_platform_limit() { + let msg = cron_trigger_message("PR-Screening", &"x".repeat(4000), 2000); + assert!(msg.chars().count() <= 2000); + assert!(msg.starts_with("🕐 [PR-Screening]: ")); + assert!(msg.contains("visible trigger truncated")); + } + + #[test] + fn cron_trigger_message_handles_tiny_limits() { + let msg = cron_trigger_message("VeryLongSenderName", "payload", 8); + assert_eq!(msg.chars().count(), 8); + } + #[test] fn should_fire_returns_false_for_distant_schedule() { let schedule = parse_cron_expr("0 0 1 1 *").unwrap(); From 208e073ca4c4a0b3f40476ac90300a9b9bb6de78 Mon Sep 17 00:00:00 2001 From: shaun-agent Date: Thu, 14 May 2026 13:44:36 +0000 Subject: [PATCH 2/3] Revert "fix(cron): truncate visible trigger messages" This reverts commit f50f7622831000118d94189b6aa7c17fc21fd0f5. --- docs/cronjob.md | 1 - src/cron.rs | 62 ++++--------------------------------------------- 2 files changed, 4 insertions(+), 59 deletions(-) diff --git a/docs/cronjob.md b/docs/cronjob.md index 15ebcf0d..6c73ffba 100644 --- a/docs/cronjob.md +++ b/docs/cronjob.md @@ -275,7 +275,6 @@ usercron_path = "cronjob.toml" - **Isolation**: Cron failures are logged but never block interactive chat traffic. - **Stateless**: No persistence needed. Schedules are re-evaluated from config on restart. - **Graceful shutdown**: In-flight cron tasks are waited on (up to 30 seconds) during shutdown. -- **Long prompts**: The full configured `message` is always sent to the agent. If the visible trigger message would exceed the platform limit, only the visible trigger is truncated before delivery. ## Sender Identity diff --git a/src/cron.rs b/src/cron.rs index 36b0423e..a570e96e 100644 --- a/src/cron.rs +++ b/src/cron.rs @@ -12,9 +12,6 @@ use std::time::SystemTime; use tokio::sync::Mutex; use tracing::{debug, error, info, warn}; -const CRON_TRIGGER_TRUNCATED_NOTICE: &str = - "\n\n...\n(full cron prompt sent to agent; visible trigger truncated)"; - /// Parse a 5-field POSIX cron expression into a `Schedule`. /// /// The `cron` crate expects a 6-field expression (with seconds), so we prepend "0". @@ -224,36 +221,6 @@ fn render_run(start: u32, end: u32) -> String { } } -fn take_chars(s: &str, limit: usize) -> String { - s.chars().take(limit).collect() -} - -fn cron_trigger_message(sender_name: &str, message: &str, limit: usize) -> String { - let prefix = format!("🕐 [{}]: ", sender_name); - let full = format!("{prefix}{message}"); - if full.chars().count() <= limit { - return full; - } - - let prefix_len = prefix.chars().count(); - if prefix_len >= limit { - return take_chars(&prefix, limit); - } - - let notice_len = CRON_TRIGGER_TRUNCATED_NOTICE.chars().count(); - if prefix_len + notice_len >= limit { - let available = limit.saturating_sub(prefix_len); - return format!("{prefix}{}", take_chars(message, available)); - } - - let available = limit - prefix_len - notice_len; - format!( - "{prefix}{}{}", - take_chars(message, available), - CRON_TRIGGER_TRUNCATED_NOTICE - ) -} - /// Check whether a cron schedule should fire right now. /// Truncates the current time to the minute boundary and checks if the /// schedule has an event at exactly that minute. @@ -581,10 +548,11 @@ async fn fire_cronjob( origin_event_id: None, }; - let trigger_content = - cron_trigger_message(&job.sender_name, &job.message, adapter.message_limit()); let trigger_msg = match adapter - .send_message(&thread_channel, &trigger_content) + .send_message( + &thread_channel, + &format!("🕐 [{}]: {}", job.sender_name, job.message), + ) .await { Ok(msg) => msg, @@ -967,28 +935,6 @@ mod tests { assert!(should_fire(&schedule, chrono_tz::UTC)); } - #[test] - fn cron_trigger_message_keeps_short_prompt_unchanged() { - assert_eq!( - cron_trigger_message("DailyOps", "summarize yesterday", 2000), - "🕐 [DailyOps]: summarize yesterday" - ); - } - - #[test] - fn cron_trigger_message_truncates_long_prompt_to_platform_limit() { - let msg = cron_trigger_message("PR-Screening", &"x".repeat(4000), 2000); - assert!(msg.chars().count() <= 2000); - assert!(msg.starts_with("🕐 [PR-Screening]: ")); - assert!(msg.contains("visible trigger truncated")); - } - - #[test] - fn cron_trigger_message_handles_tiny_limits() { - let msg = cron_trigger_message("VeryLongSenderName", "payload", 8); - assert_eq!(msg.chars().count(), 8); - } - #[test] fn should_fire_returns_false_for_distant_schedule() { let schedule = parse_cron_expr("0 0 1 1 *").unwrap(); From 3b3d008acf99d8ebb4134bdbf1a886729ef62ef9 Mon Sep 17 00:00:00 2001 From: shaun-agent Date: Thu, 14 May 2026 13:45:03 +0000 Subject: [PATCH 3/3] docs(cron): keep PR screening prompt concise --- docs/cronjob.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/cronjob.md b/docs/cronjob.md index 6c73ffba..aa368ea3 100644 --- a/docs/cronjob.md +++ b/docs/cronjob.md @@ -115,6 +115,24 @@ platform = "slack" sender_name = "OpsBot" ``` +### Concise PR Screening Prompt + +Cron job messages are posted into the target chat before the agent runs. Keep operational prompts below the platform message limit, especially Discord's 2,000 character limit. + +For OpenAB project screening, use a compact trigger prompt like: + +```text +Run OpenAB PR screening once. Use gh only; GH_TOKEN is auth, never gh auth login. + +Project: openabdev project 1. Queue: first item in Status=Incoming; if none, first open PR in openabdev/openab with label pending-screening. If no work, reply: no PR-screening work found. + +For one item: fetch title, number, URL, body, author, labels, files/checks when relevant. Post/update a GitHub comment marked . If it came from the project board, move Incoming -> PR-Screening. If it came only from pending-screening, leave board status unless a matching project item exists. + +Report sections: Intent, Feat/Fix, Who it serves, Risks, Best-practice comparison if relevant, Options, Recommendation. Be concise and useful to a maintainer. + +End the comment with: Agent-ran OpenAB PR screening. Feedback welcome; react thumbs-up if useful. +``` + ## Helm Deployment When using the Helm chart, define cronjobs under each agent in `values.yaml`: