Watch
0
0
Fork
You've already forked hyperhive
0

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:
atlas 2026-10-12 09:00:45 +02:00
commit 0d619e215e
15 changed files with 416 additions and 287 deletions

View file

@ -22,39 +22,17 @@ let
agentHarness
;
# The memory-pressure pair. `claudeMemoryMaxBytes` is the container's own
# cap, rendered per agent by meta.rs — the capped arm is the one every
# real deploy gets, the uncapped arm is a hive that set `infinity` or a
# RAM percentage and so hands the module no byte count to size against.
agentCapped = agent { claudeMemoryMaxBytes = 8589934592; };
agentUncapped = agent { };
agentDefault = agent { };
agentBashDaemon = machine: machine.systemd.services.hive-bash-daemon;
agentSubagentDaemon = machine: machine.systemd.services.hive-subagent-daemon;
cases = [
{
# Two thirds of the container's cap, and a SOFT ceiling: the daemon's
# cgroup holds every nested claude, so `MemoryHigh=` throttles the
# subagent set as a whole before the kernel picks a victim, while the
# absent `MemoryMax=` is what still lets one subagent use more than
# its share on a container that has the memory free. A hard cap here
# would trade the silent kill for a guaranteed wall, which is the
# shape this deliberately does not have.
name = "the subagent daemon throttles at two thirds of the container's memory";
ok =
let
c = (agentSubagentDaemon agentCapped).serviceConfig;
in
c.MemoryHigh == "5726623061" && !(c ? MemoryMax);
}
{
# What makes the case above able to fail. With no byte count for the
# container there is no fraction to take, and a hardcoded fallback
# would be a number about some other hive's machine — so the unit
# renders no ceiling at all rather than a fabricated one.
name = "an agent with no byte-valued memory cap renders no subagent ceiling";
ok = !((agentSubagentDaemon agentUncapped).serviceConfig ? MemoryHigh);
# The ceiling is sized at boot from the container's cap
# (../agent-modules/memory-cap.nix). One in the unit would be the cap at
# the last rebuild, and every agent would carry its own hive's number.
name = "the subagent daemon's memory ceiling is not built into its unit";
ok = !((agentSubagentDaemon agentDefault).serviceConfig ? MemoryHigh);
}
{
# The sign is the whole property, and it is easy to write backwards:
@ -68,8 +46,8 @@ let
name = "the OOM killer prefers a subagent over the agent's own session";
ok =
let
sub = (agentSubagentDaemon agentUncapped).serviceConfig.OOMScoreAdjust;
own = (agentHarness agentUncapped).serviceConfig.OOMScoreAdjust;
sub = (agentSubagentDaemon agentDefault).serviceConfig.OOMScoreAdjust;
own = (agentHarness agentDefault).serviceConfig.OOMScoreAdjust;
in
sub > 0 && own < 0 && sub > own;
}
@ -81,13 +59,13 @@ let
# chose, which is the blast radius that made the preference a bad
# trade in the first place.
name = "one subagent losing the OOM draw does not stop the daemon";
ok = (agentSubagentDaemon agentUncapped).serviceConfig.OOMPolicy == "continue";
ok = (agentSubagentDaemon agentDefault).serviceConfig.OOMPolicy == "continue";
}
{
# Each bash task is a process in the daemon's cgroup; under `stop` one
# OOM-killed task would end every running sibling task.
name = "one bash task losing the OOM draw does not stop the daemon";
ok = (agentBashDaemon agentUncapped).serviceConfig.OOMPolicy or null == "continue";
ok = (agentBashDaemon agentDefault).serviceConfig.OOMPolicy or null == "continue";
}
];
in