From bbf02f6e127ccacfe80e678d7c01f22ee79f565d Mon Sep 17 00:00:00 2001 From: Ciaran Ashton Date: Tue, 31 Mar 2026 10:39:18 +0100 Subject: [PATCH 1/2] fix: cron jobs bypassed by listen-only mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cron-originated messages carry a platform source (e.g., "slack") for adapter routing but use sender_id="system". The listen-only guard only checked message.source != "system", so cron messages were silently suppressed — the channel returned without running the LLM, producing no output and skipping delivery. Add sender_id != "system" to the guard so cron messages are correctly exempted from listen-only suppression. --- src/agent/channel.rs | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/agent/channel.rs b/src/agent/channel.rs index 457ee42d1..86571ed81 100644 --- a/src/agent/channel.rs +++ b/src/agent/channel.rs @@ -1759,7 +1759,13 @@ impl Channel { // Listen-first guardrail: // ingest all messages, but only reply when explicitly invoked. - if self.listen_only_mode && message.source != "system" && !self.is_dm() { + // Cron-originated messages carry a platform source (e.g., "slack") for adapter + // routing but use sender_id="system" — exempt them from suppression. + if self.listen_only_mode + && message.source != "system" + && message.sender_id != "system" + && !self.is_dm() + { (invoked_by_command, invoked_by_mention, invoked_by_reply) = self.compute_listen_mode_invocation(&message, &raw_text); @@ -3684,6 +3690,36 @@ mod tests { assert!(!invoked_by_reply); } + #[test] + fn listen_only_guard_allows_cron_messages_through() { + // Cron messages have source="slack" (for adapter routing) but sender_id="system". + // The listen-only guard must not suppress them. + let mut message = inbound_message("slack", &[], "Check the weather"); + message.sender_id = "system".into(); + message.conversation_id = "cron:daily-weather".into(); + + // compute_listen_mode_invocation returns all false — no command/mention/reply + let (cmd, mention, reply) = + compute_listen_mode_invocation(&message, "Check the weather"); + assert!(!cmd); + assert!(!mention); + assert!(!reply); + + // The guard condition should NOT enter suppression because sender_id == "system" + let listen_only = true; + let source_is_system = message.source == "system"; // false + let sender_is_system = message.sender_id == "system"; // true + let is_dm = is_dm_conversation_id(&message.conversation_id); // false + + // Full guard: all four conditions must be true to suppress + let would_suppress = + listen_only && !source_is_system && !sender_is_system && !is_dm; + assert!( + !would_suppress, + "cron messages (sender_id=system) must bypass listen-only suppression" + ); + } + #[test] fn discord_quiet_mode_ping_ack_requires_directed_ping() { let directed_message = inbound_message( From df87ece07a1eee0abd278785ec44226b3487bdbc Mon Sep 17 00:00:00 2001 From: Ciaran Ashton Date: Tue, 31 Mar 2026 10:45:13 +0100 Subject: [PATCH 2/2] style: cargo fmt --- src/agent/channel.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/agent/channel.rs b/src/agent/channel.rs index 76e71982f..f606c3b52 100644 --- a/src/agent/channel.rs +++ b/src/agent/channel.rs @@ -3821,8 +3821,7 @@ mod tests { message.conversation_id = "cron:daily-weather".into(); // compute_listen_mode_invocation returns all false — no command/mention/reply - let (cmd, mention, reply) = - compute_listen_mode_invocation(&message, "Check the weather"); + let (cmd, mention, reply) = compute_listen_mode_invocation(&message, "Check the weather"); assert!(!cmd); assert!(!mention); assert!(!reply);