diff --git a/nix/agent-modules/agent-service.nix b/nix/agent-modules/agent-service.nix index f3721311..db5af618 100644 --- a/nix/agent-modules/agent-service.nix +++ b/nix/agent-modules/agent-service.nix @@ -303,6 +303,16 @@ in SyslogIdentifier = binary; Restart = "on-failure"; RestartSec = 2; + # A LOWER OOMScoreAdjust means LESS likely to be killed, so this + # negative value puts the harness — and the agent's own `claude`, + # which it spawns as a child and which therefore inherits the + # value — last in line under memory pressure, behind + # `hive-subagent-daemon`'s `+500` (see ./mcp.nix). Losing a + # subagent costs one restartable task; losing this costs the + # session that was supervising it. Not `-1000`, which would + # exempt the harness entirely and leave the kernel with nothing + # to kill in a container whose only large process is this one. + OOMScoreAdjust = -500; # Per-service runtime dir owned by `User=` below; the harness # writes its regenerated claude-{mcp-config,settings,system-prompt} # files here (`paths::config_dir`). Separate from /run/hive, diff --git a/nix/agent-modules/mcp.nix b/nix/agent-modules/mcp.nix index a535e483..d5e9f5a6 100644 --- a/nix/agent-modules/mcp.nix +++ b/nix/agent-modules/mcp.nix @@ -363,6 +363,18 @@ in # `start`/`continue`/`status`/`interrupt` with no stdio fallback. Restart = "always"; RestartSec = 3; + # A HIGHER OOMScoreAdjust means MORE likely to be killed: the + # kernel adds it to the badness score it derives from the + # process's memory footprint, then kills the highest scorer. So + # the positive value here and the negative one on `hive-agent` + # put this unit first in line — a subagent turn can be retried, + # the agent's own turn is the thing everything else hangs off. + # `+500` against `hive-agent`'s `-500` is a full half of the + # score range apart, enough that a fat subagent outranks a fatter + # harness rather than merely tying with it. Every nested `claude` + # inherits the value from the daemon, so the ordering covers the + # whole subtree and not just this process. + OOMScoreAdjust = 500; User = userName; Group = userName; } diff --git a/nix/module-eval.nix b/nix/module-eval.nix index 82968aa9..ace26482 100644 --- a/nix/module-eval.nix +++ b/nix/module-eval.nix @@ -1385,6 +1385,23 @@ let 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 doctrine three glue files state, as a property a rewrite has to # keep: a client is defined by holding a certificate the store accepts,