llm: prevent orphaned subprocesses with PR_SET_PDEATHSIG
When the daemon is killed, spawned claude CLI processes survived as orphans and burned CPU indefinitely. Use pre_exec to set PR_SET_PDEATHSIG(SIGTERM) so children die with their parent. Also fix byte-index truncation of stderr preview (same UTF-8 panic pattern fixed in the daemon). Co-Authored-By: ProofOfConcept <poc@bcachefs.org>
This commit is contained in:
parent
5eb8a4eb6a
commit
d919bc3e51
1 changed files with 22 additions and 9 deletions
31
src/llm.rs
31
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<String, String> {
|
||||
// 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<String, String> {
|
|||
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<serde_json::Value, S
|
|||
}
|
||||
}
|
||||
|
||||
Err(format!("no valid JSON in response: {}...", &cleaned[..cleaned.len().min(200)]))
|
||||
let preview: String = cleaned.chars().take(200).collect();
|
||||
Err(format!("no valid JSON in response: {preview}..."))
|
||||
}
|
||||
|
||||
/// Get semantic keys (non-journal, non-system) for prompt context.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue