From 5ed61fa93a735d8ba524b20c4acca49ba07d6c40 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 20:57:49 -0400 Subject: [PATCH 1/8] P286: derive objective strikes in Core and run them in the stall watcher --- src/agent_comms/child_process.py | 8 +- src/agent_comms/commander/standing.py | 1 + src/agent_comms/todos.py | 124 ++++++++++++++++++++++++-- 3 files changed, 125 insertions(+), 8 deletions(-) diff --git a/src/agent_comms/child_process.py b/src/agent_comms/child_process.py index 9ab938ebe4..0f1889024f 100644 --- a/src/agent_comms/child_process.py +++ b/src/agent_comms/child_process.py @@ -1065,6 +1065,10 @@ def close(self) -> None: os.close(fd) +class OrderCheckFailed(ValueError): + """A completed order check did not prove its expected result.""" + + class CheckPrinted(DeclaredFamily, affix="Printed"): """The count printed by a child, or the absence of an integer.""" @@ -1102,7 +1106,7 @@ def plus(self, amount: int) -> CheckPrinted: def require(self, expected: object) -> None: if self.count: - raise ValueError(f"The order check is {self.count} away from {expected}.") + raise OrderCheckFailed(f"The order check is {self.count} away from {expected}.") def project[T](self, counted: Callable[[int], T], uncounted: Callable[[NoIntegerPrinted], T]) -> T: @@ -1122,7 +1126,7 @@ def plus(self, amount: int) -> CheckPrinted: return self def require(self, expected: object) -> None: - raise ValueError(f"The order check cannot prove {expected}: it printed no integer.") + raise OrderCheckFailed(f"The order check cannot prove {expected}: it printed no integer.") def project[T](self, counted: Callable[[int], T], uncounted: Callable[[NoIntegerPrinted], T]) -> T: diff --git a/src/agent_comms/commander/standing.py b/src/agent_comms/commander/standing.py index 8b6b85eb00..5b9d07a955 100644 --- a/src/agent_comms/commander/standing.py +++ b/src/agent_comms/commander/standing.py @@ -190,6 +190,7 @@ def after_stall(cls, comms: Comms) -> None: def watch_pass(cls, opened_wire: Callable[[], AbstractContextManager[Comms]]) -> None: """The worker borrows the root for this pass, never while the timer is idle.""" with opened_wire() as comms: + comms.goals.todos.advance_objective_failures(comms.goals) cls.after_stall(comms) diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 896e0be57f..5590f500f6 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -27,7 +27,10 @@ from . import skills from .change_report import FileLine -from .child_process import BoundedRun, CheckPrinted, ChildResult, ExitedOutcome, NoIntegerPrinted +from .child_process import BoundedRun, CheckPrinted, ChildResult, ExitedOutcome, NoIntegerPrinted, OrderCheckFailed +from .bus_publication import AgentOrigin +from .external_transcript import ExternalTranscript +from .messages import InfoKind, Message from .command import Command from .declared_family import DeclaredFamily from .errors import ToolRefusedError @@ -37,7 +40,7 @@ from .channel_targets import CommandTarget from .goals import CheckedCommit, CountCheckResult, OrderCheck, ProblemGoalOrder from .recorded_time import EventTime, RecordedEventTime, UnrecordedEventTime -from .thread_identity import RecordedThreadIncarnation, ThreadIncarnation +from .thread_identity import RecordedThreadIncarnation, StallWatcherIncarnation, ThreadIncarnation from .threads import Thread from .typed_table import Column, Index, JsonStorage, TypedStore, TypedTable from .standing_rules import UndatedStandingRule @@ -110,6 +113,9 @@ async def checked(self, check: OrderCheck) -> ChildResult: ... @abstractmethod async def delivery_time(self, check): ... + def check_failed(self, goals: Goals, todo: Todo, error: OrderCheckFailed) -> None: + """Only a commit delivery can fail a check on delivered source.""" + def strikes( self, todo: Todo, order_id: str, actor: ThreadIncarnation ) -> tuple[StrikeTodo, ...]: @@ -137,6 +143,13 @@ def delivered(self) -> str: def work(self) -> str: return f"delivered commit {self.delivered}" + def check_failed(self, goals: Goals, todo: Todo, error: OrderCheckFailed) -> None: + now = time.time() + spent = (now - todo.order.checked().at.recorded_at("An order")) / 60 + FailedCheckStrike.make(todo, now, + f"reported done at {self.delivered} but its order check failed: {error}; " + f"cost {spent:.1f} minutes").record(goals, todo.id) + async def require_source(self, check: OrderCheck) -> None: raise ValueError("A commit delivery names the unit's delivered commit and the checked one.") @@ -1977,10 +1990,16 @@ def apply(self, current: Todo) -> Todo: current.require_assignment(self.previous) _text(self.reason, "Strike reason", 2000) StrikeReason(self.reason) - if any(self.repeats(recorded) for recorded in current.live_strikes(self.previous.owner)): + if any(self.repeats(recorded) for recorded in self.prior_strikes(current)): return current return replace(current, strikes=(*current.strikes, self), revision=current.revision + 1) + def prior_strikes(self, current: Todo) -> tuple[StrikeTodo, ...]: + return current.live_strikes(self.previous.owner) + + def matches_objective(self, strike: ObjectiveStrike) -> bool: + return False + def repeats(self, recorded: StrikeTodo) -> bool: return False @@ -1994,6 +2013,83 @@ def repeats(self, recorded: StrikeTodo) -> bool: and self.reason == recorded.reason) +@dataclass(frozen=True, kw_only=True) +class ObjectiveStrike(StrikeTodo, declared_name=None): + """An objective failure recorded in the existing strike history, once per order and kind.""" + + order_id: str + + @classmethod + def make(cls, todo: Todo, now: float, reason: str) -> Self: + return cls(todo.revision, StallWatcherIncarnation.watcher(), todo.held(), reason, + RecordedEventTime(now), order_id=todo.order.checked().id) + + @classmethod + def observe(cls, goals: Goals, todo: Todo, now: float) -> tuple[Self, ...]: + return () + + def prior_strikes(self, current: Todo) -> tuple[StrikeTodo, ...]: + # A void is not permission to repeat this same automatic failure on every pass. + return current.strikes + + def matches_objective(self, strike: ObjectiveStrike) -> bool: + return (self.__class__ == strike.__class__ and self.order_id == strike.order_id + and self.previous.owner_identity == strike.previous.owner_identity) + + def repeats(self, recorded: StrikeTodo) -> bool: + return recorded.matches_objective(self) + + def apply(self, current: Todo) -> Todo: + struck = super().apply(current) + if struck != current and struck.struck_out(self.previous.owner): + return ReleaseTodo(struck.revision, self.actor, self.previous, + StrikeReason(self.reason), self.at).apply(struck) + return struck + + def record(self, goals: Goals, problem: str) -> Todo: + changed = goals.apply_problem(problem, self) + if changed.revision == self.expected_revision: + return changed + count = len(changed.live_strikes(self.previous.owner)) + line = (f"Core strike {count} on {problem} order {self.order_id}: " + f"{self.previous.owner} {self.reason}" + + (f"; {problem} taken away" if not changed.holder else "")) + goals.bus.publisher.publish_ordinary( + Message(sender=changed.creator, target="#comms", body=line, type=InfoKind), + origin=AgentOrigin()) + return changed + + +@dataclass(frozen=True, kw_only=True) +class MissedDeadlineStrike(ObjectiveStrike): + @classmethod + def observe(cls, goals: Goals, todo: Todo, now: float) -> tuple[Self, ...]: + if not todo.overdue(now): + return () + due = todo.deadline.recorded_at("An order deadline") + return (cls.make(todo, now, f"missed its deadline; cost {(now - due) / 60:.1f} minutes late"),) + + +@dataclass(frozen=True, kw_only=True) +class MissedFirstCallStrike(ObjectiveStrike): + @classmethod + def observe(cls, goals: Goals, todo: Todo, now: float) -> tuple[Self, ...]: + order = todo.order.checked() + due = order.first_call_deadline.recorded_at("A first-call deadline") + if now <= due: + return () + called = ExternalTranscript(goals.root, todo.held().owner_identity).last_tool_call() + if called.since(order.at.recorded_at("An order")): + return () + return (cls.make(todo, now, + f"made no first tool call; cost {(now - due) / 60:.1f} minutes late"),) + + +@dataclass(frozen=True, kw_only=True) +class FailedCheckStrike(ObjectiveStrike): + """DeliverTodo records this failure at the check boundary, before rejecting done.""" + + @dataclass(frozen=True) class VerdictAudience: """The threads a send is for; a send reaching many threads wakes no thread outside it.""" @@ -2274,7 +2370,11 @@ def prepare(self, goals, current): current.state.require_open(current) current.order.require_delivery(self.order_id) verified = self.delivery.verified(current, goals.registry) - return replace(self, delivery=asyncio.run(verified)) + try: + return replace(self, delivery=asyncio.run(verified)) + except OrderCheckFailed as error: + self.delivery.check_failed(goals, current, error) + raise def apply(self, current): current.require_revision(self.expected_revision) @@ -2650,10 +2750,22 @@ def for_goal(self, owner: ThreadIncarnation, goal_id: str) -> tuple[Todo, ...]: "json_extract(assignment,'$.owner')=? AND json_extract(assignment,'$.owner_created')=? " "AND json_extract(assignment,'$.generation')=?", (owner.name, owner.created_at, goal_id)) + def held_orders(self) -> tuple[Todo, ...]: + return self._open("assignment IS NOT NULL", (), OpenTodoState) + + def advance_objective_failures(self, goals: Goals) -> None: + now = time.time() + for observed in self.held_orders(): + for failure in ObjectiveStrike.members_with(ObjectiveStrike): + current = self.get(observed.id) + if not current.holder or not current.pending: + break + for strike in failure.observe(goals, current, now): + strike.record(goals, current.id) + def stalled(self, storage: StorageRule) -> tuple[Todo, ...]: """Observe active held orders without changing owners, strikes or releases.""" - held = tuple(todo for todo in self._open( - "assignment IS NOT NULL", (), OpenTodoState) if todo.pending) + held = tuple(todo for todo in self.held_orders() if todo.pending) pulls = PullRequests(held) return tuple(todo for todo in held if OrderProgress.from_source(source := ProgressSource.of(todo, storage, pulls)) From 8c353caa87408e495f3023d6ca5be50f681a6c77 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 21:02:42 -0400 Subject: [PATCH 2/8] P286: use original tool timing and limit failed-check strikes to delivered source --- src/agent_comms/external_transcript.py | 10 ++++ src/agent_comms/todos.py | 11 ++-- tests/test_objective_strikes.py | 78 ++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 tests/test_objective_strikes.py diff --git a/src/agent_comms/external_transcript.py b/src/agent_comms/external_transcript.py index 36495b72d2..4770ad1602 100644 --- a/src/agent_comms/external_transcript.py +++ b/src/agent_comms/external_transcript.py @@ -196,6 +196,7 @@ class ExternalEventRow(TypedTable): foreign_keys = (ForeignKey(("thread", "seq"), ExternalItemRow, ("thread", "seq"), on_delete="CASCADE"),) indexes = (Index(("thread", "kind", "tool", "timestamp", "call_id")), + Index(("thread", "kind", "timestamp")), Index(("thread", "kind", "seq", "ordinal")),) @classmethod @@ -404,6 +405,15 @@ def source_paths(cls, root: Path) -> tuple[Path, ...]: database = cls.path_in(root) return database, database.with_name(database.name + "-wal") + def tool_call_between(self, since: float, through: float) -> bool: + """Original tool timestamps decide first-call timeliness, not the last watcher tick.""" + with self.snapshot() as connection: + return bool(connection.execute( + f'SELECT EXISTS(SELECT 1 FROM "{ExternalEventRow.declared_name}" ' + 'WHERE thread=? AND kind=? AND timestamp>=? AND timestamp<=?)', + (self.lookup, FieldCodec.encode(ToolStartTranscript), since, through), + ).fetchone()[0]) + def last_tool_call(self) -> EventTime: with self.snapshot() as connection: timestamp = connection.execute( diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 5590f500f6..b47bf16d84 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -144,6 +144,8 @@ def work(self) -> str: return f"delivered commit {self.delivered}" def check_failed(self, goals: Goals, todo: Todo, error: OrderCheckFailed) -> None: + if self.commit != self.delivered: + return # A failed integration check is not evidence that the unit's commit failed. now = time.time() spent = (now - todo.order.checked().at.recorded_at("An order")) / 60 FailedCheckStrike.make(todo, now, @@ -2067,7 +2069,8 @@ def observe(cls, goals: Goals, todo: Todo, now: float) -> tuple[Self, ...]: if not todo.overdue(now): return () due = todo.deadline.recorded_at("An order deadline") - return (cls.make(todo, now, f"missed its deadline; cost {(now - due) / 60:.1f} minutes late"),) + return (cls.make(todo, now, + f"missed its order deadline without a delivered commit; cost {(now - due) / 60:.1f} minutes late"),) @dataclass(frozen=True, kw_only=True) @@ -2078,11 +2081,11 @@ def observe(cls, goals: Goals, todo: Todo, now: float) -> tuple[Self, ...]: due = order.first_call_deadline.recorded_at("A first-call deadline") if now <= due: return () - called = ExternalTranscript(goals.root, todo.held().owner_identity).last_tool_call() - if called.since(order.at.recorded_at("An order")): + transcript = ExternalTranscript(goals.root, todo.held().owner_identity) + if transcript.tool_call_between(order.at.recorded_at("An order"), due): return () return (cls.make(todo, now, - f"made no first tool call; cost {(now - due) / 60:.1f} minutes late"),) + f"made no first tool call before its first-call deadline; cost {(now - due) / 60:.1f} minutes late"),) @dataclass(frozen=True, kw_only=True) diff --git a/tests/test_objective_strikes.py b/tests/test_objective_strikes.py new file mode 100644 index 0000000000..de552567cb --- /dev/null +++ b/tests/test_objective_strikes.py @@ -0,0 +1,78 @@ +"""One real TodoStore journey drives every objective failure and its public line.""" +import subprocess +import time +from contextlib import nullcontext +from dataclasses import replace + +import pytest + +from agent_comms.channel_targets import CommandTarget +from agent_comms.child_process import OrderCheckFailed +from agent_comms.commander.standing import UnownedWheelSpin +from agent_comms.goals import CountCheckResult, OrderCheck +from agent_comms.recorded_time import RecordedEventTime +from agent_comms.todos import AssignTodo, CommitDelivery, ObjectiveStrike +from agent_comms.tools import CommsOrderDoneTool +from test_commander_problem_orders import create, working + + +def test_objective_family_records_once_posts_minutes_and_uses_existing_takeaway( + commander, tmp_path): + comms, caller = commander + store = comms.goals.todos + now = time.time() + todo = create(commander, 'P1', deadline=now + 120) + with store.writing() as db: + replace(todo, order=replace(todo.order, at=RecordedEventTime(now - 180), + first_call_deadline=RecordedEventTime(now - 60))).upsert(db) + + def watch(): + UnownedWheelSpin.watch_pass(lambda: nullcontext(comms)) + + def lines(): + return [message.body for message in comms.views.history_page( + CommandTarget.named('#comms'), viewer='lead').messages + if message.body.startswith('Core strike')] + + watch() + first = store.get('P1') + assert len(first.strikes) == 1 and first.holder == 'worker' + assert 'made no first tool call' in lines()[0] and 'minutes late' in lines()[0] + watch() + assert store.get('P1') == first and len(lines()) == 1 + store.apply('P1', AssignTodo(first.revision, first.held(), RecordedEventTime(now - 120), + first.order, first.goal)) + watch() + taken = store.get('P1') + assert taken.holder is None and len(taken.strikes) == 2 and len(taken.released) == 1 + assert any('P1 taken away' in line and 'missed its order deadline' in line for line in lines()) + watch() + assert store.get('P1') == taken and len(lines()) == 2 + + repo, origin = tmp_path / 'source', tmp_path / 'origin.git' + repo.mkdir() + subprocess.run(['git', 'init', '-q', '--bare', str(origin)], check=True) + for args in (('init', '-q', '-b', 'main'), ('remote', 'add', 'origin', str(origin))): + subprocess.run(['git', '-C', str(repo), *args], check=True) + (repo / 'count.txt').write_text('0\n') + for args in (('add', '.'), ('-c', 'user.name=t', '-c', 'user.email=t@t', + 'commit', '-q', '-m', 'delivered failing source'), + ('push', '-q', 'origin', 'main')): + subprocess.run(['git', '-C', str(repo), *args], check=True) + sha = subprocess.check_output(['git', '-C', str(repo), 'rev-parse', 'HEAD'], text=True).strip() + todo = create(commander, 'P2', deadline=now + 3600) + with store.writing() as db: + replace(todo, order=replace(todo.order, at=RecordedEventTime(now - 120), + first_call_deadline=RecordedEventTime(now + 60), + check=OrderCheck(str(repo), 'cat count.txt', CountCheckResult(1)))).upsert(db) + done = CommsOrderDoneTool(problem='P2', order_id=todo.order.id, + delivery=CommitDelivery(commit=sha, delivered_commit=sha), claims=working(comms)) + for _ in range(2): + with pytest.raises(OrderCheckFailed): + done.apply(comms, caller) + assert len(store.get('P2').strikes) == 1 + assert len(lines()) == 3 + assert any(sha in line and 'order check failed' in line and 'minutes' in line for line in lines()) + found = {type(strike) for pid in ('P1', 'P2') for strike in store.get(pid).strikes} + assert found == set(ObjectiveStrike.members_with(ObjectiveStrike)) + assert store.get('P2').pending # A failed check never becomes delivered work. From 579aaa01e6d6e30f512cff9ce433b15864bbd46e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 21:11:31 -0400 Subject: [PATCH 3/8] P286: share post-write publication and eliminate the competing late-done strike path --- src/agent_comms/commander/order.py | 3 -- src/agent_comms/todos.py | 53 ++++++++++++++++---------- tests/test_commander_problem_orders.py | 4 +- tests/test_objective_strikes.py | 23 ++++++++++- 4 files changed, 58 insertions(+), 25 deletions(-) diff --git a/src/agent_comms/commander/order.py b/src/agent_comms/commander/order.py index 33e6ee33de..9b149d7c38 100644 --- a/src/agent_comms/commander/order.py +++ b/src/agent_comms/commander/order.py @@ -601,9 +601,6 @@ def done(self, pid, sha, screenshot='', seen='', repo=''): claim = MergedClaim(delivery.delivered) self.call(CommsOrderDoneTool(problem=pid, order_id=todo.order.id, delivery=delivery, claims=(claim,))) - delivered_todo = self.comms.goals.todos.get(pid) - for late in delivered_todo.strikes[len(todo.strikes):]: - self.posted(unit, delivered_todo, late) print(f'{unit}: order {todo.order.id} delivered as {delivery.delivered if not operation else sha}') def first_calls(self, held, activity): diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index b47bf16d84..db90040ea4 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -166,10 +166,7 @@ def strikes( self, todo: Todo, order_id: str, actor: ThreadIncarnation ) -> tuple[StrikeTodo, ...]: """A commit delivered after the order's deadline strikes its holder once.""" - lateness = self.at.lateness(todo.deadline) - reason = (f"{todo.id} order {order_id} {lateness} as {self.delivered} " - f"(caught by {actor.name} at review)") - return (StrikeTodo(todo.revision, actor, todo.held(), reason),) * lateness.late() + return MissedDeadlineStrike.late(todo, self.at, f"as delivered commit {self.delivered}") @dataclass(frozen=True, kw_only=True) @@ -1938,8 +1935,13 @@ def prepare(self, goals: Goals, current: Todo) -> Self: return self def record(self, goals: Goals, problem: str) -> Todo: - """Write this operation; one that also revises the holder's Goal routes through it.""" - return goals.apply_problem(problem, self) + """Write this operation; publish its effects only after the canonical row was written.""" + changed = goals.apply_problem(problem, self) + self.after_record(goals, changed) + return changed + + def after_record(self, goals: Goals, changed: Todo) -> None: + """Most operations have no automatic public effect.""" @abstractmethod def apply(self, current: Todo) -> Todo: ... @@ -2048,29 +2050,34 @@ def apply(self, current: Todo) -> Todo: StrikeReason(self.reason), self.at).apply(struck) return struck - def record(self, goals: Goals, problem: str) -> Todo: - changed = goals.apply_problem(problem, self) + def after_record(self, goals: Goals, changed: Todo) -> None: if changed.revision == self.expected_revision: - return changed + return count = len(changed.live_strikes(self.previous.owner)) - line = (f"Core strike {count} on {problem} order {self.order_id}: " + line = (f"Core strike {count} on {changed.id} order {self.order_id}: " f"{self.previous.owner} {self.reason}" - + (f"; {problem} taken away" if not changed.holder else "")) + + (f"; {changed.id} taken away" if not changed.holder else "")) goals.bus.publisher.publish_ordinary( Message(sender=changed.creator, target="#comms", body=line, type=InfoKind), origin=AgentOrigin()) - return changed @dataclass(frozen=True, kw_only=True) class MissedDeadlineStrike(ObjectiveStrike): @classmethod - def observe(cls, goals: Goals, todo: Todo, now: float) -> tuple[Self, ...]: - if not todo.overdue(now): + def late(cls, todo: Todo, at: EventTime, detail: str) -> tuple[Self, ...]: + lateness = at.lateness(todo.deadline) + if not lateness.late(): return () - due = todo.deadline.recorded_at("An order deadline") - return (cls.make(todo, now, - f"missed its order deadline without a delivered commit; cost {(now - due) / 60:.1f} minutes late"),) + strike = cls.make(todo, at.recorded_at("A deadline failure"), + f"{todo.id} order {todo.order.checked().id} {lateness} {detail}; " + f"missed its order deadline; cost {lateness.seconds / 60:.1f} minutes late") + return () if any(strike.repeats(prior) for prior in todo.strikes) else (strike,) + + @classmethod + def observe(cls, goals: Goals, todo: Todo, now: float) -> tuple[Self, ...]: + return (cls.late(todo, RecordedEventTime(now), "without a delivered commit") + if todo.pending else ()) @dataclass(frozen=True, kw_only=True) @@ -2374,21 +2381,27 @@ def prepare(self, goals, current): current.order.require_delivery(self.order_id) verified = self.delivery.verified(current, goals.registry) try: - return replace(self, delivery=asyncio.run(verified)) + delivered = asyncio.run(verified) except OrderCheckFailed as error: self.delivery.check_failed(goals, current, error) raise + return replace(self, delivery=delivered) def apply(self, current): current.require_revision(self.expected_revision) current.require_commander(self.actor) current.state.require_open(current) current.order.require_delivery(self.order_id) + for strike in self.delivery.strikes(current, self.order_id, self.actor): + current = strike.apply(current) return replace(current, order=current.order.delivered(self.delivery), - strikes=(*current.strikes, - *self.delivery.strikes(current, self.order_id, self.actor)), revision=current.revision + 1) + def after_record(self, goals: Goals, changed: Todo) -> None: + for strike in changed.strikes: + if strike.expected_revision >= self.expected_revision: + strike.after_record(goals, changed) + @dataclass(frozen=True) class CloseOrderTodo(TodoOperation, declared_name=None): diff --git a/tests/test_commander_problem_orders.py b/tests/test_commander_problem_orders.py index 2bc690a82d..cf69c57e26 100644 --- a/tests/test_commander_problem_orders.py +++ b/tests/test_commander_problem_orders.py @@ -351,7 +351,9 @@ def strikes(delivery, at): commit = CommitDelivery(commit='a' * 40, delivered_commit='b' * 40) (late,) = strikes(commit, 2035) - assert (late.previous, late.actor, late.expected_revision) == (todo.held(), lead, todo.revision) + from agent_comms.thread_identity import StallWatcherIncarnation + assert (late.previous, late.actor, late.expected_revision) == ( + todo.held(), StallWatcherIncarnation.watcher(), todo.revision) assert late.reason.startswith('P1 order order-P1 late by 35 s') and 'b' * 40 in late.reason assert strikes(commit, 2000) == () assert strikes(OperationDelivery(), 9000) == () diff --git a/tests/test_objective_strikes.py b/tests/test_objective_strikes.py index de552567cb..a94a2fd3e3 100644 --- a/tests/test_objective_strikes.py +++ b/tests/test_objective_strikes.py @@ -60,7 +60,7 @@ def lines(): ('push', '-q', 'origin', 'main')): subprocess.run(['git', '-C', str(repo), *args], check=True) sha = subprocess.check_output(['git', '-C', str(repo), 'rev-parse', 'HEAD'], text=True).strip() - todo = create(commander, 'P2', deadline=now + 3600) + todo = create(commander, 'P2', deadline=now - 60) with store.writing() as db: replace(todo, order=replace(todo.order, at=RecordedEventTime(now - 120), first_call_deadline=RecordedEventTime(now + 60), @@ -76,3 +76,24 @@ def lines(): found = {type(strike) for pid in ('P1', 'P2') for strike in store.get(pid).strikes} assert found == set(ObjectiveStrike.members_with(ObjectiveStrike)) assert store.get('P2').pending # A failed check never becomes delivered work. + failed = store.get('P2') + with store.writing() as db: + replace(failed, order=replace(failed.order, + check=OrderCheck(str(repo), 'cat count.txt', CountCheckResult(0)))).upsert(db) + done.apply(comms, caller) + delivered = store.get('P2') + assert not delivered.pending and delivered.holder is None + assert len(delivered.strikes) == 2 and len(delivered.released) == 1 and len(lines()) == 4 + + # A subsequent late done must not create a second copy of the watcher's deadline failure. + third = create(commander, 'P3', deadline=now - 60) + with store.writing() as db: + replace(third, order=replace(third.order, at=RecordedEventTime(now - 180), + first_call_deadline=RecordedEventTime(now + 60), + check=OrderCheck(str(repo), 'cat count.txt', CountCheckResult(0)))).upsert(db) + store.advance_objective_failures(comms.goals) + assert len(store.get('P3').strikes) == 1 + CommsOrderDoneTool(problem='P3', order_id=third.order.id, + delivery=CommitDelivery(commit=sha, delivered_commit=sha), claims=working(comms)).apply(comms, caller) + assert not store.get('P3').pending and len(store.get('P3').strikes) == 1 + assert len(lines()) == 5 From 8d0e36cac0d06b4358ff29936fed1864a992f080 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 21:24:11 -0400 Subject: [PATCH 4/8] P286: name completed-check errors explicitly --- src/agent_comms/child_process.py | 6 +++--- src/agent_comms/todos.py | 13 ++++++++----- tests/test_objective_strikes.py | 4 ++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/agent_comms/child_process.py b/src/agent_comms/child_process.py index 0f1889024f..690f5ef615 100644 --- a/src/agent_comms/child_process.py +++ b/src/agent_comms/child_process.py @@ -1065,7 +1065,7 @@ def close(self) -> None: os.close(fd) -class OrderCheckFailed(ValueError): +class OrderCheckFailedError(ValueError): """A completed order check did not prove its expected result.""" @@ -1106,7 +1106,7 @@ def plus(self, amount: int) -> CheckPrinted: def require(self, expected: object) -> None: if self.count: - raise OrderCheckFailed(f"The order check is {self.count} away from {expected}.") + raise OrderCheckFailedError(f"The order check is {self.count} away from {expected}.") def project[T](self, counted: Callable[[int], T], uncounted: Callable[[NoIntegerPrinted], T]) -> T: @@ -1126,7 +1126,7 @@ def plus(self, amount: int) -> CheckPrinted: return self def require(self, expected: object) -> None: - raise OrderCheckFailed(f"The order check cannot prove {expected}: it printed no integer.") + raise OrderCheckFailedError(f"The order check cannot prove {expected}: it printed no integer.") def project[T](self, counted: Callable[[int], T], uncounted: Callable[[NoIntegerPrinted], T]) -> T: diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index db90040ea4..43b3f9fcc6 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -27,7 +27,9 @@ from . import skills from .change_report import FileLine -from .child_process import BoundedRun, CheckPrinted, ChildResult, ExitedOutcome, NoIntegerPrinted, OrderCheckFailed +from .child_process import ( + BoundedRun, CheckPrinted, ChildResult, ExitedOutcome, NoIntegerPrinted, OrderCheckFailedError, +) from .bus_publication import AgentOrigin from .external_transcript import ExternalTranscript from .messages import InfoKind, Message @@ -113,7 +115,7 @@ async def checked(self, check: OrderCheck) -> ChildResult: ... @abstractmethod async def delivery_time(self, check): ... - def check_failed(self, goals: Goals, todo: Todo, error: OrderCheckFailed) -> None: + def check_failed(self, goals: Goals, todo: Todo, error: OrderCheckFailedError) -> None: """Only a commit delivery can fail a check on delivered source.""" def strikes( @@ -143,7 +145,7 @@ def delivered(self) -> str: def work(self) -> str: return f"delivered commit {self.delivered}" - def check_failed(self, goals: Goals, todo: Todo, error: OrderCheckFailed) -> None: + def check_failed(self, goals: Goals, todo: Todo, error: OrderCheckFailedError) -> None: if self.commit != self.delivered: return # A failed integration check is not evidence that the unit's commit failed. now = time.time() @@ -2092,7 +2094,8 @@ def observe(cls, goals: Goals, todo: Todo, now: float) -> tuple[Self, ...]: if transcript.tool_call_between(order.at.recorded_at("An order"), due): return () return (cls.make(todo, now, - f"made no first tool call before its first-call deadline; cost {(now - due) / 60:.1f} minutes late"),) + f"made no first tool call before its first-call deadline; " + f"cost {(now - due) / 60:.1f} minutes late"),) @dataclass(frozen=True, kw_only=True) @@ -2382,7 +2385,7 @@ def prepare(self, goals, current): verified = self.delivery.verified(current, goals.registry) try: delivered = asyncio.run(verified) - except OrderCheckFailed as error: + except OrderCheckFailedError as error: self.delivery.check_failed(goals, current, error) raise return replace(self, delivery=delivered) diff --git a/tests/test_objective_strikes.py b/tests/test_objective_strikes.py index a94a2fd3e3..53d44bfc31 100644 --- a/tests/test_objective_strikes.py +++ b/tests/test_objective_strikes.py @@ -7,7 +7,7 @@ import pytest from agent_comms.channel_targets import CommandTarget -from agent_comms.child_process import OrderCheckFailed +from agent_comms.child_process import OrderCheckFailedError from agent_comms.commander.standing import UnownedWheelSpin from agent_comms.goals import CountCheckResult, OrderCheck from agent_comms.recorded_time import RecordedEventTime @@ -68,7 +68,7 @@ def lines(): done = CommsOrderDoneTool(problem='P2', order_id=todo.order.id, delivery=CommitDelivery(commit=sha, delivered_commit=sha), claims=working(comms)) for _ in range(2): - with pytest.raises(OrderCheckFailed): + with pytest.raises(OrderCheckFailedError): done.apply(comms, caller) assert len(store.get('P2').strikes) == 1 assert len(lines()) == 3 From 7a93f657fc3de7bc1d37173b842cf21182564d27 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 22:15:00 -0400 Subject: [PATCH 5/8] P286: let observed progress own automatic deadline strikes --- src/agent_comms/todos.py | 62 +++++++++++---------- tests/test_commander_problem_orders.py | 44 ++++++--------- tests/test_deadline_progress_strikes.py | 71 +++++++++++++++++++++++++ tests/test_objective_strikes.py | 6 +-- 4 files changed, 123 insertions(+), 60 deletions(-) create mode 100644 tests/test_deadline_progress_strikes.py diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 43b3f9fcc6..0484bac765 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -118,13 +118,6 @@ async def delivery_time(self, check): ... def check_failed(self, goals: Goals, todo: Todo, error: OrderCheckFailedError) -> None: """Only a commit delivery can fail a check on delivered source.""" - def strikes( - self, todo: Todo, order_id: str, actor: ThreadIncarnation - ) -> tuple[StrikeTodo, ...]: - """An operation has no commit time: recording it later is the commander's delay, so its - lateness is judged at the OVERDUE alarm and its delivery strikes nobody.""" - return () - @dataclass(frozen=True, kw_only=True) class RecordedCommitDelivery(OrderDelivery, declared_name="commit"): @@ -164,12 +157,6 @@ async def delivery_time(self, check: OrderCheck) -> EventTime: return EventTime.capture(float( await self.run(check, "git", "show", "-s", "--format=%ct", self.delivered))) - def strikes( - self, todo: Todo, order_id: str, actor: ThreadIncarnation - ) -> tuple[StrikeTodo, ...]: - """A commit delivered after the order's deadline strikes its holder once.""" - return MissedDeadlineStrike.late(todo, self.at, f"as delivered commit {self.delivered}") - @dataclass(frozen=True, kw_only=True) class CommitDelivery(RecordedCommitDelivery, declared_name="delivered_commit"): @@ -681,6 +668,10 @@ def distance_readings(self) -> DistanceReadings: """The order's distance readings; progress that read no commit has none.""" return DistanceReadings() + def deadline_strikes(self, todo: Todo, now: float) -> tuple[ObjectiveStrike, ...]: + """Observed work or unread progress cannot prove an absence of delivery.""" + return () + def stalled(self, ordered: float, now: float) -> bool: return False @@ -753,6 +744,10 @@ def distance_readings(self) -> DistanceReadings: @dataclass(frozen=True) class NoCommitProgress(Stalling, OrderProgress): + def deadline_strikes(self, todo: Todo, now: float) -> tuple[ObjectiveStrike, ...]: + return MissedDeadlineStrike.late(todo, RecordedEventTime(now), + "without an observed commit or linked pull request") + def missing_merges(self, source: ProgressSource) -> tuple[MergedProgress, ...]: return tuple(MergedProgress.found(source)) @@ -1332,6 +1327,11 @@ class ProblemOrder(DeclaredFamily, affix="ProblemOrder"): finding: ProblemFinding = field(default_factory=UnlocatedProblemFinding, metadata={"wire_omit_default": True}) + def observe_objective(self, failure: type[ObjectiveStrike], goals: Goals, todo: Todo, + now: float, pulls: PullRequests) -> tuple[ObjectiveStrike, ...]: + """Completed or retired orders do not acquire new pending-work failures.""" + return () + def reissued(self, at: float) -> PendingProblemOrder: """This order sent again at ``at`` to a new owner, its first-call window kept.""" raise ValueError("Only an undelivered order can change owner.") @@ -1584,6 +1584,10 @@ class PendingProblemOrder(CheckedProblemOrder): first_call_deadline: EventTime = field( default_factory=lambda: RecordedEventTime(time.time() + 120)) + def observe_objective(self, failure: type[ObjectiveStrike], goals: Goals, todo: Todo, + now: float, pulls: PullRequests) -> tuple[ObjectiveStrike, ...]: + return failure.observe(goals, todo, now, pulls) + def reissued(self, at: float) -> PendingProblemOrder: window = (self.first_call_deadline.recorded_at("A first-call deadline") - self.at.recorded_at("An order")) @@ -2031,7 +2035,8 @@ def make(cls, todo: Todo, now: float, reason: str) -> Self: RecordedEventTime(now), order_id=todo.order.checked().id) @classmethod - def observe(cls, goals: Goals, todo: Todo, now: float) -> tuple[Self, ...]: + def observe(cls, goals: Goals, todo: Todo, now: float, + pulls: PullRequests) -> tuple[ObjectiveStrike, ...]: return () def prior_strikes(self, current: Todo) -> tuple[StrikeTodo, ...]: @@ -2072,20 +2077,24 @@ def late(cls, todo: Todo, at: EventTime, detail: str) -> tuple[Self, ...]: if not lateness.late(): return () strike = cls.make(todo, at.recorded_at("A deadline failure"), - f"{todo.id} order {todo.order.checked().id} {lateness} {detail}; " + f"{todo.id} order {todo.order.checked().id} late by {lateness.seconds:g} s {detail}; " f"missed its order deadline; cost {lateness.seconds / 60:.1f} minutes late") return () if any(strike.repeats(prior) for prior in todo.strikes) else (strike,) @classmethod - def observe(cls, goals: Goals, todo: Todo, now: float) -> tuple[Self, ...]: - return (cls.late(todo, RecordedEventTime(now), "without a delivered commit") - if todo.pending else ()) + def observe(cls, goals: Goals, todo: Todo, now: float, + pulls: PullRequests) -> tuple[ObjectiveStrike, ...]: + if not todo.deadline.before(now): + return () + source = ProgressSource.of(todo, goals.registry.storage_rule, pulls) + return OrderProgress.from_source(source).deadline_strikes(todo, now) @dataclass(frozen=True, kw_only=True) class MissedFirstCallStrike(ObjectiveStrike): @classmethod - def observe(cls, goals: Goals, todo: Todo, now: float) -> tuple[Self, ...]: + def observe(cls, goals: Goals, todo: Todo, now: float, + pulls: PullRequests) -> tuple[Self, ...]: order = todo.order.checked() due = order.first_call_deadline.recorded_at("A first-call deadline") if now <= due: @@ -2395,16 +2404,9 @@ def apply(self, current): current.require_commander(self.actor) current.state.require_open(current) current.order.require_delivery(self.order_id) - for strike in self.delivery.strikes(current, self.order_id, self.actor): - current = strike.apply(current) return replace(current, order=current.order.delivered(self.delivery), revision=current.revision + 1) - def after_record(self, goals: Goals, changed: Todo) -> None: - for strike in changed.strikes: - if strike.expected_revision >= self.expected_revision: - strike.after_record(goals, changed) - @dataclass(frozen=True) class CloseOrderTodo(TodoOperation, declared_name=None): @@ -2774,12 +2776,14 @@ def held_orders(self) -> tuple[Todo, ...]: def advance_objective_failures(self, goals: Goals) -> None: now = time.time() - for observed in self.held_orders(): + held = self.held_orders() + pulls = PullRequests(held) + for observed in held: for failure in ObjectiveStrike.members_with(ObjectiveStrike): current = self.get(observed.id) - if not current.holder or not current.pending: + if not current.holder: break - for strike in failure.observe(goals, current, now): + for strike in current.order.observe_objective(failure, goals, current, now, pulls): strike.record(goals, current.id) def stalled(self, storage: StorageRule) -> tuple[Todo, ...]: diff --git a/tests/test_commander_problem_orders.py b/tests/test_commander_problem_orders.py index cf69c57e26..6286f9bb4f 100644 --- a/tests/test_commander_problem_orders.py +++ b/tests/test_commander_problem_orders.py @@ -311,11 +311,11 @@ def git(*args, at=None): script.done('P2', side, seen=working(comms)[0].screenshot) script.done('P2', late, seen=working(comms)[0].screenshot) struck = comms.goals.todos.get('P2') - assert commander_order.struck(struck, 'worker') == 1 and late in struck.strikes[-1].reason + assert commander_order.struck(struck, 'worker') == 0 -def test_a_late_delivery_reads_its_lateness_to_the_second(commander, commander_script): - """P136 was due 21:20:00 and delivered 21:20:35; its strike read "due 21:20 delivered 21:20".""" +def test_delivery_retains_commit_seconds_without_inventing_deadline_strikes(commander, commander_script): + """Recording delivery retains its exact commit time; only progress can prove no work.""" comms, _ = commander script, _ = commander_script @@ -325,7 +325,6 @@ def git(*args, at=None): *args), env=env, stderr=subprocess.DEVNULL).decode().strip() git('init', '-q', '-b', 'main') git('commit', '-q', '--allow-empty', '-m', 'base', at=500) - clock = lambda at: time.strftime('%H:%M:%S', time.localtime(at)) # noqa: E731 for pid, at in (('P1', 2000), ('P2', 2035)): create(commander, pid, deadline=2000) git('commit', '-q', '--allow-empty', '-m', f'{pid} delivered', at=at) @@ -333,30 +332,21 @@ def git(*args, at=None): git('update-ref', 'refs/remotes/origin/main', delivered) script.done(pid, delivered, seen=working(comms)[0].screenshot) assert commander_order.struck(comms.goals.todos.get('P1'), 'worker') == 0 - (strike,) = comms.goals.todos.get('P2').strikes - assert strike.reason.startswith(f'P2 order order-P2 late by 35 s: due {clock(2000)}, ' - f'delivered {clock(2035)} as {delivered}') + delivered_order = comms.goals.todos.get('P2').order + assert delivered_order.delivery.at == RecordedEventTime(2035) + assert comms.goals.todos.get('P2').strikes == () -def test_each_delivery_declares_the_strikes_it_carries(commander): - """A commit delivered after the deadline strikes its holder once; one on time strikes nobody, - and an operation strikes nobody, whenever it is recorded.""" - comms, _ = commander - create(commander, 'P1', deadline=2000) - todo = comms.goals.todos.get('P1') - lead = comms.registry.require('lead').incarnation - - def strikes(delivery, at): - return replace(delivery, at=RecordedEventTime(at)).strikes(todo, todo.order.id, lead) - - commit = CommitDelivery(commit='a' * 40, delivered_commit='b' * 40) - (late,) = strikes(commit, 2035) +def test_progress_owns_deadline_strikes_not_delivery_records(commander): + from agent_comms.todos import MissedDeadlineStrike, PullRequests from agent_comms.thread_identity import StallWatcherIncarnation + comms, _ = commander + todo = create(commander, 'P1', deadline=2000) + (late,) = MissedDeadlineStrike.observe(comms.goals, todo, 2035, PullRequests((todo,))) assert (late.previous, late.actor, late.expected_revision) == ( todo.held(), StallWatcherIncarnation.watcher(), todo.revision) - assert late.reason.startswith('P1 order order-P1 late by 35 s') and 'b' * 40 in late.reason - assert strikes(commit, 2000) == () - assert strikes(OperationDelivery(), 9000) == () + assert 'late by 35 s' in late.reason + assert MissedDeadlineStrike.observe(comms.goals, todo, 2000, PullRequests((todo,))) == () @pytest.mark.parametrize('retained_problem', [None, 'P197', 'P1970']) @@ -390,8 +380,8 @@ def test_done_distinguishes_non_git_home_from_exact_order_checkout( assert not comms.goals.todos.get('P197').pending -def test_a_late_commit_done_through_the_tool_is_struck_in_its_delivery(commander): - """Core strikes a late delivered commit in the revision that records it; no script is needed.""" +def test_a_verified_commit_delivery_does_not_invent_a_deadline_strike(commander): + """Delivered source is work; its recording path never creates an absence-of-work strike.""" comms, caller = commander def git(*args, at=1000): @@ -415,9 +405,7 @@ def done(problem, commit): assert done('P1', on_time).strikes == () delivered = done('P2', late) - (strike,) = delivered.strikes - assert not delivered.pending and strike.previous.owner == 'worker' - assert strike.reason.startswith('P2 order order-P2 late by 35 s') and late in strike.reason + assert not delivered.pending and delivered.strikes == () def test_a_main_merge_is_never_the_delivered_commit(commander): diff --git a/tests/test_deadline_progress_strikes.py b/tests/test_deadline_progress_strikes.py new file mode 100644 index 0000000000..84686a2d72 --- /dev/null +++ b/tests/test_deadline_progress_strikes.py @@ -0,0 +1,71 @@ +"""Deadline strikes consume real Git/PR progress through the retained order store.""" +import time +from contextlib import nullcontext +from dataclasses import replace + +from agent_comms.channel_targets import CommandTarget +from agent_comms.commander.standing import UnownedWheelSpin +from agent_comms.recorded_time import RecordedEventTime +from agent_comms.storage_rules import StorageRule +from agent_comms.todos import ( + MissedDeadlineStrike, NoCommitProgress, OpenPrProgress, OrderProgress, + ProgressSource, PullRequests, PushedProgress, UnmergedPullRequest, +) +from test_order_progress import checkout, commit, git + + +def overdue(commander, checkout, monkeypatch): + comms, _ = commander + todo, unit = checkout + now = time.time() + monkeypatch.setattr(comms.registry, 'storage_rule', StorageRule(checkout_base=unit.parent)) + with comms.goals.todos.writing() as db: + replace(todo, deadline=RecordedEventTime(now - 60), order=replace(todo.order, + at=RecordedEventTime(now - 180), + first_call_deadline=RecordedEventTime(now + 60))).upsert(db) + return comms.goals.todos.get(todo.id) + + +def watch(comms): + UnownedWheelSpin.watch_pass(lambda: nullcontext(comms)) + + +def lines(comms): + return [message.body for message in comms.views.history_page( + CommandTarget.named('#comms'), viewer='lead').messages + if message.body.startswith('Core strike')] + + +def test_pushed_past_due_order_gets_no_deadline_strike(commander, checkout, monkeypatch): + comms, _ = commander + todo = overdue(commander, checkout, monkeypatch) + unit = checkout[1] + commit(unit) + git(unit, 'push', '-q', 'origin', 'HEAD:fix/p1') + sha = git(unit, 'rev-parse', 'HEAD') + source = ProgressSource.of(todo, comms.registry.storage_rule, PullRequests((todo,))) + assert isinstance(OrderProgress.from_source(source), PushedProgress) + watch(comms) + assert comms.goals.todos.get(todo.id).strikes == () and lines(comms) == [] + # Control only the external GitHub response, never the derived progress or TodoStore. + pr = UnmergedPullRequest(7, 'fix/p1', sha, updated_at=time.time(), orders=(todo.order.id,)) + monkeypatch.setattr(PullRequests, 'read', + lambda self, repo, state, order: (pr,) if state == 'open' else ()) + source = ProgressSource.of(todo, comms.registry.storage_rule, PullRequests((todo,))) + assert isinstance(OrderProgress.from_source(source), OpenPrProgress) + watch(comms) + assert comms.goals.todos.get(todo.id).strikes == () and lines(comms) == [] + print('Past-due actual pushed branch and open PR at its SHA: no automatic strike.') + + +def test_stalled_past_due_order_gets_deadline_strike(commander, checkout, monkeypatch): + comms, _ = commander + todo = overdue(commander, checkout, monkeypatch) + source = ProgressSource.of(todo, comms.registry.storage_rule, PullRequests((todo,))) + assert isinstance(OrderProgress.from_source(source), NoCommitProgress) + watch(comms) + watch(comms) + (strike,) = comms.goals.todos.get(todo.id).strikes + assert isinstance(strike, MissedDeadlineStrike) + assert len(lines(comms)) == 1 and 'minutes late' in lines(comms)[0] + print(lines(comms)[0]) diff --git a/tests/test_objective_strikes.py b/tests/test_objective_strikes.py index 53d44bfc31..b24e5e365d 100644 --- a/tests/test_objective_strikes.py +++ b/tests/test_objective_strikes.py @@ -82,8 +82,8 @@ def lines(): check=OrderCheck(str(repo), 'cat count.txt', CountCheckResult(0)))).upsert(db) done.apply(comms, caller) delivered = store.get('P2') - assert not delivered.pending and delivered.holder is None - assert len(delivered.strikes) == 2 and len(delivered.released) == 1 and len(lines()) == 4 + assert not delivered.pending and delivered.holder == 'worker' + assert len(delivered.strikes) == 1 and delivered.released == () and len(lines()) == 3 # A subsequent late done must not create a second copy of the watcher's deadline failure. third = create(commander, 'P3', deadline=now - 60) @@ -96,4 +96,4 @@ def lines(): CommsOrderDoneTool(problem='P3', order_id=third.order.id, delivery=CommitDelivery(commit=sha, delivered_commit=sha), claims=working(comms)).apply(comms, caller) assert not store.get('P3').pending and len(store.get('P3').strikes) == 1 - assert len(lines()) == 5 + assert len(lines()) == 4 From 8f5816b894b8a588139b514882d03570843eedff Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 22:32:44 -0400 Subject: [PATCH 6/8] P286: retain checked-order ownership through delivery verification --- src/agent_comms/child_process.py | 3 ++- src/agent_comms/todos.py | 5 +++-- tests/test_commander_problem_orders.py | 8 ++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/agent_comms/child_process.py b/src/agent_comms/child_process.py index 690f5ef615..c9aa82f776 100644 --- a/src/agent_comms/child_process.py +++ b/src/agent_comms/child_process.py @@ -1126,7 +1126,8 @@ def plus(self, amount: int) -> CheckPrinted: return self def require(self, expected: object) -> None: - raise OrderCheckFailedError(f"The order check cannot prove {expected}: it printed no integer.") + raise OrderCheckFailedError( + f"The order check cannot prove {expected}: it printed no integer.") def project[T](self, counted: Callable[[int], T], uncounted: Callable[[NoIntegerPrinted], T]) -> T: diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 0484bac765..cfcedf726f 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -100,8 +100,9 @@ async def run(self, check, *command): async def verified(self, todo: Todo, registry: Registration) -> OrderDelivery: """The order's check and every count its deletion declares run on the delivered source.""" - check = todo.order.check - for each in (check, *todo.order.deletes.checks): + order = todo.order.checked() + check = order.check + for each in (check, *order.deletes.checks): await self.require_source(each) each.result.require(await self.checked(each)) return replace(self, at=await self.delivery_time(check)) diff --git a/tests/test_commander_problem_orders.py b/tests/test_commander_problem_orders.py index 6286f9bb4f..4ac0cb0dd8 100644 --- a/tests/test_commander_problem_orders.py +++ b/tests/test_commander_problem_orders.py @@ -717,7 +717,6 @@ def test_order_check_replaces_a_wrong_check_so_done_runs_the_corrected_one(comma git('update-ref', 'refs/remotes/origin/main', 'HEAD') body = order_body(repo, 'grep -c two count.txt -> 1') problem(script, body) - script.strike('worker', 'P1', 'P1 was sent back once before its check was corrected') sent = comms.goals.todos.get('P1') corrected = f'{repo} :: cat count.txt -> 1' why = 'the recorded check greps a word the ordered edit never writes' @@ -734,10 +733,15 @@ def test_order_check_replaces_a_wrong_check_so_done_runs_the_corrected_one(comma fixed = git('rev-parse', 'HEAD') with pytest.raises(ValueError, match=r'The order check is 1 away from CountCheckResult\(expected=1\)\.'): script.done('P1', fixed, seen=working(comms)[0].screenshot) + failed = comms.goals.todos.get('P1') + from agent_comms.todos import FailedCheckStrike + assert len(failed.strikes) == 1 and isinstance(failed.strikes[0], FailedCheckStrike) + assert (failed.order.id, failed.assignment, failed.deadline) == ( + sent.order.id, sent.assignment, sent.deadline) script.correct_check('P1', corrected, why) current = comms.goals.todos.get('P1') assert (current.order.id, current.assignment, current.deadline, current.strikes) == ( - sent.order.id, sent.assignment, sent.deadline, sent.strikes) + failed.order.id, failed.assignment, failed.deadline, failed.strikes) [record] = current.order.check_corrections assert (record.previous, record.check, record.reason) == ( sent.order.check, OrderCheck(str(repo), 'cat count.txt', CountCheckResult(1)), why) From 2b3414b78941b443cfa74f30610d7c0396fe1266 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:31:13 -0400 Subject: [PATCH 7/8] P286: keep Core-released orders eligible for replacement --- src/agent_comms/commander/standing.py | 8 +++---- src/agent_comms/todos.py | 23 +++++++++++------- tests/test_stall_watcher_strike.py | 34 ++++++++++++++++++++++++++- 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/agent_comms/commander/standing.py b/src/agent_comms/commander/standing.py index 5b9d07a955..fac1640640 100644 --- a/src/agent_comms/commander/standing.py +++ b/src/agent_comms/commander/standing.py @@ -179,10 +179,10 @@ def dispatches(self, comms: Comms, commander: str) -> Iterable[WorkDispatch]: @classmethod def after_stall(cls, comms: Comms) -> None: - """Assign first: refused or unavailable replacements leave the old holder untouched.""" - stalled = comms.goals.todos.stalled(comms.registry.storage_rule) - for commander in {todo.creator_identity.name for todo in stalled}: - mine = (todo for todo in stalled if todo.creator_identity.name == commander) + """Assign eligible work; refused replacements preserve the observed row.""" + available = comms.goals.todos.reassignable(comms.registry.storage_rule) + for commander in {todo.creator_identity.name for todo in available}: + mine = (todo for todo in available if todo.creator_identity.name == commander) for rule in cls.paired(comms, commander, mine, StallWatcherIncarnation.watcher()): rule.enforce(comms, commander) diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index cfcedf726f..94030d64e4 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -2075,7 +2075,8 @@ class MissedDeadlineStrike(ObjectiveStrike): @classmethod def late(cls, todo: Todo, at: EventTime, detail: str) -> tuple[Self, ...]: lateness = at.lateness(todo.deadline) - if not lateness.late(): + if not lateness.late() or todo.deadline.before( + todo.order.checked().at.recorded_at("An order")): return () strike = cls.make(todo, at.recorded_at("A deadline failure"), f"{todo.id} order {todo.order.checked().id} late by {lateness.seconds:g} s {detail}; " @@ -2564,7 +2565,9 @@ def assigned(self, proposed: Assignment, traced: FileLine | None = None) -> Todo return replace(self, assignment=proposed, revision=self.revision + 1) def reassigned(self, assigned: Todo, actor: ThreadIncarnation) -> Todo: - """After assignment validation, fold its strike and release into the same row write.""" + """Strike a replaced holder, never invent another release for already-unowned work.""" + if self.assignment is None: + return assigned owner, at = self.held(), RecordedEventTime(time.time()) reason = StrikeReason(f"{owner.owner} stopped making progress on {self.id}; " f"Core reassigned the order to {assigned.held().owner}.") @@ -2787,13 +2790,17 @@ def advance_objective_failures(self, goals: Goals) -> None: for strike in current.order.observe_objective(failure, goals, current, now, pulls): strike.record(goals, current.id) - def stalled(self, storage: StorageRule) -> tuple[Todo, ...]: - """Observe active held orders without changing owners, strikes or releases.""" - held = tuple(todo for todo in self.held_orders() if todo.pending) + def reassignable(self, storage: StorageRule) -> tuple[Todo, ...]: + """Include Core-released work, never a commander's deliberately released backlog.""" + pending = tuple(todo for todo in self._open("1", (), OpenTodoState) if todo.pending) + released = tuple(todo for todo in pending if not todo.holder and todo.released + and todo.released[-1].actor == StallWatcherIncarnation.watcher()) + held = tuple(todo for todo in pending if todo.holder) pulls = PullRequests(held) - return tuple(todo for todo in held - if OrderProgress.from_source(source := ProgressSource.of(todo, storage, pulls)) - .stalled(source.since, time.time())) + return released + tuple(todo for todo in held + if OrderProgress.from_source( + source := ProgressSource.of(todo, storage, pulls)) + .stalled(source.since, time.time())) def reconcile_merges(self, commander: ThreadIncarnation, repos: Sequence[Repo], storage: StorageRule) -> None: diff --git a/tests/test_stall_watcher_strike.py b/tests/test_stall_watcher_strike.py index 19813c61c7..9b4d09ef8a 100644 --- a/tests/test_stall_watcher_strike.py +++ b/tests/test_stall_watcher_strike.py @@ -19,6 +19,7 @@ from agent_comms import coordinator from agent_comms.thread_identity import StallWatcherIncarnation from agent_comms.todos import (BlockedTodoState, ChangeTodoState, CorrectCheckTodo, + MissedDeadlineStrike, MissedFirstCallStrike, ReleaseTodo, StrikeReason, StrikeTodo, TodoConflict) from test_commander_problem_orders import create from test_standing_rules import working @@ -121,8 +122,35 @@ def test_commander_released_backlog_is_not_automatically_reassigned(commander): released = comms.goals.todos.apply('P1', ReleaseTodo( todo.revision, comms.registry.require('lead').incarnation, todo.held(), StrikeReason('Commander intentionally froze this unfinished order without authorizing automatic reassignment.'))) + comms.registry.declare(comms.registry.new_thread( + 'spare', frozenset({'comms'}), str(comms.root), created_at=3.0)) + working(comms, 'spare', 3.0) + standing.UnownedWheelSpin.after_stall(comms) + assert comms.goals.todos.get('P1') == released + + +def test_core_released_order_waits_for_a_spare_without_striking_the_replacement( + commander, commander_script): + comms, _ = commander + create(commander, 'P1') + comms.goals.todos.advance_objective_failures(comms.goals) + released = comms.goals.todos.get('P1') + assert released.holder is None + assert {type(strike) for strike in released.strikes} == { + MissedDeadlineStrike, MissedFirstCallStrike} standing.UnownedWheelSpin.after_stall(comms) assert comms.goals.todos.get('P1') == released + comms.registry.declare(comms.registry.new_thread( + 'spare', frozenset({'comms'}), str(comms.root), created_at=3.0)) + working(comms, 'spare', 3.0) + standing.UnownedWheelSpin.after_stall(comms) + assigned = comms.goals.todos.get('P1') + assert assigned.holder == 'spare' + assert assigned.strikes == released.strikes + assert assigned.released == released.released + comms.goals.todos.advance_objective_failures(comms.goals) + standing.UnownedWheelSpin.after_stall(comms) + assert comms.goals.todos.get('P1') == assigned def test_no_spare_leaves_the_old_holder_and_strikes_untouched(commander, commander_script): @@ -205,7 +233,11 @@ async def between(seconds): await asyncio.wait_for(done.wait(), timeout=30) assert 'Service worker pass failed' not in caplog.text after = Comms(copied).goals.todos.get('P1') - assert after.holder == 'spare' and len(after.strikes) == len(after.released) == 1 + assert after.holder == 'spare' + assert len(after.strikes) == 2 and len(after.released) == 1 + assert {type(strike) for strike in after.strikes} == { + MissedDeadlineStrike, MissedFirstCallStrike} + assert all(strike.previous == before.held() for strike in after.strikes) assert original.goals.todos.get('P1') == before (tmp_path / 'three-pass-result.json').write_text(json.dumps({ 'copy': str(copied), 'passes': 3, 'maintenance': receipts, From c24cca65982ae2b3c4d7be03cbb5408fe89a5328 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:34:36 -0400 Subject: [PATCH 8/8] P286: let watcher identity own held-order replacement effects --- src/agent_comms/thread_identity.py | 4 +++- src/agent_comms/todos.py | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/agent_comms/thread_identity.py b/src/agent_comms/thread_identity.py index 6b20289488..d4de479e2c 100644 --- a/src/agent_comms/thread_identity.py +++ b/src/agent_comms/thread_identity.py @@ -232,7 +232,9 @@ def require_may_change(self, todo: Todo) -> None: """The Core watcher may advance stalled orders without a commander turn.""" def assigned(self, previous: Todo, assigned: Todo) -> Todo: - return previous.reassigned(assigned, self) + if previous.holder: + return previous.reassigned(assigned, self) + return assigned @classmethod def watcher(cls) -> StallWatcherIncarnation: diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 94030d64e4..f45c56b492 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -2565,9 +2565,7 @@ def assigned(self, proposed: Assignment, traced: FileLine | None = None) -> Todo return replace(self, assignment=proposed, revision=self.revision + 1) def reassigned(self, assigned: Todo, actor: ThreadIncarnation) -> Todo: - """Strike a replaced holder, never invent another release for already-unowned work.""" - if self.assignment is None: - return assigned + """After assignment validation, fold its strike and release into the same row write.""" owner, at = self.held(), RecordedEventTime(time.time()) reason = StrikeReason(f"{owner.owner} stopped making progress on {self.id}; " f"Core reassigned the order to {assigned.held().owner}.")