From 0ca05e3de3533f0a781a31678d123d002f719c20 Mon Sep 17 00:00:00 2001 From: Illia Polosukhin Date: Thu, 5 Feb 2026 19:46:43 -0800 Subject: [PATCH] Seed HEARTBEAT.md on first access and skip effectively-empty checklists The heartbeat feature was dead on arrival: nothing ever created HEARTBEAT.md, so the runner silently skipped every cycle. Now the workspace returns an in-memory seed template when the file doesn't exist in the database (no DB write), and the runner detects "effectively empty" content (headers, HTML comments, bare list markers) to avoid wasting LLM API calls on placeholder templates. The user creates the real DB entry via memory_write when they actually want periodic checks. Co-Authored-By: Claude Opus 4.6 --- src/agent/heartbeat.rs | 144 ++++++++++++++++++++++++++++++++++++++++- src/workspace/mod.rs | 26 +++++++- 2 files changed, 168 insertions(+), 2 deletions(-) diff --git a/src/agent/heartbeat.rs b/src/agent/heartbeat.rs index e6f25379..115b8159 100644 --- a/src/agent/heartbeat.rs +++ b/src/agent/heartbeat.rs @@ -178,7 +178,7 @@ impl HeartbeatRunner { pub async fn check_heartbeat(&self) -> HeartbeatResult { // Get the heartbeat checklist let checklist = match self.workspace.heartbeat_checklist().await { - Ok(Some(content)) if !content.trim().is_empty() => content, + Ok(Some(content)) if !is_effectively_empty(&content) => content, Ok(_) => return HeartbeatResult::Skipped, Err(e) => return HeartbeatResult::Failed(format!("Failed to read checklist: {}", e)), }; @@ -257,6 +257,45 @@ impl HeartbeatRunner { } } +/// Check if heartbeat content is effectively empty. +/// +/// Returns true if the content contains only: +/// - Whitespace +/// - Markdown headers (lines starting with #) +/// - HTML comments (``) +/// - Empty list items (`- [ ]`, `- [x]`, `-`, `*`) +/// +/// This skips the LLM call when the user hasn't added real tasks yet, +/// saving API costs. +fn is_effectively_empty(content: &str) -> bool { + let without_comments = strip_html_comments(content); + + without_comments.lines().all(|line| { + let trimmed = line.trim(); + trimmed.is_empty() + || trimmed.starts_with('#') + || trimmed == "- [ ]" + || trimmed == "- [x]" + || trimmed == "-" + || trimmed == "*" + }) +} + +/// Remove HTML comments from content. +fn strip_html_comments(content: &str) -> String { + let mut result = String::with_capacity(content.len()); + let mut rest = content; + while let Some(start) = rest.find("") { + Some(end) => rest = &rest[start + end + 3..], + None => return result, // unclosed comment, treat rest as comment + } + } + result.push_str(rest); + result +} + /// Spawn the heartbeat runner as a background task. /// /// Returns a handle that can be used to stop the runner. @@ -301,4 +340,107 @@ mod tests { let disabled = HeartbeatConfig::default().disabled(); assert!(!disabled.enabled); } + + // ==================== strip_html_comments ==================== + + #[test] + fn test_strip_html_comments_no_comments() { + assert_eq!(strip_html_comments("hello world"), "hello world"); + } + + #[test] + fn test_strip_html_comments_single() { + assert_eq!( + strip_html_comments("beforeafter"), + "beforeafter" + ); + } + + #[test] + fn test_strip_html_comments_multiple() { + let input = "abc"; + assert_eq!(strip_html_comments(input), "abc"); + } + + #[test] + fn test_strip_html_comments_multiline() { + let input = "# Title\n\nreal content"; + assert_eq!(strip_html_comments(input), "# Title\n\nreal content"); + } + + #[test] + fn test_strip_html_comments_unclosed() { + let input = "before")); + } + + #[test] + fn test_effectively_empty_empty_checkboxes() { + assert!(is_effectively_empty("# Checklist\n- [ ]\n- [x]")); + } + + #[test] + fn test_effectively_empty_bare_list_markers() { + assert!(is_effectively_empty("-\n*\n-")); + } + + #[test] + fn test_effectively_empty_seeded_template() { + let template = "\ +# Heartbeat Checklist + +"; + assert!(is_effectively_empty(template)); + } + + #[test] + fn test_effectively_empty_real_checklist() { + let content = "\ +# Heartbeat Checklist + +- [ ] Check for unread emails needing a reply +- [ ] Review today's calendar for upcoming meetings"; + assert!(!is_effectively_empty(content)); + } + + #[test] + fn test_effectively_empty_mixed_real_and_headers() { + let content = "# Title\n\nDo something important"; + assert!(!is_effectively_empty(content)); + } + + #[test] + fn test_effectively_empty_comment_plus_real_content() { + let content = "\nActual task here"; + assert!(!is_effectively_empty(content)); + } } diff --git a/src/workspace/mod.rs b/src/workspace/mod.rs index 32cc2121..24f77217 100644 --- a/src/workspace/mod.rs +++ b/src/workspace/mod.rs @@ -60,6 +60,23 @@ use uuid::Uuid; use crate::error::WorkspaceError; +/// Default template seeded into HEARTBEAT.md on first access. +/// +/// Intentionally comment-only so the heartbeat runner treats it as +/// "effectively empty" and skips the LLM call until the user adds +/// real tasks. +const HEARTBEAT_SEED: &str = "\ +# Heartbeat Checklist + +"; + /// Workspace provides database-backed memory storage for an agent. /// /// Each workspace is scoped to a user (and optionally an agent). @@ -246,10 +263,17 @@ impl Workspace { } /// Get the heartbeat checklist (HEARTBEAT.md). + /// + /// Returns the DB-stored checklist if it exists, otherwise falls back + /// to the in-memory seed template. The seed is never written to the + /// database; the user creates the real file via `memory_write` when + /// they actually want periodic checks. The seed content is all HTML + /// comments, which the heartbeat runner treats as "effectively empty" + /// and skips the LLM call. pub async fn heartbeat_checklist(&self) -> Result, WorkspaceError> { match self.read(paths::HEARTBEAT).await { Ok(doc) => Ok(Some(doc.content)), - Err(WorkspaceError::DocumentNotFound { .. }) => Ok(None), + Err(WorkspaceError::DocumentNotFound { .. }) => Ok(Some(HEARTBEAT_SEED.to_string())), Err(e) => Err(e), } }