From 65cb4fe099fb91101bc0b172224aca3053be67cb Mon Sep 17 00:00:00 2001 From: Argenis Date: Thu, 19 Mar 2026 08:17:08 -0400 Subject: [PATCH] =?UTF-8?q?feat(heartbeat):=20default=20interval=2030?= =?UTF-8?q?=E2=86=925min=20+=20prune=20heartbeat=20from=20auto-save=20(#39?= =?UTF-8?q?38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lower the default heartbeat interval to 5 minutes to match the renewable partial wake-lock cadence. Add `[heartbeat task` to the memory auto-save skip filter so heartbeat prompts (both Phase 1 decision and Phase 2 task execution) do not pollute persistent conversation memory. Co-authored-by: Claude Opus 4.6 --- src/config/schema.rs | 11 ++++++++--- src/daemon/mod.rs | 5 ++++- src/memory/mod.rs | 7 +++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/config/schema.rs b/src/config/schema.rs index e56ae52e1..82344184a 100644 --- a/src/config/schema.rs +++ b/src/config/schema.rs @@ -4045,7 +4045,8 @@ pub struct ClassificationRule { pub struct HeartbeatConfig { /// Enable periodic heartbeat pings. Default: `false`. pub enabled: bool, - /// Interval in minutes between heartbeat pings. Default: `30`. + /// Interval in minutes between heartbeat pings. Default: `5`. + #[serde(default = "default_heartbeat_interval")] pub interval_minutes: u32, /// Enable two-phase heartbeat: Phase 1 asks LLM whether to run, Phase 2 /// executes only when the LLM decides there is work to do. Saves API cost @@ -4089,6 +4090,10 @@ pub struct HeartbeatConfig { pub max_run_history: u32, } +fn default_heartbeat_interval() -> u32 { + 5 +} + fn default_two_phase() -> bool { true } @@ -4109,7 +4114,7 @@ impl Default for HeartbeatConfig { fn default() -> Self { Self { enabled: false, - interval_minutes: 30, + interval_minutes: default_heartbeat_interval(), two_phase: true, message: None, target: None, @@ -8335,7 +8340,7 @@ mod tests { async fn heartbeat_config_default() { let h = HeartbeatConfig::default(); assert!(!h.enabled); - assert_eq!(h.interval_minutes, 30); + assert_eq!(h.interval_minutes, 5); assert!(h.message.is_none()); assert!(h.target.is_none()); assert!(h.to.is_none()); diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index 4a2e2b8c6..179dd7a1d 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -315,7 +315,10 @@ async fn run_heartbeat_worker(config: Config) -> Result<()> { // ── Phase 1: LLM decision (two-phase mode) ────────────── let tasks_to_run = if two_phase { - let decision_prompt = HeartbeatEngine::build_decision_prompt(&tasks); + let decision_prompt = format!( + "[Heartbeat Task | decision] {}", + HeartbeatEngine::build_decision_prompt(&tasks), + ); match Box::pin(crate::agent::run( config.clone(), Some(decision_prompt), diff --git a/src/memory/mod.rs b/src/memory/mod.rs index 4a3395c67..c4facf257 100644 --- a/src/memory/mod.rs +++ b/src/memory/mod.rs @@ -101,6 +101,7 @@ pub fn should_skip_autosave_content(content: &str) -> bool { let lowered = normalized.to_ascii_lowercase(); lowered.starts_with("[cron:") + || lowered.starts_with("[heartbeat task") || lowered.starts_with("[distilled_") || lowered.contains("distilled_index_sig:") } @@ -471,6 +472,12 @@ mod tests { assert!(should_skip_autosave_content( "[DISTILLED_MEMORY_CHUNK 1/2] DISTILLED_INDEX_SIG:abc123" )); + assert!(should_skip_autosave_content( + "[Heartbeat Task | decision] Should I run tasks?" + )); + assert!(should_skip_autosave_content( + "[Heartbeat Task | high] Execute scheduled patrol" + )); assert!(!should_skip_autosave_content( "User prefers concise answers." ));