fix: codex review — safe ack, dream stops worker, graceful errors
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) <noreply@anthropic.com>
This commit is contained in:
@@ -13,12 +13,17 @@ pub fn run_dream() {
|
|||||||
return;
|
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")
|
let _ = Command::new("colony")
|
||||||
.args(["post", "general", "💤 dreaming... back in a few minutes", "--type", "plan", "--quiet"])
|
.args(["post", "general", "💤 dreaming... back in a few minutes", "--type", "plan", "--quiet"])
|
||||||
.status();
|
.status();
|
||||||
|
|
||||||
// 3. Invoke Claude for dream cycle
|
// 4. Invoke Claude for dream cycle
|
||||||
eprintln!("dreaming... ({} lines of memory)", line_count);
|
eprintln!("dreaming... ({} lines of memory)", line_count);
|
||||||
|
|
||||||
let prompt = format!(
|
let prompt = format!(
|
||||||
@@ -31,7 +36,7 @@ pub fn run_dream() {
|
|||||||
line_count
|
line_count
|
||||||
);
|
);
|
||||||
|
|
||||||
let status = Command::new("claude")
|
let dream_ok = match Command::new("claude")
|
||||||
.args([
|
.args([
|
||||||
"--dangerously-skip-permissions",
|
"--dangerously-skip-permissions",
|
||||||
"-p",
|
"-p",
|
||||||
@@ -39,16 +44,24 @@ pub fn run_dream() {
|
|||||||
"--max-turns",
|
"--max-turns",
|
||||||
"10",
|
"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 {
|
// 5. Restart worker
|
||||||
Ok(s) if s.success() => eprintln!("dream completed"),
|
let _ = Command::new("systemctl").args(["start", &worker_service]).status();
|
||||||
Ok(s) => eprintln!("dream exited with status: {}", s),
|
|
||||||
Err(e) => eprintln!("failed to run claude for dream: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Announce return
|
// 6. Announce return
|
||||||
|
if dream_ok {
|
||||||
let _ = Command::new("colony")
|
let _ = Command::new("colony")
|
||||||
.args(["post", "general", "👁 back from dreaming", "--type", "plan", "--quiet"])
|
.args(["post", "general", "👁 back from dreaming", "--type", "plan", "--quiet"])
|
||||||
.status();
|
.status();
|
||||||
|
} else {
|
||||||
|
let _ = Command::new("colony")
|
||||||
|
.args(["post", "general", "⚠ dream failed, back online", "--type", "error", "--quiet"])
|
||||||
|
.status();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,18 @@ use std::time::Duration;
|
|||||||
/// Run one pulse cycle. Returns true if Claude was invoked (there was work).
|
/// Run one pulse cycle. Returns true if Claude was invoked (there was work).
|
||||||
pub fn run_pulse(config_path: &str) -> bool {
|
pub fn run_pulse(config_path: &str) -> bool {
|
||||||
// 1. Check inbox
|
// 1. Check inbox
|
||||||
let inbox_output = Command::new("colony")
|
let inbox_output = match Command::new("colony").args(["inbox", "--json"]).output() {
|
||||||
.args(["inbox", "--json"])
|
Ok(o) => o,
|
||||||
.output()
|
Err(e) => {
|
||||||
.expect("failed to run colony inbox");
|
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_str = String::from_utf8_lossy(&inbox_output.stdout);
|
||||||
let inbox: Vec<serde_json::Value> = serde_json::from_str(&inbox_str).unwrap_or_default();
|
let inbox: Vec<serde_json::Value> = 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 <channel>` to get more context if needed. ");
|
prompt.push_str("Use `colony read <channel>` to get more context if needed. ");
|
||||||
prompt.push_str("Log what you did to memory/memory.md.\n");
|
prompt.push_str("Log what you did to memory/memory.md.\n");
|
||||||
|
|
||||||
// 5. Write prompt to temp file
|
// 5. Write prompt to temp file (per-agent path to avoid conflicts)
|
||||||
let prompt_path = "/tmp/colony-agent-prompt.md";
|
let agent_name = std::env::var("COLONY_AGENT").unwrap_or_else(|_| "agent".into());
|
||||||
std::fs::write(prompt_path, &prompt).expect("failed to write prompt file");
|
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
|
// 6. Invoke Claude Code
|
||||||
eprintln!("invoking claude with {} inbox items + heartbeat", inbox.len());
|
eprintln!("invoking claude with {} inbox items + heartbeat", inbox.len());
|
||||||
let claude_status = Command::new("claude")
|
let claude_ok = match Command::new("claude")
|
||||||
.args([
|
.args([
|
||||||
"--dangerously-skip-permissions",
|
"--dangerously-skip-permissions",
|
||||||
"-p",
|
"-p",
|
||||||
@@ -64,22 +76,24 @@ pub fn run_pulse(config_path: &str) -> bool {
|
|||||||
"--max-turns",
|
"--max-turns",
|
||||||
"20",
|
"20",
|
||||||
])
|
])
|
||||||
.status();
|
.status()
|
||||||
|
{
|
||||||
match claude_status {
|
|
||||||
Ok(s) if s.success() => {
|
Ok(s) if s.success() => {
|
||||||
eprintln!("claude completed successfully");
|
eprintln!("claude completed successfully");
|
||||||
|
true
|
||||||
}
|
}
|
||||||
Ok(s) => {
|
Ok(s) => {
|
||||||
eprintln!("claude exited with status: {}", s);
|
eprintln!("claude exited with status: {}", s);
|
||||||
|
false
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("failed to run claude: {}", e);
|
eprintln!("failed to run claude: {}", e);
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// 7. Ack inbox items
|
// 7. Only ack inbox items if Claude succeeded
|
||||||
if !inbox.is_empty() {
|
if claude_ok && !inbox.is_empty() {
|
||||||
let ids: Vec<String> = inbox.iter()
|
let ids: Vec<String> = inbox.iter()
|
||||||
.filter_map(|i| i["id"].as_i64().map(|id| id.to_string()))
|
.filter_map(|i| i["id"].as_i64().map(|id| id.to_string()))
|
||||||
.collect();
|
.collect();
|
||||||
@@ -89,6 +103,8 @@ pub fn run_pulse(config_path: &str) -> bool {
|
|||||||
.args(&ids)
|
.args(&ids)
|
||||||
.status();
|
.status();
|
||||||
}
|
}
|
||||||
|
} else if !claude_ok && !inbox.is_empty() {
|
||||||
|
eprintln!("claude failed — NOT acking {} inbox items (will retry next pulse)", inbox.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
|
|||||||
Reference in New Issue
Block a user