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
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue