subagent: keep runs at +1000 when their cgroup is unavailable
Without a run cgroup (no delegation, setup failed, or one create failed) a subagent run was started plain at oom_score_adj 0, below the +500 the unit gave it before. Every run of the daemon now goes through the entry wrapper; it sets +1000 and enters the cgroup only when there is one. run_cgroup::entry takes the cgroup as an Option.
This commit is contained in:
parent
b31db6c7d9
commit
afe6026d44
5 changed files with 159 additions and 64 deletions
|
|
@ -65,7 +65,7 @@ async fn serve() -> Result<()> {
|
|||
.on_runtime(runtime)
|
||||
.publishing_to(hive_subagent_mcp::swarm_term::Publisher::spawn())
|
||||
.reading_models_from(hive_subagent_mcp::swarm_models::Reader::spawn())
|
||||
.in_run_cgroups(hive_agent_sock::run_cgroup::RunCgroups::init("runs")),
|
||||
.entering_runs(hive_agent_sock::run_cgroup::RunCgroups::init("runs")),
|
||||
);
|
||||
|
||||
// Serve the MCP tools over streamable-http forever. No background poll
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
|
|||
use std::sync::{Arc, Mutex, PoisonError};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use hive_agent_sock::run_cgroup::{RunCgroup, RunCgroups};
|
||||
use hive_agent_sock::run_cgroup::{self, RunCgroup, RunCgroups};
|
||||
use hive_claude::{Attach, Claude, Config, SessionStore};
|
||||
use hive_runtime::{
|
||||
AcpCommand, AcpRuntime, PermissionAsk, PermissionPolicy, Runtime as _, RuntimeSpec, TurnStop,
|
||||
|
|
@ -407,8 +407,11 @@ pub struct State {
|
|||
/// The agent's own model list, checked when the swarm sets none:
|
||||
/// [`available_models`] outside tests.
|
||||
local_models: fn() -> Option<String>,
|
||||
/// Whether runs start through this binary's entry wrapper
|
||||
/// ([`State::entering_runs`]).
|
||||
runs_entered: bool,
|
||||
/// Where each run gets its own cgroup, when this daemon was delegated
|
||||
/// one ([`State::in_run_cgroups`]).
|
||||
/// one.
|
||||
run_cgroups: Option<RunCgroups>,
|
||||
/// The `<n>` of the next run cgroup `<name>-<n>`.
|
||||
run_seq: AtomicU64,
|
||||
|
|
@ -467,6 +470,7 @@ impl State {
|
|||
term: None,
|
||||
models: None,
|
||||
local_models: available_models,
|
||||
runs_entered: false,
|
||||
run_cgroups: None,
|
||||
run_seq: AtomicU64::new(0),
|
||||
}
|
||||
|
|
@ -487,10 +491,12 @@ impl State {
|
|||
self
|
||||
}
|
||||
|
||||
/// Run each subagent run in its own cgroup under `cgroups`, at
|
||||
/// `oom_score_adj` +1000.
|
||||
/// Start every subagent run through this binary's entry wrapper, whose
|
||||
/// `main` calls `run_cgroup::exec_entry` first: at `oom_score_adj` +1000
|
||||
/// always, and in its own cgroup under `cgroups` when there are any.
|
||||
#[must_use]
|
||||
pub fn in_run_cgroups(mut self, cgroups: Option<RunCgroups>) -> Self {
|
||||
pub fn entering_runs(mut self, cgroups: Option<RunCgroups>) -> Self {
|
||||
self.runs_entered = true;
|
||||
self.run_cgroups = cgroups;
|
||||
self
|
||||
}
|
||||
|
|
@ -507,6 +513,33 @@ impl State {
|
|||
.ok()
|
||||
}
|
||||
|
||||
/// `config` with its program started through the entry wrapper, at
|
||||
/// [`RUN_OOM_SCORE_ADJ`] and inside `cgroup` if there is one; unchanged
|
||||
/// without [`State::entering_runs`]. `hive-claude` builds the `Command`
|
||||
/// itself and offers no `pre_exec`, hence the wrapper.
|
||||
fn entered_config(&self, config: &Config, cgroup: Option<&RunCgroup>) -> Config {
|
||||
let mut config = config.clone();
|
||||
if self.runs_entered {
|
||||
// `hive-claude`'s own default program.
|
||||
let program = config.program.take().unwrap_or_else(|| "claude".into());
|
||||
let (entry, env) = run_cgroup::entry(&program, RUN_OOM_SCORE_ADJ, cgroup);
|
||||
config.program = Some(entry);
|
||||
config.env.extend(env);
|
||||
}
|
||||
config
|
||||
}
|
||||
|
||||
/// [`State::entered_config`] for an ACP agent's command.
|
||||
fn entered_command(&self, mut command: AcpCommand, cgroup: Option<&RunCgroup>) -> AcpCommand {
|
||||
if self.runs_entered {
|
||||
let (entry, env) =
|
||||
run_cgroup::entry(Path::new(&command.command), RUN_OOM_SCORE_ADJ, cgroup);
|
||||
command.command = entry.to_string_lossy().into_owned();
|
||||
command.env.extend(env);
|
||||
}
|
||||
command
|
||||
}
|
||||
|
||||
/// Refuse a `model` outside the list in effect for this agent (see
|
||||
/// `check_model`). The swarm key is read only for a named model.
|
||||
async fn allow_model(&self, model: Option<&str>) -> anyhow::Result<()> {
|
||||
|
|
@ -1424,7 +1457,11 @@ fn start_on_runtime(
|
|||
) -> anyhow::Result<String> {
|
||||
if let SubagentRuntime::Acp { command, sessions } = &state.runtime {
|
||||
let cgroup = state.run_cgroup(name);
|
||||
let runtime = acp_runtime(command.clone(), sessions, name, cgroup.as_ref());
|
||||
let runtime = acp_runtime(
|
||||
state.entered_command(command.clone(), cgroup.as_ref()),
|
||||
sessions,
|
||||
name,
|
||||
);
|
||||
if let Some(archived) = runtime
|
||||
.archive()
|
||||
.map_err(|e| anyhow::anyhow!("archiving the prior `{name}` session failed: {e}"))?
|
||||
|
|
@ -1639,7 +1676,11 @@ fn continue_on_runtime(
|
|||
}
|
||||
state.begin_continue(name, dir);
|
||||
let cgroup = state.run_cgroup(name);
|
||||
let runtime = acp_runtime(command.clone(), sessions, name, cgroup.as_ref());
|
||||
let runtime = acp_runtime(
|
||||
state.entered_command(command.clone(), cgroup.as_ref()),
|
||||
sessions,
|
||||
name,
|
||||
);
|
||||
return spawn_and_track_acp(
|
||||
state,
|
||||
name,
|
||||
|
|
@ -1771,7 +1812,7 @@ fn spawn_and_track(
|
|||
// Every turn of the run, and all they start, share one cgroup, released
|
||||
// when the background task below ends.
|
||||
let cgroup = state.run_cgroup(name);
|
||||
let config = &entered_config(config, cgroup.as_ref());
|
||||
let config = &state.entered_config(config, cgroup.as_ref());
|
||||
let running = Claude::spawn(config, attach)
|
||||
.map_err(|e| anyhow::anyhow!("starting the subagent process failed: {e}"))?;
|
||||
state.track(name, running.cancel_handle().into());
|
||||
|
|
@ -1946,46 +1987,19 @@ fn acp_session_file(sessions: &Path, name: &str) -> PathBuf {
|
|||
/// own session (-500, `nix/agent-modules/agent-service.nix`).
|
||||
const RUN_OOM_SCORE_ADJ: i32 = 1000;
|
||||
|
||||
/// `config` with its program started inside `cgroup`, if there is one.
|
||||
/// `hive-claude` builds the `Command` itself and offers no `pre_exec`, so
|
||||
/// the program becomes this daemon's binary as a [`RunCgroup::entry`]
|
||||
/// wrapper.
|
||||
fn entered_config(config: &Config, cgroup: Option<&RunCgroup>) -> Config {
|
||||
let mut config = config.clone();
|
||||
if let Some(cgroup) = cgroup {
|
||||
// `hive-claude`'s own default program.
|
||||
let program = config.program.take().unwrap_or_else(|| "claude".into());
|
||||
let (entry, env) = cgroup.entry(&program, RUN_OOM_SCORE_ADJ);
|
||||
config.program = Some(entry);
|
||||
config.env.extend(env);
|
||||
}
|
||||
config
|
||||
}
|
||||
|
||||
/// [`entered_config`] for an ACP agent's command.
|
||||
fn entered_command(mut command: AcpCommand, cgroup: Option<&RunCgroup>) -> AcpCommand {
|
||||
if let Some(cgroup) = cgroup {
|
||||
let (entry, env) = cgroup.entry(Path::new(&command.command), RUN_OOM_SCORE_ADJ);
|
||||
command.command = entry.to_string_lossy().into_owned();
|
||||
command.env.extend(env);
|
||||
}
|
||||
command
|
||||
}
|
||||
|
||||
/// The ACP runtime `name`'s run drives, keeping its session id in
|
||||
/// [`acp_session_file`] and answering permission requests per
|
||||
/// [`acp_permits`]. Its agent process starts inside `cgroup`.
|
||||
/// [`acp_permits`].
|
||||
fn acp_runtime(
|
||||
command: AcpCommand,
|
||||
sessions: &Path,
|
||||
name: &str,
|
||||
cgroup: Option<&RunCgroup>,
|
||||
) -> AcpRuntime<hive_claude::NeverCompact> {
|
||||
let groups = hive_sh4re::permissions::effective_tool_groups();
|
||||
let permit: PermissionPolicy =
|
||||
Arc::new(move |ask: &PermissionAsk<'_>| acp_permits(ask, &groups));
|
||||
AcpRuntime::new(
|
||||
entered_command(command, cgroup),
|
||||
command,
|
||||
acp_session_file(sessions, name),
|
||||
permit,
|
||||
hive_claude::NeverCompact,
|
||||
|
|
@ -5244,4 +5258,55 @@ echo '{"type":"result","subtype":"success","is_error":false}'
|
|||
};
|
||||
assert!(acp_permits(&mcp, none));
|
||||
}
|
||||
|
||||
fn env_of<'a>(env: &'a [(String, String)], name: &str) -> Option<&'a str> {
|
||||
env.iter().find(|(n, _)| n == name).map(|(_, v)| v.as_str())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_run_without_a_cgroup_still_starts_at_the_subagent_score() {
|
||||
let state = State::new(PathBuf::from("/dev/null"), signal_url()).entering_runs(None);
|
||||
let config = state.entered_config(&Config::default(), None);
|
||||
assert_eq!(config.program.as_deref(), Some(Path::new("/proc/self/exe")));
|
||||
assert_eq!(
|
||||
env_of(&config.env, "HIVE_RUN_ENTRY_PROGRAM"),
|
||||
Some("claude")
|
||||
);
|
||||
assert_eq!(
|
||||
env_of(&config.env, "HIVE_RUN_ENTRY_OOM_SCORE_ADJ"),
|
||||
Some("1000")
|
||||
);
|
||||
assert_eq!(env_of(&config.env, "HIVE_RUN_ENTRY_CGROUP"), None);
|
||||
|
||||
let command = AcpCommand {
|
||||
command: "agent".to_owned(),
|
||||
args: Vec::new(),
|
||||
env: std::collections::BTreeMap::new(),
|
||||
api_key_env: None,
|
||||
};
|
||||
let command = state.entered_command(command, None);
|
||||
assert_eq!(command.command, "/proc/self/exe");
|
||||
assert_eq!(
|
||||
command
|
||||
.env
|
||||
.get("HIVE_RUN_ENTRY_PROGRAM")
|
||||
.map(String::as_str),
|
||||
Some("agent")
|
||||
);
|
||||
assert_eq!(
|
||||
command
|
||||
.env
|
||||
.get("HIVE_RUN_ENTRY_OOM_SCORE_ADJ")
|
||||
.map(String::as_str),
|
||||
Some("1000")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runs_are_started_plain_unless_the_daemon_enters_them() {
|
||||
let state = State::new(PathBuf::from("/dev/null"), signal_url());
|
||||
let config = state.entered_config(&Config::default(), None);
|
||||
assert_eq!(config.program, None);
|
||||
assert!(config.env.is_empty());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,3 +26,16 @@ fn the_entry_enters_the_cgroup_raises_the_score_and_execs_the_program() {
|
|||
assert_eq!(String::from_utf8_lossy(&out.stdout), "1000|unset|an-arg");
|
||||
assert_eq!(procs.expect("cgroup.procs written"), "0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_entry_without_a_cgroup_still_raises_the_score() {
|
||||
let out = Command::new(env!("CARGO_BIN_EXE_hive-subagent-daemon"))
|
||||
.env("HIVE_RUN_ENTRY_PROGRAM", "/bin/sh")
|
||||
.env_remove("HIVE_RUN_ENTRY_CGROUP")
|
||||
.env("HIVE_RUN_ENTRY_OOM_SCORE_ADJ", "1000")
|
||||
.args(["-c", "cat /proc/self/oom_score_adj"])
|
||||
.output()
|
||||
.expect("the binary runs");
|
||||
assert!(out.status.success(), "{out:?}");
|
||||
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "1000");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue