Skip to content

Commit 4afa33d

Browse files
sanil-23claude
andcommitted
fix(subconscious): remove HEARTBEAT.md task import, use SQLite as sole task source
Tasks are now managed exclusively in SQLite via the Subconscious UI. HEARTBEAT.md is retained for instructions/context only, not as a task list. Situation report now reads pending tasks from SQLite instead of HEARTBEAT.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7e5703d commit 4afa33d

5 files changed

Lines changed: 30 additions & 72 deletions

File tree

src/openhuman/heartbeat/engine.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,12 @@ impl HeartbeatEngine {
114114
pub async fn ensure_heartbeat_file(workspace_dir: &Path) -> Result<()> {
115115
let path = workspace_dir.join("HEARTBEAT.md");
116116
if !path.exists() {
117-
let default = "# Periodic Tasks\n\
117+
let default = "# Subconscious Instructions\n\
118118
#\n\
119-
# The subconscious loop checks these tasks periodically against\n\
119+
# The subconscious loop evaluates pending tasks periodically against\n\
120120
# your workspace state (memory, skills, email, etc.)\n\
121-
# Add or remove tasks — one per line starting with `- `\n\n\
122-
- Check for new emails that need attention\n\
123-
- Review upcoming deadlines and calendar events\n\
124-
- Monitor connected skills for errors or disconnections\n";
121+
# Tasks are managed in the Subconscious UI — this file provides\n\
122+
# additional context and instructions for task evaluation.\n";
125123
tokio::fs::write(&path, default).await?;
126124
}
127125
Ok(())
@@ -178,9 +176,10 @@ mod tests {
178176
let path = dir.join("HEARTBEAT.md");
179177
assert!(path.exists());
180178
let content = tokio::fs::read_to_string(&path).await.unwrap();
179+
assert!(content.contains("Subconscious Instructions"));
180+
// Instructions only — no task lines
181181
let tasks = HeartbeatEngine::parse_tasks(&content);
182-
assert_eq!(tasks.len(), 3);
183-
assert!(tasks.iter().any(|t| t.contains("email")));
182+
assert_eq!(tasks.len(), 0);
184183

185184
let _ = tokio::fs::remove_dir_all(&dir).await;
186185
}

src/openhuman/subconscious/engine.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ impl SubconsciousEngine {
5757
// Seed default system tasks eagerly so they show in the UI immediately,
5858
// without waiting for the first tick.
5959
let seeded = match store::with_connection(&workspace_dir, |conn| {
60-
store::seed_from_heartbeat(conn, &workspace_dir)
60+
store::seed_default_tasks(conn)
6161
}) {
6262
Ok(count) => {
6363
if count > 0 {
@@ -130,7 +130,7 @@ impl SubconsciousEngine {
130130

131131
let mut state = self.state.lock().await;
132132

133-
// Seed from HEARTBEAT.md on first tick (fallback if init seeding failed)
133+
// Seed default tasks on first tick (fallback if init seeding failed)
134134
if !state.seeded {
135135
self.seed_tasks();
136136
state.seeded = true;
@@ -418,11 +418,11 @@ impl SubconsciousEngine {
418418

419419
fn seed_tasks(&self) {
420420
match store::with_connection(&self.workspace_dir, |conn| {
421-
store::seed_from_heartbeat(conn, &self.workspace_dir)
421+
store::seed_default_tasks(conn)
422422
}) {
423423
Ok(count) => {
424424
if count > 0 {
425-
info!("[subconscious] seeded {count} tasks from HEARTBEAT.md");
425+
info!("[subconscious] seeded {count} default tasks");
426426
}
427427
}
428428
Err(e) => warn!("[subconscious] seed failed: {e}"),

src/openhuman/subconscious/integration_test.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,14 +202,9 @@ mod tests {
202202
#[test]
203203
fn seed_then_query_tasks() {
204204
let dir = tempfile::tempdir().unwrap();
205-
std::fs::write(
206-
dir.path().join("HEARTBEAT.md"),
207-
"# Tasks\n\n- Check email\n- Monitor skills\n- Review calendar\n",
208-
)
209-
.unwrap();
210205

211206
store::with_connection(dir.path(), |conn| {
212-
let count = store::seed_from_heartbeat(conn, dir.path())?;
207+
let count = store::seed_default_tasks(conn)?;
213208
assert_eq!(count, 3);
214209

215210
let tasks = store::list_tasks(conn, true)?;

src/openhuman/subconscious/situation_report.rs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Assembles a bounded "situation report" for the subconscious tick.
22
//! Gathers deltas since the last tick from memory, tools, environment,
3-
//! and HEARTBEAT.md — capped at the configured token budget.
3+
//! and pending tasks — capped at the configured token budget.
44

55
use crate::openhuman::memory::MemoryClient;
66
use std::fmt::Write;
@@ -27,8 +27,8 @@ pub async fn build_situation_report(
2727
let env_section = build_environment_section(workspace_dir);
2828
append_section(&mut report, &mut remaining, &env_section);
2929

30-
// Section 2: HEARTBEAT.md tasks
31-
let tasks_section = build_tasks_section(workspace_dir).await;
30+
// Section 2: Pending tasks from SQLite
31+
let tasks_section = build_tasks_section(workspace_dir);
3232
append_section(&mut report, &mut remaining, &tasks_section);
3333

3434
// Section 3: Memory documents (delta since last tick)
@@ -74,25 +74,21 @@ fn build_environment_section(workspace_dir: &Path) -> String {
7474
)
7575
}
7676

77-
async fn build_tasks_section(workspace_dir: &Path) -> String {
78-
let heartbeat_path = workspace_dir.join("HEARTBEAT.md");
79-
let content = match tokio::fs::read_to_string(&heartbeat_path).await {
80-
Ok(c) => c,
81-
Err(_) => return "## Pending Tasks\n\nNo HEARTBEAT.md found.\n".to_string(),
77+
fn build_tasks_section(workspace_dir: &Path) -> String {
78+
let tasks = match super::store::with_connection(workspace_dir, |conn| {
79+
super::store::list_tasks(conn, false)
80+
}) {
81+
Ok(tasks) => tasks,
82+
Err(_) => return "## Pending Tasks\n\nFailed to read tasks.\n".to_string(),
8283
};
8384

84-
let tasks: Vec<&str> = content
85-
.lines()
86-
.filter_map(|line| line.trim().strip_prefix("- "))
87-
.collect();
88-
8985
if tasks.is_empty() {
9086
return "## Pending Tasks\n\nNo tasks defined.\n".to_string();
9187
}
9288

9389
let mut section = String::from("## Pending Tasks\n\n");
9490
for task in &tasks {
95-
let _ = writeln!(section, "- {task}");
91+
let _ = writeln!(section, "- {}", task.title);
9692
}
9793
section
9894
}

src/openhuman/subconscious/store.rs

Lines changed: 8 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ pub fn get_escalation(conn: &Connection, escalation_id: &str) -> Result<Escalati
420420
.with_context(|| format!("escalation not found: {escalation_id}"))
421421
}
422422

423-
// ── Seed from HEARTBEAT.md ───────────────────────────────────────────────────
423+
// ── Seed default system tasks ────────────────────────────────────────────────
424424

425425
/// Default system tasks that are always seeded and cannot be deleted.
426426
const DEFAULT_SYSTEM_TASKS: &[&str] = &[
@@ -429,36 +429,18 @@ const DEFAULT_SYSTEM_TASKS: &[&str] = &[
429429
"Monitor system health (Ollama, memory, connections)",
430430
];
431431

432-
/// Seed default system tasks + any tasks from HEARTBEAT.md into SQLite.
432+
/// Seed default system tasks into SQLite.
433433
/// Skips tasks whose title already exists. Returns the count of newly created tasks.
434-
pub fn seed_from_heartbeat(conn: &Connection, workspace_dir: &Path) -> Result<usize> {
434+
pub fn seed_default_tasks(conn: &Connection) -> Result<usize> {
435435
let mut count = 0;
436436

437-
// 1. Always seed the default system tasks
438437
for title in DEFAULT_SYSTEM_TASKS {
439438
if !task_title_exists(conn, title)? {
440439
add_task(conn, title, TaskSource::System, TaskRecurrence::Pending)?;
441440
count += 1;
442441
}
443442
}
444443

445-
// 2. Also import from HEARTBEAT.md if it exists
446-
let heartbeat_path = workspace_dir.join("HEARTBEAT.md");
447-
if let Ok(content) = std::fs::read_to_string(&heartbeat_path) {
448-
let tasks: Vec<&str> = content
449-
.lines()
450-
.filter_map(|line| line.trim().strip_prefix("- "))
451-
.filter(|s| !s.is_empty())
452-
.collect();
453-
454-
for title in tasks {
455-
if !task_title_exists(conn, title)? {
456-
add_task(conn, title, TaskSource::System, TaskRecurrence::Pending)?;
457-
count += 1;
458-
}
459-
}
460-
}
461-
462444
Ok(count)
463445
}
464446

@@ -699,35 +681,21 @@ mod tests {
699681
}
700682

701683
#[test]
702-
fn seed_from_heartbeat_imports_tasks() {
684+
fn seed_default_tasks_creates_system_tasks() {
703685
let conn = test_conn();
704-
let dir = tempfile::tempdir().unwrap();
705-
std::fs::write(
706-
dir.path().join("HEARTBEAT.md"),
707-
"# Tasks\n\n- Check email\n- Monitor skills\n",
708-
)
709-
.unwrap();
710686

711-
let count = seed_from_heartbeat(&conn, dir.path()).unwrap();
712-
assert_eq!(count, 2);
687+
let count = seed_default_tasks(&conn).unwrap();
688+
assert_eq!(count, DEFAULT_SYSTEM_TASKS.len());
713689

714690
// Second seed should not duplicate
715-
let count2 = seed_from_heartbeat(&conn, dir.path()).unwrap();
691+
let count2 = seed_default_tasks(&conn).unwrap();
716692
assert_eq!(count2, 0);
717693

718694
let tasks = list_tasks(&conn, false).unwrap();
719-
assert_eq!(tasks.len(), 2);
695+
assert_eq!(tasks.len(), DEFAULT_SYSTEM_TASKS.len());
720696
assert!(tasks.iter().all(|t| t.source == TaskSource::System));
721697
}
722698

723-
#[test]
724-
fn seed_from_missing_heartbeat_returns_zero() {
725-
let conn = test_conn();
726-
let dir = tempfile::tempdir().unwrap();
727-
let count = seed_from_heartbeat(&conn, dir.path()).unwrap();
728-
assert_eq!(count, 0);
729-
}
730-
731699
#[test]
732700
fn recurrence_roundtrip() {
733701
assert_eq!(

0 commit comments

Comments
 (0)