# 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: # # - `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`). # - `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`. { pkgs, ... }: let # Sets `bytes`, or exits 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 = '' bytes= [ -r /run/hive/memory-max-bytes ] && read -r bytes < /run/hive/memory-max-bytes case "$bytes" in "" | 0* | *[!0-9]* | ????????????????*) exit 0 ;; esac ''; envGenerator = pkgs.writeShellScript "hyperhive-memory-cap-env" '' ${readCap} 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} 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" ''; # 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; environment.etc."hyperhive/memory-cap.sh".source = profile; environment.extraInit = ". /etc/hyperhive/memory-cap.sh"; }