From d47905a68fbf6c9189b677892c7f0963fe54a0a7 Mon Sep 17 00:00:00 2001 From: limiteinductive Date: Sun, 29 Mar 2026 23:03:28 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20codex=20review=20=E2=80=94=20safe=20ack,?= =?UTF-8?q?=20dream=20stops=20worker,=20graceful=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Worker: - Only ack inbox items if Claude succeeds (prevents losing work on crash) - Graceful error if colony not in PATH (no panic) - Check colony inbox exit code before parsing - Per-agent prompt path (/tmp/colony-{name}-prompt.md) Dream: - Stops worker service before dreaming (prevents file races) - Restarts worker after dream completes - Posts error message if dream fails - Uses COLONY_AGENT env var for service name Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/colony-agent/src/dream.rs | 39 ++++++++++++++++++--------- crates/colony-agent/src/worker.rs | 44 +++++++++++++++++++++---------- 2 files changed, 56 insertions(+), 27 deletions(-) diff --git a/crates/colony-agent/src/dream.rs b/crates/colony-agent/src/dream.rs index 87e5053..e4d1554 100644 --- a/crates/colony-agent/src/dream.rs +++ b/crates/colony-agent/src/dream.rs @@ -13,12 +13,17 @@ pub fn run_dream() { return; } - // 2. Announce dream + // 2. Stop worker to prevent file races + let agent_name = std::env::var("COLONY_AGENT").unwrap_or_else(|_| "agent".into()); + let worker_service = format!("agent-{}-worker", agent_name); + let _ = Command::new("systemctl").args(["stop", &worker_service]).status(); + + // 3. Announce dream let _ = Command::new("colony") .args(["post", "general", "💤 dreaming... back in a few minutes", "--type", "plan", "--quiet"]) .status(); - // 3. Invoke Claude for dream cycle + // 4. Invoke Claude for dream cycle eprintln!("dreaming... ({} lines of memory)", line_count); let prompt = format!( @@ -31,7 +36,7 @@ pub fn run_dream() { line_count ); - let status = Command::new("claude") + let dream_ok = match Command::new("claude") .args([ "--dangerously-skip-permissions", "-p", @@ -39,16 +44,24 @@ pub fn run_dream() { "--max-turns", "10", ]) - .status(); + .status() + { + Ok(s) if s.success() => { eprintln!("dream completed"); true } + Ok(s) => { eprintln!("dream exited with status: {}", s); false } + Err(e) => { eprintln!("failed to run claude for dream: {}", e); false } + }; - match status { - Ok(s) if s.success() => eprintln!("dream completed"), - Ok(s) => eprintln!("dream exited with status: {}", s), - Err(e) => eprintln!("failed to run claude for dream: {}", e), + // 5. Restart worker + let _ = Command::new("systemctl").args(["start", &worker_service]).status(); + + // 6. Announce return + if dream_ok { + let _ = Command::new("colony") + .args(["post", "general", "👁 back from dreaming", "--type", "plan", "--quiet"]) + .status(); + } else { + let _ = Command::new("colony") + .args(["post", "general", "⚠ dream failed, back online", "--type", "error", "--quiet"]) + .status(); } - - // 4. Announce return - let _ = Command::new("colony") - .args(["post", "general", "👁 back from dreaming", "--type", "plan", "--quiet"]) - .status(); } diff --git a/crates/colony-agent/src/worker.rs b/crates/colony-agent/src/worker.rs index 6d6fb90..64f626a 100644 --- a/crates/colony-agent/src/worker.rs +++ b/crates/colony-agent/src/worker.rs @@ -4,10 +4,18 @@ use std::time::Duration; /// Run one pulse cycle. Returns true if Claude was invoked (there was work). pub fn run_pulse(config_path: &str) -> bool { // 1. Check inbox - let inbox_output = Command::new("colony") - .args(["inbox", "--json"]) - .output() - .expect("failed to run colony inbox"); + let inbox_output = match Command::new("colony").args(["inbox", "--json"]).output() { + Ok(o) => o, + Err(e) => { + eprintln!("colony not found or failed: {}. is it in PATH?", e); + return false; + } + }; + + if !inbox_output.status.success() { + eprintln!("colony inbox failed (exit {}), skipping pulse", inbox_output.status); + return false; + } let inbox_str = String::from_utf8_lossy(&inbox_output.stdout); let inbox: Vec = serde_json::from_str(&inbox_str).unwrap_or_default(); @@ -50,13 +58,17 @@ pub fn run_pulse(config_path: &str) -> bool { prompt.push_str("Use `colony read ` to get more context if needed. "); prompt.push_str("Log what you did to memory/memory.md.\n"); - // 5. Write prompt to temp file - let prompt_path = "/tmp/colony-agent-prompt.md"; - std::fs::write(prompt_path, &prompt).expect("failed to write prompt file"); + // 5. Write prompt to temp file (per-agent path to avoid conflicts) + let agent_name = std::env::var("COLONY_AGENT").unwrap_or_else(|_| "agent".into()); + let prompt_path = format!("/tmp/colony-{}-prompt.md", agent_name); + if let Err(e) = std::fs::write(&prompt_path, &prompt) { + eprintln!("failed to write prompt file: {}", e); + return false; + } // 6. Invoke Claude Code eprintln!("invoking claude with {} inbox items + heartbeat", inbox.len()); - let claude_status = Command::new("claude") + let claude_ok = match Command::new("claude") .args([ "--dangerously-skip-permissions", "-p", @@ -64,22 +76,24 @@ pub fn run_pulse(config_path: &str) -> bool { "--max-turns", "20", ]) - .status(); - - match claude_status { + .status() + { Ok(s) if s.success() => { eprintln!("claude completed successfully"); + true } Ok(s) => { eprintln!("claude exited with status: {}", s); + false } Err(e) => { eprintln!("failed to run claude: {}", e); + false } - } + }; - // 7. Ack inbox items - if !inbox.is_empty() { + // 7. Only ack inbox items if Claude succeeded + if claude_ok && !inbox.is_empty() { let ids: Vec = inbox.iter() .filter_map(|i| i["id"].as_i64().map(|id| id.to_string())) .collect(); @@ -89,6 +103,8 @@ pub fn run_pulse(config_path: &str) -> bool { .args(&ids) .status(); } + } else if !claude_ok && !inbox.is_empty() { + eprintln!("claude failed — NOT acking {} inbox items (will retry next pulse)", inbox.len()); } true