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
|
|
@ -115,6 +115,91 @@ 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
|
||||
/// (1024-based), not decimal — this matches. Pure integer arithmetic
|
||||
/// throughout (via `u128` headroom) rather than `f64`: a `MemoryMax=`
|
||||
/// 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.
|
||||
#[must_use]
|
||||
pub fn parse_bytes(value: &str) -> Option<u64> {
|
||||
if value == "infinity" || value.ends_with('%') {
|
||||
return None;
|
||||
}
|
||||
let (mantissa, exponent) = [('K', 1u32), ('M', 2), ('G', 3), ('T', 4)]
|
||||
.into_iter()
|
||||
.find_map(|(suffix, exp)| {
|
||||
value
|
||||
.strip_suffix([suffix, suffix.to_ascii_lowercase()])
|
||||
.map(|m| (m, exp))
|
||||
})
|
||||
.unwrap_or((value, 0));
|
||||
let scale = u128::from(1024u64.checked_pow(exponent)?);
|
||||
let (int_part, frac_part) = mantissa.split_once('.').unwrap_or((mantissa, ""));
|
||||
let whole: u128 = int_part.parse().ok()?;
|
||||
let mut bytes = whole.checked_mul(scale)?;
|
||||
if !frac_part.is_empty() {
|
||||
let frac_num: u128 = frac_part.parse().ok()?;
|
||||
let frac_denom = 10u128.checked_pow(u32::try_from(frac_part.len()).ok()?)?;
|
||||
bytes = bytes.checked_add(frac_num.checked_mul(scale)? / frac_denom)?;
|
||||
}
|
||||
u64::try_from(bytes).ok()
|
||||
}
|
||||
|
||||
/// Persist the full map. Sorted JSON output keeps meta-repo diffs
|
||||
/// minimal.
|
||||
///
|
||||
|
|
@ -323,4 +408,74 @@ mod tests {
|
|||
assert!(validate_memory_max(v).is_err(), "{v} should be rejected");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_bytes_handles_plain_and_suffixed_values() {
|
||||
assert_eq!(parse_bytes("1024"), Some(1024));
|
||||
assert_eq!(parse_bytes("4G"), Some(4 * 1024 * 1024 * 1024));
|
||||
assert_eq!(parse_bytes("512M"), Some(512 * 1024 * 1024));
|
||||
assert_eq!(parse_bytes("4096K"), Some(4096 * 1024));
|
||||
assert_eq!(parse_bytes("2T"), Some(2 * 1024 * 1024 * 1024 * 1024));
|
||||
assert_eq!(
|
||||
parse_bytes("1.5G"),
|
||||
Some(1024 * 1024 * 1024 + 512 * 1024 * 1024)
|
||||
);
|
||||
// Lowercase suffixes accepted, matching `is_byte_size`.
|
||||
assert_eq!(parse_bytes("4g"), Some(4 * 1024 * 1024 * 1024));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_bytes_rejects_infinity_and_percentages() {
|
||||
assert_eq!(parse_bytes("infinity"), None);
|
||||
assert_eq!(parse_bytes("50%"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_bytes_rejects_garbage() {
|
||||
for v in ["", "abc", "-8G", "8Gi"] {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -435,8 +435,10 @@ async fn handle_set_resource_limits(
|
|||
&hive.agent_memory_max,
|
||||
);
|
||||
Ok(HostResponse::messages(vec![format!(
|
||||
"{name}: CPUQuota={cpu} MemoryMax={mem} (restart the container if it is running \
|
||||
and the new caps need to take effect immediately)"
|
||||
"{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 agents rebuild {name}`) to actually track this change"
|
||||
)]))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,11 @@ let
|
|||
# runtime shell. Absent (option unset) → "unknown".
|
||||
hiveDisplayName = config.environment.variables.HYPERHIVE_HIVE_NAME or "unknown";
|
||||
swarmDisplayName = config.environment.variables.HYPERHIVE_SWARM_NAME or "unknown";
|
||||
# Effective per-agent MemoryMax=, in bytes, injected by meta.rs's
|
||||
# per-agent flake render (`hyperhive.claudeMemoryMaxBytes`). `null`
|
||||
# when the effective cap is unbounded ("infinity") or a RAM
|
||||
# percentage — see `resource_limits::effective_memory_bytes`.
|
||||
memoryMaxBytes = config.hyperhive.claudeMemoryMaxBytes;
|
||||
# Base claude-code environment applied to every agent regardless of OTEL.
|
||||
# Shipped via the managed settings `env` block so claude and `hivectl
|
||||
# choom` both inherit them without a launch wrapper.
|
||||
|
|
@ -45,6 +50,19 @@ let
|
|||
CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT = "1";
|
||||
# Tag remote-control sessions with "<hive>-<agent>" for identification.
|
||||
CLAUDE_REMOTE_CONTROL_SESSION_NAME_PREFIX = "${hiveDisplayName}-${userName}";
|
||||
}
|
||||
# Bun/JavaScriptCore (the runtime under claude-code's `.claude-wrapped`
|
||||
# binary) sizes its default heap ceiling off the container's visible
|
||||
# `/proc/meminfo`, which under nspawn is the *host's* physical RAM, not
|
||||
# the systemd `MemoryMax=` cgroup cap actually enforced on this
|
||||
# container — the cap lives on the host's outer cgroup and is invisible
|
||||
# from inside (confirmed: `/sys/fs/cgroup/memory.max` reads `max` at
|
||||
# every level from in here). That mismatch lets JSC's heap grow well
|
||||
# past the real wall before GC kicks in hard, causing choom sessions to
|
||||
# hang. Pin JSC's ceiling to 75% of the *actual* effective cap instead,
|
||||
# once it's known at build time.
|
||||
// lib.optionalAttrs (memoryMaxBytes != null) {
|
||||
BUN_JSC_forceRAMSize = toString (memoryMaxBytes * 75 / 100);
|
||||
};
|
||||
# OTEL environment Claude Code reads to export metrics/logs/traces.
|
||||
# Shipped via the managed claude settings json (below), which claude
|
||||
|
|
@ -186,6 +204,28 @@ in
|
|||
};
|
||||
};
|
||||
|
||||
# Build-time implementation surface for the JSC-heap-ceiling fix:
|
||||
# meta.rs's per-agent flake render injects this from the effective
|
||||
# `MemoryMax=` (per-agent `resource-limits.json` override, else the
|
||||
# hive-wide `services.hyperhive.agentMemoryMax`) — see
|
||||
# `resource_limits::effective_memory_bytes_from`. Not meant to be set
|
||||
# directly in an agent.nix, same convention as `hyperhive.otel.*`
|
||||
# above; the host option (or `hivectl agents set-resource-limits`) is
|
||||
# the real operator knob, and this only reflects the value baked in at
|
||||
# the agent's *last rebuild* — `set-resource-limits` still applies the
|
||||
# cgroup cap live via a drop-in reload, but this derived heap ceiling
|
||||
# needs a rebuild to pick up a new value.
|
||||
options.hyperhive.claudeMemoryMaxBytes = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.ints.positive;
|
||||
default = null;
|
||||
internal = true;
|
||||
description = ''
|
||||
Effective per-agent memory cap in bytes, when it's a plain
|
||||
byte-size value (null for an unbounded or percentage-based cap).
|
||||
Used to derive `BUN_JSC_forceRAMSize` in `baseClaudeEnv`.
|
||||
'';
|
||||
};
|
||||
|
||||
options.hyperhive.claudeMarketplaces = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ "anthropics/claude-plugins-official" ];
|
||||
|
|
|
|||
Loading…
Reference in a new issue