diff --git a/src/llm.rs b/src/llm.rs index e755d0a..00fca5f 100644 --- a/src/llm.rs +++ b/src/llm.rs @@ -1,27 +1,38 @@ // LLM utilities: model invocation and response parsing // -// Shared by digest, audit, enrich, consolidate, knowledge, and fact_mine. +// Calls claude CLI as a subprocess. Uses prctl(PR_SET_PDEATHSIG) +// so child processes die when the daemon exits, preventing orphans. use crate::store::Store; use regex::Regex; use std::fs; +use std::os::unix::process::CommandExt; use std::process::Command; /// Call a model via claude CLI. Returns the response text. +/// +/// Sets PR_SET_PDEATHSIG on the child so it gets SIGTERM if the +/// parent daemon exits — no more orphaned claude processes. fn call_model(model: &str, prompt: &str) -> Result { // Write prompt to temp file (claude CLI needs file input for large prompts) - // Use thread ID + PID to avoid collisions under parallel rayon calls let tmp = std::env::temp_dir().join(format!("poc-llm-{}-{:?}.txt", std::process::id(), std::thread::current().id())); fs::write(&tmp, prompt) .map_err(|e| format!("write temp prompt: {}", e))?; - let result = Command::new("claude") - .args(["-p", "--model", model, "--tools", "", "--no-session-persistence"]) - .stdin(fs::File::open(&tmp).map_err(|e| format!("open temp: {}", e))?) - .env_remove("CLAUDECODE") - .output(); + let result = unsafe { + Command::new("claude") + .args(["-p", "--model", model, "--tools", "", "--no-session-persistence"]) + .stdin(fs::File::open(&tmp).map_err(|e| format!("open temp: {}", e))?) + .env_remove("CLAUDECODE") + .pre_exec(|| { + // Kill this child if the parent dies + libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM); + Ok(()) + }) + .output() + }; fs::remove_file(&tmp).ok(); @@ -31,7 +42,8 @@ fn call_model(model: &str, prompt: &str) -> Result { Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } else { let stderr = String::from_utf8_lossy(&output.stderr); - Err(format!("claude exited {}: {}", output.status, stderr.trim())) + let preview: String = stderr.chars().take(500).collect(); + Err(format!("claude exited {}: {}", output.status, preview.trim())) } } Err(e) => Err(format!("spawn claude: {}", e)), @@ -75,7 +87,8 @@ pub(crate) fn parse_json_response(response: &str) -> Result