diff --git a/src/agent_comms/commander/capacity.py b/src/agent_comms/commander/capacity.py index d62875f89b..68de8b53cf 100644 --- a/src/agent_comms/commander/capacity.py +++ b/src/agent_comms/commander/capacity.py @@ -1,15 +1,11 @@ -"""Commander load and forks (HARD RULE: as soon as the commander is overwhelmed it forks; a structural reflex). +"""Report commander load and live forks; no command refuses on capacity. -Load is measured from the files the fleet already writes, never from how the commander feels: -each open order counts 1, each overdue order 2 more, and every 10 unjudged edits 1. One commander -carries PER_COMMANDER load. Forks register by passing `--fork NAME` to order, which touches -forks/NAME.beat; a beat younger than BEAT_AGE is a live fork. While load needs more forks than are -live, order refuses the main commander's actions and rotate prints FORK NOW, so the only -way forward is forking a slice. +Each open order counts 1, each overdue order 2 more, and every 10 unjudged edits 1. +Forks register with `--fork NAME`; a heartbeat younger than BEAT_AGE is live. """ from __future__ import annotations -import math, os, sys, time +import os, time from dataclasses import dataclass from ..comms import Comms @@ -17,7 +13,6 @@ from .config import CommanderConfig FORKS = f'{CommanderConfig.installed().state}/forks' -PER_COMMANDER = 6 BEAT_AGE = 10 * 60 @@ -52,25 +47,10 @@ def of(cls, comms: Comms, commander: str) -> Load: def total(self) -> float: return self.owed + 2 * self.overdue + self.unjudged / 10 - @property - def needed(self) -> int: - return math.ceil(self.total / PER_COMMANDER) - 1 - - @property - def short(self) -> bool: - """Live forks do not cover the load: the commander must fork now.""" - return len(self.forks) < self.needed - def __str__(self) -> str: return (f'load {self.total:.0f} ({self.owed} open orders, {self.overdue} overdue, ' - f'{self.unjudged} unjudged edits) needs {self.needed} live fork(s), ' - f'{len(self.forks)} live {list(self.forks)}; fork a slice now (each fork passes --fork NAME to order)') - - def require(self) -> None: - if self.short: - sys.exit(f'REFUSED, nothing recorded: overloaded: {self}.') + f'{self.unjudged} unjudged edits), {len(self.forks)} live forks {list(self.forks)}') def main(a): - load = Load.of(order.COMMS, order.ME) - print(load if load.short else f'covered: load {load.total:.0f}, forks {list(load.forks)}') + print(Load.of(order.COMMS, order.ME)) diff --git a/src/agent_comms/commander/order.py b/src/agent_comms/commander/order.py index 92f37f8993..de6a972d2e 100644 --- a/src/agent_comms/commander/order.py +++ b/src/agent_comms/commander/order.py @@ -759,21 +759,12 @@ def fire(unit: str, why: str) -> None: print(f'{todo.id}: released from {unit}: {why}') -class LoadedVerb(RuledVerb): - """HARD RULE: overloaded means fork, not more orders from me; a fork is never blocked.""" - - def gate(self) -> None: - super().gate() - if not self.fork: - capacity.Load.of(COMMS, ME).require() - - class OrderVerb(CommanderVerb): """A verb of `order`.""" @dataclass(frozen=True, kw_only=True) -class SendVerb(OrderVerb, LoadedVerb): +class SendVerb(OrderVerb, RuledVerb): help = "record the order in Core and send it" unit: str = option("unit") due: str = option("due", help="HH:MM local time") @@ -916,7 +907,7 @@ def run(self) -> None: @dataclass(frozen=True, kw_only=True) -class SayVerb(OrderVerb, LoadedVerb): +class SayVerb(OrderVerb, RuledVerb): help = "a non-order message; refused if it orders work" units: str = option("units", metavar="UNIT[,UNIT]") body_file: str = option("body_file", metavar="BODYFILE") @@ -926,7 +917,7 @@ def run(self) -> None: @dataclass(frozen=True, kw_only=True) -class ProblemVerb(OrderVerb, LoadedVerb): +class ProblemVerb(OrderVerb, RuledVerb): help = "Core records the problem with its order, or nothing" text: str = option("text") to: str = option("--to") @@ -987,7 +978,7 @@ def run(self) -> None: @dataclass(frozen=True, kw_only=True) -class HuntVerb(OrderVerb, LoadedVerb): +class HuntVerb(OrderVerb, RuledVerb): help = "a hot-path read: clean unless --sent names the order it became" where: str = option("where", metavar="FILE:LINE") why: str = option("--why") diff --git a/src/agent_comms/commander/standing.py b/src/agent_comms/commander/standing.py index b8408a26b7..4232f88329 100644 --- a/src/agent_comms/commander/standing.py +++ b/src/agent_comms/commander/standing.py @@ -17,7 +17,7 @@ from ..threads import Thread from ..todos import FlatTrend, OrderProgress, PullRequests, StrikeTodo, Todo from ..turn_digests import TurnDigestRead, TurnsSince -from . import capacity, judge +from . import judge from .config import CommanderConfig, Repo CONFIG = CommanderConfig.installed() @@ -150,21 +150,6 @@ def enforce(self, comms: Comms, commander: str) -> None: self.assign(comms, commander, comms.registry.require(self.unit)) -@dataclass(frozen=True) -class ForkNow(UndatedStandingRule): - """The load on the asked Comms needs more live forks than there are.""" - - load: capacity.Load - - @classmethod - def violations(cls, comms: Comms, commander: str) -> tuple[ForkNow, ...]: - load = capacity.Load.of(comms, commander) - return (cls(load),) * load.short - - def line(self) -> str: - return f"FORK NOW: {self.load}" - - @dataclass(frozen=True) class Unjudged(ReasonedStandingRule, UndatedStandingRule): limit: ClassVar[int] = 30 diff --git a/tests/test_commander_capacity.py b/tests/test_commander_capacity.py new file mode 100644 index 0000000000..fe0ded1098 --- /dev/null +++ b/tests/test_commander_capacity.py @@ -0,0 +1,14 @@ +"""Capacity reports the existing load formula and live forks without policy advice.""" + +import pytest + + +@pytest.mark.parametrize("unjudged", [0, 1, 395, 1_000_000]) +def test_capacity_reports_load_and_forks(commander_script, unjudged): + script, _ = commander_script + load = script.capacity.Load(24, 14, unjudged, ("judge",)) + assert load.total == 24 + 2 * 14 + unjudged / 10 + assert str(load) == ( + f"load {load.total:.0f} (24 open orders, 14 overdue, {unjudged} unjudged edits), " + "1 live forks ['judge']" + ) diff --git a/tests/test_standing_rules.py b/tests/test_standing_rules.py index e1e5534aa9..beca82731b 100644 --- a/tests/test_standing_rules.py +++ b/tests/test_standing_rules.py @@ -215,34 +215,13 @@ def progress(): assert comms.goals.todos.get("P2").holder == "worker" -def test_fork_now_and_unjudged_follow_the_unjudged_edits(commander, standing): +def test_unjudged_follows_the_unjudged_edits(commander, standing): comms, state, module = standing - assert lines(comms, module.ForkNow) == lines(comms, module.Unjudged) == [] + assert lines(comms, module.Unjudged) == [] create(commander, 'P1') native_judge_edits(commander, module.judge, [dict(id=f'e{n}', at=200., path='a.py', hunks=[dict(old='before', new='after')]) for n in range(70)]) assert lines(comms, module.Unjudged) == ["UNJUDGED 70 edits -> judge or fork a judge slice"] - [fork] = lines(comms, module.ForkNow) - assert fork.startswith("FORK NOW: load 10 (1 open orders, 1 overdue, 70 unjudged edits) needs 1") - - -def test_fork_now_reads_the_load_of_the_bus_it_is_asked_about(commander, standing, tmp_path): - """Three overdue orders on one bus need a fork there; a second bus without orders carries no load.""" - from agent_comms.tool_caller import ThreadCaller - - comms, _, _ = standing - for problem in ("P1", "P2", "P3"): - create(commander, problem) - other = Comms(tmp_path / "other-bus") - other.registry.declare(other.registry.new_thread("lead", frozenset(), str(tmp_path), created_at=1.0)) - - def fork_now(bus): - read = CommsStandingRulesTool().apply(bus, ThreadCaller("lead")) - return [line for line in read.broken if line.startswith("FORK NOW")] - - assert [line.split(") ")[0] for line in fork_now(comms)] == [ - "FORK NOW: load 9 (3 open orders, 3 overdue, 0 unjudged edits"] - assert fork_now(other) == [] def test_rules_due_until_every_drawn_rule_is_answered_and_fresh(commander, monkeypatch):