Watch
0
0
Fork
You've already forked hyperhive
0

agents: hand over the kept drop-in's cap, warn at boot without one

When hive-c0re can't read resource-limits.json and a limits drop-in
already exists, it keeps the drop-in, but it wrote no memory cap file. A
container whose runtime dir had no file yet (hive-c0re upgraded, /run
not cleared) then started with no heap ceiling and no subagent
MemoryHigh=, silently. `stage_limits` now writes the file on every
call: from the new limits, or from the last `MemoryMax=` in the kept
drop-in (`infinity` when it has none or can't be read).

A missing or malformed cap file at boot is no longer silent. The
generators run before the container's journal and an nspawn container
has no /dev/kmsg, so a oneshot `hyperhive-memory-cap-check.service`
logs a warning naming the path. The agent then runs with no JSC heap
ceiling and no subagent MemoryHigh=; the container's MemoryMax= still
applies.

module-eval-agent-memory gains a case pinning how the ceiling reaches
claude: `hive-agent` and `hive-subagent-daemon` neither set nor unset
BUN_JSC_forceRAMSize, both generators and the check unit are installed.

Part of #5081 (slice S0-2).
This commit is contained in:
atlas 2026-10-12 10:10:11 +02:00
commit 57955a6c53
6 changed files with 144 additions and 32 deletions

View file

@ -61,8 +61,8 @@ const MEMORY_CAP_FILE: &str = "memory-max-bytes";
/// [`limits_to_apply`], plus handing the memory cap to the agent through
/// [`MEMORY_CAP_FILE`] in `runtime_dir`. The container cannot read the cap
/// systemd enforces on it, so this file is how it learns the value it sizes
/// its heap and its subagents against. When the drop-in is kept as it is, so is
/// the file.
/// its heap and its subagents against. It is written on every call, from the
/// `MemoryMax=` the agent will start under: the new one, or the kept drop-in's.
fn stage_limits(
agent_name: &str,
limits_path: &Path,
@ -71,12 +71,39 @@ fn stage_limits(
hive: &HiveEnv,
) -> Result<Option<(String, String)>> {
let limits = limits_to_apply(agent_name, limits_path, dropin_path, hive);
if let Some((_, memory_max)) = &limits {
write_memory_cap(runtime_dir, agent_name, memory_max)?;
}
let memory_max = match &limits {
Some((_, memory_max)) => memory_max.clone(),
None => dropin_memory_max(agent_name, dropin_path),
};
write_memory_cap(runtime_dir, agent_name, &memory_max)?;
Ok(limits)
}
/// The `MemoryMax=` the drop-in at `path` applies: its last assignment, as
/// systemd reads it. `infinity`, systemd's default, when it has none, its last
/// one is empty, or the file can't be read.
fn dropin_memory_max(agent_name: &str, path: &Path) -> String {
let body = match std::fs::read_to_string(path) {
Ok(body) => body,
Err(e) => {
tracing::warn!(
agent = %agent_name,
path = %path.display(),
error = ?e,
"limits drop-in unreadable; the agent gets no memory cap byte count"
);
return "infinity".to_owned();
}
};
body.lines()
.rev()
.find_map(|line| line.trim().strip_prefix("MemoryMax="))
.map(str::trim)
.filter(|v| !v.is_empty())
.unwrap_or("infinity")
.to_owned()
}
/// Write `memory_max` into [`MEMORY_CAP_FILE`] as a byte count, or remove the
/// file when the value has none: `infinity`, or a RAM percentage, which needs
/// the host's `MemTotal` to resolve. With no file the agent sets no heap or
@ -815,15 +842,33 @@ mod tests {
}
#[test]
fn a_kept_dropin_keeps_the_agents_copy() {
fn a_kept_dropin_hands_the_agent_its_cap() {
let dir = tempfile::tempdir().expect("tempdir");
let limits = dir.path().join("resource-limits.json");
let dropin = dir.path().join("hyperhive-limits.conf");
let copy = dir.path().join("runtime").join(MEMORY_CAP_FILE);
std::fs::write(&limits, r#"{"sock": {"memory_max": "1G"}}"#).expect("seed limits");
assert_eq!(stage(&dir).1, Some("1073741824\n".to_owned()));
// Unreadable limits keep the drop-in; the copy follows the drop-in.
std::fs::write(&limits, TRUNCATED_LIMITS).expect("seed limits");
std::fs::write(dir.path().join("hyperhive-limits.conf"), "MemoryMax=1G\n")
.expect("seed drop-in");
assert_eq!(stage(&dir), (None, Some("1073741824\n".to_owned())));
std::fs::write(
&dropin,
"[Unit]\nConditionPathIsDirectory=/rt/sock\n\n[Service]\nRestartSec=5\nMemoryMax=2G\nCPUQuota=200%\n",
)
.expect("seed drop-in");
assert_eq!(stage(&dir), (None, Some("2147483648\n".to_owned())));
// A kept drop-in with no copy beside it, as after an upgrade.
std::fs::remove_file(&copy).expect("remove copy");
assert_eq!(stage(&dir), (None, Some("2147483648\n".to_owned())));
// The last assignment is the one systemd applies.
std::fs::write(&dropin, "[Service]\nMemoryMax=2G\nMemoryMax=3G\n").expect("seed drop-in");
assert_eq!(stage(&dir), (None, Some("3221225472\n".to_owned())));
// No `MemoryMax=` is systemd's default, `infinity`: no byte count.
std::fs::write(&dropin, "[Service]\nCPUQuota=200%\n").expect("seed drop-in");
assert_eq!(stage(&dir), (None, None));
}
}