feat: derive BUN_JSC_forceRAMSize from effective per-agent MemoryMax=
This commit is contained in:
parent
ee52ddc027
commit
bd14cc5c46
4 changed files with 288 additions and 35 deletions
|
|
@ -49,6 +49,35 @@ pub struct AgentSpec {
|
|||
pub port: u16,
|
||||
}
|
||||
|
||||
/// Stage every generated meta JSON file that exists: topology.json is
|
||||
/// regenerated by `reconcile` whenever the agent set changed;
|
||||
/// tool-groups/capabilities/resource-limits/roles are created lazily on
|
||||
/// first write (`set_groups`/`set_caps`/`set_limits`/role assignment) —
|
||||
/// absent means every agent is on defaults, no file needed. Without
|
||||
/// staging, an existing-but-untracked file (e.g. roles.json) shows up as
|
||||
/// untracked in the meta repo, which can confuse nix's dirty-tree fetch.
|
||||
/// `git add` is a no-op when content is unchanged.
|
||||
async fn stage_generated_meta_files(dir: &std::path::Path) -> Result<()> {
|
||||
for (path, name) in [
|
||||
(crate::topology::topology_path(), "topology.json"),
|
||||
(crate::tool_groups::tool_groups_path(), "tool-groups.json"),
|
||||
(
|
||||
crate::capabilities::capabilities_path(),
|
||||
"capabilities.json",
|
||||
),
|
||||
(
|
||||
crate::resource_limits::resource_limits_path(),
|
||||
"resource-limits.json",
|
||||
),
|
||||
(crate::topology::roles_path(), "roles.json"),
|
||||
] {
|
||||
if path.exists() {
|
||||
git(dir, &["add", name]).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Idempotently reconcile the meta repo with the current agent set.
|
||||
/// First call inits the git repo, runs `nix flake lock`, and lands a
|
||||
/// seed commit. Subsequent calls only touch `flake.nix` when the
|
||||
|
|
@ -66,6 +95,7 @@ pub async fn sync_agents(hive: &HiveEnv, agents: &[AgentSpec]) -> Result<()> {
|
|||
hive.dashboard_port,
|
||||
&hive.operator_pronouns,
|
||||
&hive.context_window_tokens,
|
||||
&hive.agent_memory_max,
|
||||
agents,
|
||||
);
|
||||
let flake_path = dir.join("flake.nix");
|
||||
|
|
@ -158,37 +188,7 @@ pub async fn sync_agents(hive: &HiveEnv, agents: &[AgentSpec]) -> Result<()> {
|
|||
for name in &ca_touched {
|
||||
let _ = git(&dir, &["add", "--", name]).await;
|
||||
}
|
||||
// Stage topology.json on every sync (regenerated by reconcile
|
||||
// above when the agent set changed). git add is a no-op when the
|
||||
// file content is unchanged.
|
||||
if crate::topology::topology_path().exists() {
|
||||
git(&dir, &["add", "topology.json"]).await?;
|
||||
}
|
||||
// Stage tool-groups.json when it exists. Created on first
|
||||
// `set_groups` call (operator-driven); absent = all agents on
|
||||
// their role defaults, no file needed. git add is a no-op when
|
||||
// the file is unchanged.
|
||||
if crate::tool_groups::tool_groups_path().exists() {
|
||||
git(&dir, &["add", "tool-groups.json"]).await?;
|
||||
}
|
||||
// Stage capabilities.json when it exists. Created on first
|
||||
// `set_caps` call; absent = no agents have extra capabilities.
|
||||
if crate::capabilities::capabilities_path().exists() {
|
||||
git(&dir, &["add", "capabilities.json"]).await?;
|
||||
}
|
||||
// Stage resource-limits.json when it exists. Created on first
|
||||
// `set_limits` call; absent = every agent on the hive-wide
|
||||
// CPU/memory defaults.
|
||||
if crate::resource_limits::resource_limits_path().exists() {
|
||||
git(&dir, &["add", "resource-limits.json"]).await?;
|
||||
}
|
||||
// Stage roles.json when it exists. Written by topology::write_roles /
|
||||
// reconcile_roles on first role assignment or manager default seeding.
|
||||
// Without this, roles.json appears as untracked in the meta repo
|
||||
// (visible in `git status`) which can confuse nix dirty-tree fetches.
|
||||
if crate::topology::roles_path().exists() {
|
||||
git(&dir, &["add", "roles.json"]).await?;
|
||||
}
|
||||
stage_generated_meta_files(&dir).await?;
|
||||
nix(&dir, &["flake", "lock"]).await?;
|
||||
if std::path::Path::new(&dir).join("flake.lock").exists() {
|
||||
git(&dir, &["add", "flake.lock"]).await?;
|
||||
|
|
@ -653,6 +653,7 @@ fn render_flake(
|
|||
dashboard_port: u16,
|
||||
operator_pronouns: &str,
|
||||
context_window_tokens: &std::collections::HashMap<String, u64>,
|
||||
hive_memory_max: &str,
|
||||
agents: &[AgentSpec],
|
||||
) -> String {
|
||||
render_flake_with_lookup(
|
||||
|
|
@ -662,6 +663,7 @@ fn render_flake(
|
|||
dashboard_port,
|
||||
operator_pronouns,
|
||||
context_window_tokens,
|
||||
hive_memory_max,
|
||||
agents,
|
||||
agent_canonical_inputs,
|
||||
)
|
||||
|
|
@ -938,6 +940,7 @@ 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,
|
||||
) -> String
|
||||
|
|
@ -1021,7 +1024,7 @@ where
|
|||
let pronouns_escaped = operator_pronouns.replace('\\', "\\\\").replace('"', "\\\"");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" dashboardPort = {dashboard_port};\n operatorPronouns = \"{pronouns_escaped}\";\n mkAgent = {{ name, isManager, port, parent ? null, toolGroups ? null, capabilities ? null }}:"
|
||||
" dashboardPort = {dashboard_port};\n operatorPronouns = \"{pronouns_escaped}\";\n mkAgent = {{ name, isManager, port, parent ? null, toolGroups ? null, capabilities ? null, memoryMaxBytes ? null }}:"
|
||||
);
|
||||
out.push_str(
|
||||
r#" let
|
||||
|
|
@ -1127,6 +1130,7 @@ where
|
|||
# `hyperhive.user.name` match the agent's identity
|
||||
# instead of the harness default of `"agent"`.
|
||||
hyperhive.user.name = name;
|
||||
hyperhive.claudeMemoryMaxBytes = memoryMaxBytes;
|
||||
programs.git.config.user = {
|
||||
name = name;
|
||||
email = "${name}@hyperhive.local";
|
||||
|
|
@ -1216,6 +1220,7 @@ where
|
|||
let topology = crate::topology::read();
|
||||
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 {
|
||||
let parent_attr = topology
|
||||
.get(&spec.name)
|
||||
|
|
@ -1245,9 +1250,22 @@ 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
|
||||
// `claude-settings.nix` can derive a JSC heap ceiling from it
|
||||
// (see `hyperhive.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 = {}; parent = {}; toolGroups = {}; capabilities = {}; }};",
|
||||
" {} = mkAgent {{ name = \"{}\"; isManager = {}; port = {}; parent = {}; toolGroups = {}; capabilities = {}; memoryMaxBytes = {}; }};",
|
||||
spec.name,
|
||||
spec.name,
|
||||
if spec.is_manager { "true" } else { "false" },
|
||||
|
|
@ -1255,6 +1273,7 @@ where
|
|||
parent_attr,
|
||||
tool_groups_attr,
|
||||
capabilities_attr,
|
||||
memory_max_attr,
|
||||
);
|
||||
}
|
||||
out.push_str(" };\n };\n}\n");
|
||||
|
|
@ -1545,6 +1564,7 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
// nixpkgs is a top-level input with an explicit URL; hyperhive
|
||||
|
|
@ -1588,6 +1608,7 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
assert!(
|
||||
|
|
@ -1607,6 +1628,7 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
assert!(
|
||||
|
|
@ -1636,6 +1658,7 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[
|
||||
sample_spec("argus", false, 9001),
|
||||
sample_spec("bitburner", false, 9002),
|
||||
|
|
@ -1668,6 +1691,7 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
|_| Vec::new(),
|
||||
);
|
||||
|
|
@ -1702,6 +1726,7 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
unsafe {
|
||||
|
|
@ -1739,6 +1764,7 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
let want = format!(
|
||||
|
|
@ -1780,6 +1806,7 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
};
|
||||
|
|
@ -1862,6 +1889,7 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
};
|
||||
|
|
@ -1946,6 +1974,7 @@ mod tests {
|
|||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
)
|
||||
};
|
||||
|
|
@ -1969,4 +1998,31 @@ mod tests {
|
|||
"github.enable = false must be injected when the host disables it:\n{disabled}"
|
||||
);
|
||||
}
|
||||
|
||||
/// 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).
|
||||
#[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",
|
||||
8000,
|
||||
"she/her",
|
||||
&std::collections::HashMap::new(),
|
||||
"4G",
|
||||
&[sample_spec("alice", false, 9001)],
|
||||
);
|
||||
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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue