# The container's memory cap, read at boot rather than built in, so a changed # cap needs no rebuild. # # The cap is `MemoryMax=` on the host's `container@` unit, which nothing in # here can read: `/sys/fs/cgroup/memory.max` reads `max` at every level from # inside. hive-c0re writes its byte count to `/run/hive/memory-max-bytes` # before every start (`lifecycle::host_config`), and removes the file when the # cap is `infinity` or a RAM percentage. Two values are derived from it, and # neither is set without the file; `hyperhive-memory-cap-check` then logs a # warning at boot: # # - `BUN_JSC_forceRAMSize`, 75% of the cap. Bun/JavaScriptCore, the runtime # under claude-code, sizes its heap ceiling off `/proc/meminfo`, which under # nspawn is the host's RAM, so without it the heap grows well past the real # wall before GC kicks in hard and choom sessions hang. An environment # generator hands it to every unit of the container's systemd, `machinectl # shell` sessions (`hivectl choom`) included; `/etc/set-environment` exports # it in login shells that no unit starts (`nixos-container root-login`). # `hive-agent` and `hive-subagent-daemon` spawn `claude` as a plain child, # which inherits it, so neither unit may set or unset it. # - `MemoryHigh=` on `hive-subagent-daemon`, two thirds of the cap, as a # generator drop-in. The daemon spawns nested `claude` sessions as plain # children, so its cgroup already is the "all subagents" cgroup. `High`, not # `Max`: past it the kernel reclaims hard and the subagents slow down, so the # last third stays free for the agent's own turn, but a subagent that needs # more still gets it when the container has memory to spare. # # Tested by `checks.script-test-agent-memory-cap` and # `checks.module-eval-agent-memory`. { pkgs, ... }: let # Sets `bytes`, or runs `noCap` when there is no file or its first line is # not a plain byte count. A leading zero would read as octal, and 16 digits # or more could overflow `bytes * 75`. Shell builtins only. readCap = noCap: '' bytes= [ -r /run/hive/memory-max-bytes ] && read -r bytes < /run/hive/memory-max-bytes case "$bytes" in "" | 0* | *[!0-9]* | ????????????????*) ${noCap} ;; esac ''; envGenerator = pkgs.writeShellScript "hyperhive-memory-cap-env" '' ${readCap "exit 0"} printf 'BUN_JSC_forceRAMSize=%s\n' "$((bytes * 75 / 100))" ''; # `$1` is the generator's normal-priority output dir. A drop-in there merges # with the unit's own file in /etc. unitGenerator = pkgs.writeShellScript "hyperhive-memory-cap" '' ${readCap "exit 0"} dir="$1/hive-subagent-daemon.service.d" ${pkgs.coreutils}/bin/mkdir -p "$dir" printf '[Service]\nMemoryHigh=%s\n' "$((bytes * 2 / 3))" > "$dir/50-memory-high.conf" ''; # The generators run before the container's journal starts and an nspawn # container has no /dev/kmsg, so the boot warning for an unusable cap comes # from a unit. `<4>` is the journal's warning priority. check = pkgs.writeShellScript "hyperhive-memory-cap-check" '' ${readCap '' if [ -e /run/hive/memory-max-bytes ]; then why="does not start with a plain byte count" else why="is missing" fi echo "<4>/run/hive/memory-max-bytes $why: no BUN_JSC_forceRAMSize heap ceiling and no MemoryHigh= on hive-subagent-daemon; the host's MemoryMax= on the container still applies. hive-c0re writes no file for a cap of infinity or a RAM percentage." >&2 exit 0 ''} ''; # Sourced by every shell's `/etc/set-environment`, so POSIX sh only. profile = pkgs.writeText "hyperhive-memory-cap.sh" '' __hyperhive_cap=$(${envGenerator}) case "$__hyperhive_cap" in BUN_JSC_forceRAMSize=?*) export "$__hyperhive_cap" ;; esac unset __hyperhive_cap ''; in { environment.etc."systemd/system-environment-generators/50-hyperhive-memory-cap".source = envGenerator; systemd.generators.hyperhive-memory-cap = unitGenerator; systemd.services.hyperhive-memory-cap-check = { description = "Warn when the container's memory cap is unusable"; wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "oneshot"; ExecStart = check; }; }; environment.etc."hyperhive/memory-cap.sh".source = profile; environment.extraInit = ". /etc/hyperhive/memory-cap.sh"; }