nix: split module-eval into per-subsystem checks
The single module-eval derivation forced ~62 full nixosSystem fixtures live at once to compute its cases list: 10.6GB peak RSS / 5m25s to evaluate, by far the dominant cost in nix flake check. Splits it into 21 independent checks.module-eval-* derivations (1-7 fixtures each) sharing builders/helpers via module-eval/lib.nix, so no single derivation needs more than a handful of fixtures live at once. A few cases spanning two clusters carry a small duplicated fixture rather than threading shared state through lib.nix.
This commit is contained in:
parent
69b70a9c6f
commit
dc418a5223
24 changed files with 3760 additions and 2997 deletions
87
nix/module-eval/agent-memory.nix
Normal file
87
nix/module-eval/agent-memory.nix
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# `checks.module-eval-agent-memory` — see ./lib.nix for the shared
|
||||
# rationale (why this suite exists, naming convention, "evaluates
|
||||
# not executes").
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
self,
|
||||
nixosSystem,
|
||||
}:
|
||||
let
|
||||
inherit
|
||||
(import ./lib.nix {
|
||||
inherit
|
||||
pkgs
|
||||
lib
|
||||
self
|
||||
nixosSystem
|
||||
;
|
||||
})
|
||||
agent
|
||||
runGroup
|
||||
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 { };
|
||||
|
||||
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 sign is the whole property, and it is easy to write backwards:
|
||||
# a HIGHER OOMScoreAdjust is a MORE likely victim. So the subagent
|
||||
# daemon must be strictly above zero and the harness strictly below
|
||||
# it — swap the two and the kernel takes the agent's own turn first,
|
||||
# which is worse than setting nothing at all. Both nested `claude`
|
||||
# processes inherit their unit's value, so ordering the units orders
|
||||
# the sessions. Held as an inequality rather than two constants: what
|
||||
# must not drift is the order, not the magnitudes.
|
||||
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;
|
||||
in
|
||||
sub > 0 && own < 0 && sub > own;
|
||||
}
|
||||
{
|
||||
# The pair the case above only makes sense with: preferring this unit
|
||||
# as the victim is an improvement only if losing one subagent isn't
|
||||
# losing all of them. systemd's default `stop` would take the daemon
|
||||
# and every sibling session down with whichever process the kernel
|
||||
# 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";
|
||||
}
|
||||
];
|
||||
in
|
||||
runGroup "agent-memory" cases
|
||||
Loading…
Reference in a new issue