agents: read the memory cap at boot, not from the build
An agent's memory cap was rendered into its flake (`memoryMaxBytes` → `services.hyperhive.agent.claudeMemoryMaxBytes`), so the closure depended on it and a changed cap reached the JSC heap ceiling and the subagent `MemoryHigh=` only after a rebuild. hive-c0re now writes the effective `MemoryMax=` in bytes to the agent's runtime dir (`/run/hive/memory-max-bytes` inside) in `write_dropins`, from the same value the drop-in gets. The file is removed for `infinity` or a RAM percentage, and left alone when the drop-in is kept. Inside the container, `nix/agent-modules/memory-cap.nix` reads it at boot: an environment generator sets `BUN_JSC_forceRAMSize` (75%) for every unit and `machinectl shell`, `/etc/set-environment` exports it in login shells, and a unit generator writes `MemoryHigh=` (2/3) as a drop-in for `hive-subagent-daemon`. The render drops `memoryMaxBytes`, the agent option is removed (both spellings fail eval with a removal message), and `effective_memory_bytes_from` goes with its only caller. Part of #5081 (slice S0-2).
This commit is contained in:
parent
c8df666bf3
commit
0d619e215e
15 changed files with 416 additions and 287 deletions
|
|
@ -122,55 +122,6 @@ fn resolve(limits: &AgentLimits, hive_cpu_quota: &str, hive_memory_max: &str) ->
|
|||
(cpu, mem)
|
||||
}
|
||||
|
||||
/// Effective `MemoryMax=` string for `name` against an already-loaded
|
||||
/// map — the memory-only half of [`effective_from`], split out because
|
||||
/// [`effective_memory_bytes_from`] doesn't have a CPU quota to pass.
|
||||
/// No single-agent convenience wrapper (unlike [`effective`] /
|
||||
/// [`effective_from`]): every current caller already has a loaded map
|
||||
/// in hand, so one would just be dead code.
|
||||
#[must_use]
|
||||
fn effective_memory_max_from(
|
||||
limits: &BTreeMap<String, AgentLimits>,
|
||||
name: &str,
|
||||
hive_memory_max: &str,
|
||||
) -> String {
|
||||
limits
|
||||
.get(name)
|
||||
.and_then(|l| l.memory_max.clone())
|
||||
.unwrap_or_else(|| hive_memory_max.to_owned())
|
||||
}
|
||||
|
||||
/// Effective `MemoryMax=` for `name`, as a raw byte count, against an
|
||||
/// already-loaded map — the form `render_flake_with_lookup` uses so it
|
||||
/// doesn't re-read `resource-limits.json` once per agent (same
|
||||
/// reasoning as [`effective_from`] / `container_view::build_all`). No
|
||||
/// single-agent convenience wrapper, for the same reason as
|
||||
/// [`effective_memory_max_from`] — every current caller already has a
|
||||
/// loaded map in hand.
|
||||
///
|
||||
/// Logs a `warn!` naming `name` when the effective value is a RAM
|
||||
/// percentage: unlike `"infinity"`, a percentage IS a real, resolvable
|
||||
/// cap — resolving one just needs host `MemTotal`, which this module
|
||||
/// doesn't track — so today it degrades to `None` (no derived heap
|
||||
/// ceiling) rather than silently guessing wrong against `MemTotal`.
|
||||
#[must_use]
|
||||
pub fn effective_memory_bytes_from(
|
||||
limits: &BTreeMap<String, AgentLimits>,
|
||||
name: &str,
|
||||
hive_memory_max: &str,
|
||||
) -> Option<u64> {
|
||||
let mem = effective_memory_max_from(limits, name, hive_memory_max);
|
||||
if mem.ends_with('%') {
|
||||
tracing::warn!(
|
||||
agent = name,
|
||||
memory_max = %mem,
|
||||
"effective MemoryMax= is a RAM percentage; can't derive a JSC heap ceiling from it \
|
||||
without host MemTotal — leaving BUN_JSC_forceRAMSize unset for this agent"
|
||||
);
|
||||
}
|
||||
parse_bytes(&mem)
|
||||
}
|
||||
|
||||
/// Parse a systemd `MemoryMax=`-style byte-size value (`"4G"`, `"512M"`,
|
||||
/// a bare byte count, optionally with a decimal like `"1.5G"`) into a
|
||||
/// raw byte count. systemd's `K`/`M`/`G`/`T` suffixes are IEC binary
|
||||
|
|
@ -179,9 +130,8 @@ pub fn effective_memory_bytes_from(
|
|||
/// value is always a small non-negative decimal (enforced by
|
||||
/// [`is_plain_number`] upstream in [`validate_memory_max`]), so floats
|
||||
/// would only add rounding / sign-loss risk for no benefit. Returns
|
||||
/// `None` for `"infinity"` and percentages; see
|
||||
/// [`effective_memory_bytes_from`] for why those can't be turned into a
|
||||
/// byte count here.
|
||||
/// `None` for `"infinity"` and for percentages, which need the host's
|
||||
/// `MemTotal` to resolve.
|
||||
#[must_use]
|
||||
pub fn parse_bytes(value: &str) -> Option<u64> {
|
||||
if value == "infinity" || value.ends_with('%') {
|
||||
|
|
@ -532,46 +482,4 @@ mod tests {
|
|||
assert_eq!(parse_bytes(v), None, "{v} should not parse");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_memory_bytes_falls_back_to_hive_default() {
|
||||
// Empty map, i.e. no per-agent override — exercises the
|
||||
// "use the hive-wide default" arm end to end.
|
||||
let empty = BTreeMap::new();
|
||||
assert_eq!(
|
||||
effective_memory_bytes_from(&empty, "nobody-configured-this-agent", "4G"),
|
||||
Some(4 * 1024 * 1024 * 1024)
|
||||
);
|
||||
assert_eq!(
|
||||
effective_memory_bytes_from(&empty, "nobody-configured-this-agent", "infinity"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_memory_bytes_from_prefers_per_agent_override() {
|
||||
let map = BTreeMap::from([("sock".to_owned(), limits(None, Some("8G")))]);
|
||||
assert_eq!(
|
||||
effective_memory_bytes_from(&map, "sock", HIVE_MEM),
|
||||
Some(8 * 1024 * 1024 * 1024)
|
||||
);
|
||||
// A different agent not in the map still falls back to the
|
||||
// hive-wide default from the same loaded map (no re-read).
|
||||
assert_eq!(
|
||||
effective_memory_bytes_from(&map, "iris", HIVE_MEM),
|
||||
Some(4 * 1024 * 1024 * 1024)
|
||||
);
|
||||
}
|
||||
|
||||
/// A percentage cap is real and resolvable in principle, but this
|
||||
/// module has no host `MemTotal` to resolve it against — must
|
||||
/// degrade to `None` (no derived heap ceiling) rather than silently
|
||||
/// treating it as unbounded or guessing a number. `warn!` firing is
|
||||
/// exercised for coverage but not asserted on (no tracing test
|
||||
/// subscriber wired up here) — the `None` return is the contract.
|
||||
#[test]
|
||||
fn effective_memory_bytes_from_returns_none_for_percentage() {
|
||||
let map = BTreeMap::from([("sock".to_owned(), limits(None, Some("50%")))]);
|
||||
assert_eq!(effective_memory_bytes_from(&map, "sock", HIVE_MEM), None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
//! Per-container host-side config: the nspawn conf rewrite (bind mounts,
|
||||
//! network isolation, forwarded credentials), the systemd resource-limits
|
||||
//! drop-in, and the `write_dropins` verb that re-applies both.
|
||||
//! drop-in, the agent's copy of its memory cap, and the `write_dropins` verb
|
||||
//! that re-applies them.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
|
|
@ -16,20 +17,21 @@ use super::{
|
|||
};
|
||||
|
||||
/// Re-apply the per-container host-side config: nspawn flags (bind
|
||||
/// mounts etc.), the systemd resource-limits drop-in, and a daemon
|
||||
/// reload so both take effect on the next unit (re)start. Idempotent —
|
||||
/// the job queue's `WriteDropin` node, also folded into every `Swap`
|
||||
/// (rebuild is the reconcile verb).
|
||||
/// mounts etc.), the systemd resource-limits drop-in, the agent's copy of
|
||||
/// its memory cap, and a daemon reload so all of it takes effect on the next
|
||||
/// unit (re)start. Idempotent — the job queue's `WriteDropin` node, also
|
||||
/// folded into every `Swap` (rebuild is the reconcile verb).
|
||||
pub async fn write_dropins(name: &str, hive: &HiveEnv, paths: &AgentPaths) -> Result<()> {
|
||||
validate(name)?;
|
||||
let container = container_name(name);
|
||||
set_nspawn_flags(&container, &paths.agent, &paths.claude, &paths.notes).await?;
|
||||
if let Some((cpu_quota, memory_max)) = limits_to_apply(
|
||||
if let Some((cpu_quota, memory_max)) = stage_limits(
|
||||
name,
|
||||
&crate::resource_limits::resource_limits_path(),
|
||||
&limits_dropin_path(&container),
|
||||
&paths.agent,
|
||||
hive,
|
||||
) {
|
||||
)? {
|
||||
set_resource_limits(
|
||||
&container,
|
||||
&cpu_quota,
|
||||
|
|
@ -51,6 +53,60 @@ fn limits_dropin_path(container: &str) -> PathBuf {
|
|||
))
|
||||
}
|
||||
|
||||
/// File in the agent's runtime dir, `/run/hive/` in the container, holding the
|
||||
/// byte count of the `MemoryMax=` its drop-in carries.
|
||||
/// `nix/agent-modules/memory-cap.nix` reads it at boot under this name.
|
||||
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.
|
||||
fn stage_limits(
|
||||
agent_name: &str,
|
||||
limits_path: &Path,
|
||||
dropin_path: &Path,
|
||||
runtime_dir: &Path,
|
||||
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)?;
|
||||
}
|
||||
Ok(limits)
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// subagent ceiling.
|
||||
fn write_memory_cap(runtime_dir: &Path, agent_name: &str, memory_max: &str) -> Result<()> {
|
||||
let path = runtime_dir.join(MEMORY_CAP_FILE);
|
||||
let Some(bytes) = crate::resource_limits::parse_bytes(memory_max) else {
|
||||
if memory_max.ends_with('%') {
|
||||
tracing::warn!(
|
||||
agent = agent_name,
|
||||
memory_max,
|
||||
"effective MemoryMax= is a RAM percentage; the agent gets no byte count, so \
|
||||
no JSC heap ceiling and no subagent MemoryHigh="
|
||||
);
|
||||
}
|
||||
return match std::fs::remove_file(&path) {
|
||||
Err(e) if e.kind() != std::io::ErrorKind::NotFound => {
|
||||
Err(e).with_context(|| format!("remove {}", path.display()))
|
||||
}
|
||||
_ => Ok(()),
|
||||
};
|
||||
};
|
||||
std::fs::create_dir_all(runtime_dir)
|
||||
.with_context(|| format!("create agent runtime dir {}", runtime_dir.display()))?;
|
||||
let tmp = runtime_dir.join(format!(".{MEMORY_CAP_FILE}.tmp"));
|
||||
std::fs::write(&tmp, format!("{bytes}\n"))
|
||||
.with_context(|| format!("write {}", tmp.display()))?;
|
||||
std::fs::rename(&tmp, &path).with_context(|| format!("rename onto {}", path.display()))
|
||||
}
|
||||
|
||||
/// The `(CPUQuota, MemoryMax)` to write into `agent_name`'s limits
|
||||
/// drop-in, or `None` to leave the drop-in at `dropin_path` as it is.
|
||||
///
|
||||
|
|
@ -489,8 +545,9 @@ async fn set_nspawn_flags(
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
BindMount, QUEUE_CLIENT_ID_CREDENTIAL, QUEUE_SECRET_CREDENTIAL, bind_child_agent_dirs,
|
||||
bind_own_agent_dirs, holds_manage_root_agent, limits_to_apply, queue_agent_credentials,
|
||||
BindMount, MEMORY_CAP_FILE, QUEUE_CLIENT_ID_CREDENTIAL, QUEUE_SECRET_CREDENTIAL,
|
||||
bind_child_agent_dirs, bind_own_agent_dirs, holds_manage_root_agent, limits_to_apply,
|
||||
queue_agent_credentials, stage_limits,
|
||||
};
|
||||
use crate::coordinator::HiveEnv;
|
||||
|
||||
|
|
@ -707,4 +764,66 @@ mod tests {
|
|||
let applied = limits_to_apply("sock", &limits, &dropin, &HiveEnv::default());
|
||||
assert_eq!(applied, Some(("200%".to_owned(), "1G".to_owned())));
|
||||
}
|
||||
|
||||
/// Runs [`stage_limits`] against the files in `dir` and returns the
|
||||
/// drop-in's `MemoryMax=` and the agent's copy of it, `None` when absent.
|
||||
fn stage(dir: &tempfile::TempDir) -> (Option<String>, Option<String>) {
|
||||
let runtime = dir.path().join("runtime");
|
||||
let applied = stage_limits(
|
||||
"sock",
|
||||
&dir.path().join("resource-limits.json"),
|
||||
&dir.path().join("hyperhive-limits.conf"),
|
||||
&runtime,
|
||||
&HiveEnv::default(),
|
||||
)
|
||||
.expect("stage limits");
|
||||
let copy = std::fs::read_to_string(runtime.join(MEMORY_CAP_FILE)).ok();
|
||||
(applied.map(|(_, mem)| mem), copy)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_agent_reads_the_cap_its_dropin_carries() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let limits = dir.path().join("resource-limits.json");
|
||||
|
||||
// No overrides anywhere: the hive-wide default, 4G.
|
||||
assert_eq!(
|
||||
stage(&dir),
|
||||
(Some("4G".to_owned()), Some("4294967296\n".to_owned()))
|
||||
);
|
||||
|
||||
std::fs::write(&limits, r#"{"sock": {"memory_max": "16G"}}"#).expect("seed limits");
|
||||
assert_eq!(
|
||||
stage(&dir),
|
||||
(Some("16G".to_owned()), Some("17179869184\n".to_owned()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cap_with_no_byte_count_removes_the_agents_copy() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let limits = dir.path().join("resource-limits.json");
|
||||
assert_eq!(stage(&dir).1, Some("4294967296\n".to_owned()));
|
||||
|
||||
std::fs::write(&limits, r#"{"sock": {"memory_max": "infinity"}}"#).expect("seed limits");
|
||||
assert_eq!(stage(&dir), (Some("infinity".to_owned()), None));
|
||||
|
||||
assert_eq!(stage(&dir).1, None, "an absent copy stays absent");
|
||||
|
||||
std::fs::write(&limits, r#"{"sock": {"memory_max": "50%"}}"#).expect("seed limits");
|
||||
assert_eq!(stage(&dir), (Some("50%".to_owned()), None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_kept_dropin_keeps_the_agents_copy() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let limits = dir.path().join("resource-limits.json");
|
||||
std::fs::write(&limits, r#"{"sock": {"memory_max": "1G"}}"#).expect("seed limits");
|
||||
assert_eq!(stage(&dir).1, Some("1073741824\n".to_owned()));
|
||||
|
||||
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())));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,17 +88,7 @@ pub async fn sync_agents(hive: &HiveEnv, agents: &[AgentSpec]) -> Result<()> {
|
|||
let dir = crate::paths::meta_root();
|
||||
std::fs::create_dir_all(&dir).with_context(|| format!("create {}", dir.display()))?;
|
||||
|
||||
let new_flake = render_flake(
|
||||
&hive.hyperhive_flake,
|
||||
&hive.hyperhive_docs_flake,
|
||||
&hive.nixpkgs_flake,
|
||||
hive.claude_code_path.as_deref(),
|
||||
hive.dashboard_port,
|
||||
&hive.operator_pronouns,
|
||||
&hive.context_window_tokens,
|
||||
&hive.agent_memory_max,
|
||||
agents,
|
||||
)?;
|
||||
let new_flake = render_hive_flake(hive, agents)?;
|
||||
let flake_path = dir.join("flake.nix");
|
||||
let on_disk = std::fs::read_to_string(&flake_path).unwrap_or_default();
|
||||
let initial = !dir.join(".git").exists();
|
||||
|
|
@ -444,6 +434,21 @@ pub async fn commit_perms(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// [`render_flake`] with its inputs taken from `hive`, as [`sync_agents`]
|
||||
/// renders it.
|
||||
fn render_hive_flake(hive: &HiveEnv, agents: &[AgentSpec]) -> Result<String> {
|
||||
render_flake(
|
||||
&hive.hyperhive_flake,
|
||||
&hive.hyperhive_docs_flake,
|
||||
&hive.nixpkgs_flake,
|
||||
hive.claude_code_path.as_deref(),
|
||||
hive.dashboard_port,
|
||||
&hive.operator_pronouns,
|
||||
&hive.context_window_tokens,
|
||||
agents,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(
|
||||
clippy::too_many_arguments,
|
||||
reason = "many genuine flake inputs (source flakes, port, pronouns, tokens, \
|
||||
|
|
@ -457,7 +462,6 @@ fn render_flake(
|
|||
dashboard_port: u16,
|
||||
operator_pronouns: &str,
|
||||
context_window_tokens: &std::collections::HashMap<String, u64>,
|
||||
hive_memory_max: &str,
|
||||
agents: &[AgentSpec],
|
||||
) -> Result<String> {
|
||||
render_flake_with_lookup(
|
||||
|
|
@ -468,7 +472,6 @@ fn render_flake(
|
|||
dashboard_port,
|
||||
operator_pronouns,
|
||||
context_window_tokens,
|
||||
hive_memory_max,
|
||||
agents,
|
||||
agent_canonical_inputs,
|
||||
)
|
||||
|
|
@ -861,7 +864,6 @@ fn render_flake_with_lookup<F>(
|
|||
dashboard_port: u16,
|
||||
operator_pronouns: &str,
|
||||
context_window_tokens: &std::collections::HashMap<String, u64>,
|
||||
hive_memory_max: &str,
|
||||
agents: &[AgentSpec],
|
||||
lookup: F,
|
||||
) -> Result<String>
|
||||
|
|
@ -943,7 +945,7 @@ where
|
|||
let pronouns_escaped = operator_pronouns.replace('\\', "\\\\").replace('"', "\\\"");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" dashboardPort = {dashboard_port};\n operatorPronouns = \"{pronouns_escaped}\";\n mkAgent = {{ name, isManager, port, toolGroups ? null, capabilities ? null, memoryMaxBytes ? null }}:"
|
||||
" dashboardPort = {dashboard_port};\n operatorPronouns = \"{pronouns_escaped}\";\n mkAgent = {{ name, isManager, port, toolGroups ? null, capabilities ? null }}:"
|
||||
);
|
||||
out.push_str(
|
||||
r#" let
|
||||
|
|
@ -1094,7 +1096,6 @@ where
|
|||
# nspawn itself already call this container, so logs join to
|
||||
# both without a prefix transform.
|
||||
networking.hostName = "h-${name}";
|
||||
services.hyperhive.agent.claudeMemoryMaxBytes = memoryMaxBytes;
|
||||
programs.git.config.user = {
|
||||
name = name;
|
||||
email = "${name}@hyperhive.local";
|
||||
|
|
@ -1177,12 +1178,9 @@ where
|
|||
);
|
||||
// Propagated, never read as empty: an agent with no tool-groups entry
|
||||
// renders `toolGroups = null` and gets `AGENT_DEFAULT`, which fails
|
||||
// open for any agent whose explicit entry is narrower. Likewise an
|
||||
// agent with no resource-limits entry gets the hive `memoryMaxBytes`,
|
||||
// a larger heap ceiling than a tighter override.
|
||||
// open for any agent whose explicit entry is narrower.
|
||||
let tool_groups_map = crate::tool_groups::read()?;
|
||||
let capabilities_map = crate::capabilities::read()?;
|
||||
let resource_limits_map = crate::resource_limits::read()?;
|
||||
for spec in agents {
|
||||
// Emit `toolGroups = "group1,group2"` when the operator has
|
||||
// explicitly configured groups for this agent. Absent entry = null
|
||||
|
|
@ -1208,29 +1206,15 @@ where
|
|||
let joined = caps.join(",");
|
||||
format!("\"{joined}\"")
|
||||
};
|
||||
// Effective `MemoryMax=` for this agent (per-agent override, else
|
||||
// the hive-wide default), turned into a raw byte count so
|
||||
// the agent's claude-settings module can derive a JSC heap ceiling
|
||||
// (see `services.hyperhive.agent.claudeMemoryMaxBytes`). `null` when the
|
||||
// effective value is `"infinity"` or a RAM percentage — no
|
||||
// byte count to derive, dependent env var stays unset, same as
|
||||
// today's no-cap behavior.
|
||||
let memory_max_attr = crate::resource_limits::effective_memory_bytes_from(
|
||||
&resource_limits_map,
|
||||
&spec.name,
|
||||
hive_memory_max,
|
||||
)
|
||||
.map_or_else(|| "null".to_owned(), |b| b.to_string());
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" {} = mkAgent {{ name = \"{}\"; isManager = {}; port = {}; toolGroups = {}; capabilities = {}; memoryMaxBytes = {}; }};",
|
||||
" {} = mkAgent {{ name = \"{}\"; isManager = {}; port = {}; toolGroups = {}; capabilities = {}; }};",
|
||||
spec.name,
|
||||
spec.name,
|
||||
if spec.is_manager { "true" } else { "false" },
|
||||
spec.port,
|
||||
tool_groups_attr,
|
||||
capabilities_attr,
|
||||
memory_max_attr,
|
||||
);
|
||||
}
|
||||
out.push_str(" };\n };\n}\n");
|
||||
|
|
@ -1468,7 +1452,6 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
.expect("render flake");
|
||||
|
|
@ -1516,7 +1499,6 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
.expect("render flake");
|
||||
|
|
@ -1536,7 +1518,6 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
.expect("render flake");
|
||||
|
|
@ -1578,7 +1559,6 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
.expect("render flake");
|
||||
|
|
@ -1608,7 +1588,6 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
.expect("render flake");
|
||||
|
|
@ -1630,7 +1609,6 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
.expect("render flake");
|
||||
|
|
@ -1662,7 +1640,6 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[
|
||||
sample_spec("argus", false, 9001),
|
||||
sample_spec("bitburner", false, 9002),
|
||||
|
|
@ -1697,7 +1674,6 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
|_| Vec::new(),
|
||||
)
|
||||
|
|
@ -1737,7 +1713,6 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
.expect("render flake");
|
||||
|
|
@ -1784,7 +1759,6 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
.expect("render flake")
|
||||
|
|
@ -1966,7 +1940,6 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
.expect("render flake");
|
||||
|
|
@ -2004,7 +1977,6 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
.expect("render flake");
|
||||
|
|
@ -2050,7 +2022,6 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
.expect("render flake")
|
||||
|
|
@ -2144,7 +2115,6 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
.expect("render flake")
|
||||
|
|
@ -2220,7 +2190,6 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
.expect("render flake")
|
||||
|
|
@ -2246,32 +2215,20 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
/// The JSC-heap-ceiling fix needs the effective per-agent memory cap
|
||||
/// threaded into the flake as raw bytes, since nspawn hides the real
|
||||
/// cgroup cap from inside the container. An
|
||||
/// agent with no `resource-limits.json` override falls back to the
|
||||
/// hive-wide default passed to `render_flake` (`"4G"` in every test
|
||||
/// in this module) — this locks in the byte-count conversion end to
|
||||
/// end through the actual render path (not just `parse_bytes`
|
||||
/// in isolation).
|
||||
/// The agent reads its memory cap at boot from the copy
|
||||
/// `lifecycle::write_dropins` hands it, so the cap is no build input.
|
||||
#[test]
|
||||
fn render_flake_derives_memory_max_bytes_from_hive_default() {
|
||||
let out = render_flake(
|
||||
"github:example/hyperhive",
|
||||
"path:/nix/store/bbbb-hyperhive-docs-source",
|
||||
"path:/nix/store/aaaa-nixpkgs-source",
|
||||
None,
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
.expect("render flake");
|
||||
let want_bytes = 4u64 * 1024 * 1024 * 1024;
|
||||
assert!(
|
||||
out.contains(&format!("memoryMaxBytes = {want_bytes};")),
|
||||
"memoryMaxBytes must reflect the hive-wide default in bytes:\n{out}"
|
||||
fn render_hive_flake_is_identical_across_memory_caps() {
|
||||
let hive = |cap: &str| HiveEnv {
|
||||
agent_memory_max: cap.to_owned(),
|
||||
..HiveEnv::default()
|
||||
};
|
||||
let agents = [sample_spec("alice", false, 9001)];
|
||||
let small = render_hive_flake(&hive("4G"), &agents).expect("render 4G");
|
||||
let large = render_hive_flake(&hive("16G"), &agents).expect("render 16G");
|
||||
assert_eq!(
|
||||
small, large,
|
||||
"the rendered flake must not depend on the memory cap"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -433,10 +433,9 @@ async fn handle_set_resource_limits(
|
|||
&hive.agent_memory_max,
|
||||
)?;
|
||||
Ok(HostResponse::messages(vec![format!(
|
||||
"{name}: CPUQuota={cpu} MemoryMax={mem} — the cgroup cap itself is live now (restart \
|
||||
the container if it's running and needs the new cap immediately), but the derived \
|
||||
Claude/JSC heap ceiling is baked in at build time, so it needs a REBUILD \
|
||||
(`hivectl agent {name} rebuild`) to actually track this change"
|
||||
"{name}: CPUQuota={cpu} MemoryMax={mem} — the cgroup cap itself is live now; the \
|
||||
Claude/JSC heap ceiling and the subagent MemoryHigh= derived from it follow at the \
|
||||
container's next start (restart it if it's running and needs them now)"
|
||||
)]))
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue