From e73e2615c05b0e7aa0860836e5e3785bd988c218 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 18:52:40 -0400 Subject: [PATCH 01/66] P265: preserve source-prefix clocks and require recorded times through EventTime --- src/agent_comms/cli_bridges/codex_protocol.py | 2 +- src/agent_comms/cli_protocol.py | 4 +- src/agent_comms/recorded_time.py | 34 ++++++++------ src/agent_comms/runtime_updates.py | 7 +-- tests/test_claude_tool_preview.py | 4 +- tests/test_claude_turn.py | 21 +++++---- tests/test_codex_earlier_pages.py | 2 +- tests/test_codex_items.py | 2 +- tests/test_transcript_event_time.py | 46 +++++++++++++++++-- 9 files changed, 88 insertions(+), 34 deletions(-) diff --git a/src/agent_comms/cli_bridges/codex_protocol.py b/src/agent_comms/cli_bridges/codex_protocol.py index 9523eccefd..d11883533e 100644 --- a/src/agent_comms/cli_bridges/codex_protocol.py +++ b/src/agent_comms/cli_bridges/codex_protocol.py @@ -741,7 +741,7 @@ def publication(self) -> RuntimeUpdate: if self.completedAtMs is None: raise ValueError(f'Item {self.item.id} has not finished') event_time = self.started_at.continued_from(self.completedAtMs / 1000) - return self.item.completed(self.turnId, self.item.events(event_time.transcript_timestamp())) + return self.item.completed(self.turnId, self.item.events(event_time.recorded_at('A Codex item'))) class ItemsPage(WireModel): diff --git a/src/agent_comms/cli_protocol.py b/src/agent_comms/cli_protocol.py index 93aa3650e3..86881d80fc 100644 --- a/src/agent_comms/cli_protocol.py +++ b/src/agent_comms/cli_protocol.py @@ -15,7 +15,7 @@ from .maintenance_barrier import MaintenanceHeldError from .messages import Message from .field_codec import TimestampText -from .recorded_time import MissingTranscriptTimestampError +from .recorded_time import UnrecordedTimeError from .external_transcript import RequestedEarlierPage if TYPE_CHECKING: @@ -43,7 +43,7 @@ class TimestampedWire(WireModel): @classmethod def require_source_time(cls, value): if isinstance(value, dict) and value.get('timestamp') is None: - raise MissingTranscriptTimestampError(f'{cls.__name__} has no recorded timestamp') + raise UnrecordedTimeError(f'{cls.__name__} has no recorded timestamp') return value @field_validator('timestamp', mode='before') diff --git a/src/agent_comms/recorded_time.py b/src/agent_comms/recorded_time.py index 35bb91357d..97d52c5e26 100644 --- a/src/agent_comms/recorded_time.py +++ b/src/agent_comms/recorded_time.py @@ -4,15 +4,21 @@ import time from abc import abstractmethod -from dataclasses import dataclass +from dataclasses import dataclass, replace from math import isfinite - +from typing import TYPE_CHECKING, TypeVar from .declared_family import DeclaredFamily from .field_codec import FieldCodec, FieldRepresentation -class MissingTranscriptTimestampError(ValueError): - """An external source cannot publish an event without its recorded time.""" +if TYPE_CHECKING: + from .transcript_events import TranscriptEvent + +Event = TypeVar("Event", bound="TranscriptEvent") + + +class UnrecordedTimeError(ValueError): + """An operation requires a source-recorded time, but none was supplied.""" class GeneratedTimestamp(FieldRepresentation): @@ -67,11 +73,11 @@ def clock(self) -> str: return "-" def require_recorded(self, label: str) -> None: - raise ValueError(f"{label} requires a recorded time.") + self.recorded_at(label) def recorded_at(self, label: str) -> float: - """The recorded instant; an unrecorded time refuses like ``require_recorded``.""" - raise ValueError(f"{label} requires a recorded time.") + """The recorded instant; callers requiring a time cannot invent one.""" + raise UnrecordedTimeError(f"{label} requires a recorded time.") def lateness(self, deadline: EventTime) -> Lateness: """How late this delivery time is against ``deadline``; either unrecorded refuses.""" @@ -83,8 +89,8 @@ def require_deadline(self) -> None: def require_delivery(self) -> None: self.require_recorded("A delivery") - def transcript_timestamp(self) -> float: - raise MissingTranscriptTimestampError("A transcript event requires its source's recorded time") + @abstractmethod + def stamp(self, event: Event) -> Event: ... @abstractmethod def continued_from(self, original_at: float): ... @@ -92,6 +98,9 @@ def continued_from(self, original_at: float): ... @dataclass(frozen=True) class UnrecordedEventTime(EventTime): + def stamp(self, event: Event) -> Event: + return event + def continued_from(self, original_at): return RecordedEventTime(original_at) @@ -104,9 +113,6 @@ def __post_init__(self): if type(self.at) not in (int, float) or not isfinite(self.at): raise ValueError("A recorded event time must be a finite Unix timestamp.") - def require_recorded(self, label: str) -> None: - pass - def since(self, timestamp: float) -> bool: return self.at >= timestamp @@ -119,8 +125,8 @@ def clock(self) -> str: def recorded_at(self, label: str) -> float: return self.at - def transcript_timestamp(self) -> float: - return self.at + def stamp(self, event: Event) -> Event: + return replace(event, timestamp=self.at) def continued_from(self, original_at): return self diff --git a/src/agent_comms/runtime_updates.py b/src/agent_comms/runtime_updates.py index b1b9f6bba6..07390707b4 100644 --- a/src/agent_comms/runtime_updates.py +++ b/src/agent_comms/runtime_updates.py @@ -49,8 +49,9 @@ def append(self, text: str, section: int) -> None: raise ValueError("Received a text delta for a non-text item") def finish(self, update: CompletedItem) -> tuple[ClosedDraft, FinishedItem]: - """Close with the completed record's own already decoded event times.""" - return ClosedDraft(self.started_at), FinishedItem(update.turn_id, update.item_id, update.events) + """Keep the observed source prefix's clock; completions are already dated.""" + events = tuple(self.started_at.stamp(event) for event in update.events) + return ClosedDraft(self.started_at), FinishedItem(update.turn_id, update.item_id, events) class NoPrefixDraft(DraftAccumulator): @@ -95,7 +96,7 @@ def events(self) -> tuple[TranscriptEvent, ...]: ) return ( (self.event_type(text, position=self.position, - timestamp=self.started_at.transcript_timestamp()),) if text else () + timestamp=self.started_at.recorded_at("A transcript event")),) if text else () ) def snapshot(self, turn_id: str, item_id: str) -> tuple[DraftItem, ...]: diff --git a/tests/test_claude_tool_preview.py b/tests/test_claude_tool_preview.py index 118983aab1..0bf1ffc0be 100644 --- a/tests/test_claude_tool_preview.py +++ b/tests/test_claude_tool_preview.py @@ -24,13 +24,15 @@ def test_claude_tool_names_arguments_and_diff_reach_shared_events(name, argument record=TimedRecord(position=0,uuid='record',timestamp='2026-10-17T00:00:00+00:00') tool=ToolUse(id='call',name=name,input=arguments) active,=tool.updates(record,session) - call=session.tools['call'] + call,source=session.tools['call'] + assert source is record assert call.name==native and call.arguments==expected and call.title==title assert active.detail == title result=ToolResult(tool_use_id='call',content=[{'type':'text','text':'done'}]) completed,_=result.updates(record,session) started,ended=completed.events assert started.raw_input==expected and started.title==title + assert started.timestamp == ended.timestamp == record.timestamp if patch is None: assert ended.diff is None else: diff --git a/tests/test_claude_turn.py b/tests/test_claude_turn.py index d09ad40c36..6cc2c60ab7 100644 --- a/tests/test_claude_turn.py +++ b/tests/test_claude_turn.py @@ -21,10 +21,10 @@ def test_unknown_turn_uses_the_reader_phase_for_every_item(question, phase, expectation): session = ClaudeSession(phase=phase()) record = TimedRecord(uuid='record', timestamp='2026-10-18T00:00:00Z', position=17) - session.started(ToolCall('call', 'read', {'path': 'a.py'})) + session.started(ToolCall('call', 'read', {'path': 'a.py'}), record) actions = { - 'finished': lambda: session.finished('item', (AssistantTranscript('done', position=17),)), - 'result': lambda: session.tool_result('call', 'done', True, record.position), + 'finished': lambda: session.finished('item', (AssistantTranscript('done', position=17, timestamp=record.timestamp),)), + 'result': lambda: session.tool_result('call', 'done', True, record), 'thinking': lambda: RedactedThinking().updates(record, session), 'tool': lambda: ToolUse(id='next', name='Read', input={'file_path': 'a.py'}).updates(record, session), } @@ -34,10 +34,15 @@ def test_unknown_turn_uses_the_reader_phase_for_every_item(question, phase, expe def test_prompt_identifies_finished_and_active_items_with_the_same_turn(): session = ClaudeSession(phase=Live()) - UserRecord(uuid='prompt', timestamp='2026-10-18T00:00:00Z', position=17, - promptId='turn-id', message={'content': []}).updates(session) + record = UserRecord(uuid='prompt', timestamp='2026-10-18T00:00:00Z', position=17, + promptId='turn-id', message={'content': []}) + record.updates(session) assert session.turn == KnownTurn('turn-id') - session.started(ToolCall('call', 'read', {'path': 'a.py'})) - completed, active = session.tool_result('call', 'done', True, 18) - assert completed == FinishedItem('turn-id', 'call', ToolCall('call', 'read', {'path': 'a.py'}).finished('done', True, 18)) + call = ToolCall('call', 'read', {'path': 'a.py'}) + session.started(call, record) + result = TimedRecord(uuid='result', timestamp='2026-10-18T00:00:01Z', position=18) + completed, active = session.tool_result('call', 'done', True, result) + assert completed == FinishedItem('turn-id', 'call', ( + call.started(record.position, record.timestamp), + call.ended('done', True, result.position, result.timestamp))) assert active == ActiveItem('turn-id', 'call', ActivityState.WORKING, '') diff --git a/tests/test_codex_earlier_pages.py b/tests/test_codex_earlier_pages.py index 35e0f47538..4d8658267e 100644 --- a/tests/test_codex_earlier_pages.py +++ b/tests/test_codex_earlier_pages.py @@ -170,7 +170,7 @@ def test_stopped_carry_preserves_external_items(tmp_path): root = tmp_path / 'root' root.mkdir() path = ExternalTranscript.path_in(root) - saved = json.dumps(FieldCodec.encode((AssistantTranscript('preserved', position='i'),))) + saved = json.dumps(FieldCodec.encode((AssistantTranscript('preserved', position='i', timestamp=1.0),))) # This historical conversion predates the separate event table. with closing(CoordinationStore.writer(path)) as connection: connection.execute('CREATE TABLE external_item (thread TEXT NOT NULL, turn_id TEXT NOT NULL, ' diff --git a/tests/test_codex_items.py b/tests/test_codex_items.py index 14ab5793ed..f23fb237f5 100644 --- a/tests/test_codex_items.py +++ b/tests/test_codex_items.py @@ -118,7 +118,7 @@ def test_coalescing_preserves_completion_and_unrelated_updates(): first = DraftItem("t", "a", TextDraft.from_parts(AssistantTranscript, ("H",), UnrecordedEventTime(), "a")) newer = DraftItem("t", "a", TextDraft.from_parts(AssistantTranscript, ("Hello",), UnrecordedEventTime(), "a")) other = DraftItem("t", "b", TextDraft.from_parts(AssistantTranscript, ("Other",), UnrecordedEventTime(), "b")) - final = FinishedItem("t", "a", (AssistantTranscript("Hello!", position="a"),)) + final = FinishedItem("t", "a", (AssistantTranscript("Hello!", position="a", timestamp=1.0),)) end = TurnEnded() assert RuntimeUpdate.coalesced([first, other, newer]) == [other, newer] assert RuntimeUpdate.coalesced([first, other, newer, final, end]) == [other, final, end] diff --git a/tests/test_transcript_event_time.py b/tests/test_transcript_event_time.py index 49a042ffb1..fcf03b4db8 100644 --- a/tests/test_transcript_event_time.py +++ b/tests/test_transcript_event_time.py @@ -56,10 +56,10 @@ def test_original_record_clock_survives_split_and_routed_projection(tmp_path, st def test_new_native_entry_inherits_original_clock_projection(): class ExternalNoteEntry(NativeEntry): - def _events(self, context): - return [AssistantTranscript("a new external note", position=context.position)] + def _events(self, context, timestamp): + return [AssistantTranscript("a new external note", position=context.position, timestamp=timestamp)] - # Adding a content declaration does not add another clock propagation site. + # The inherited entry owner decodes the source clock before constructing any content. stamp = "2026-09-28T23:12:33.123Z" expected = datetime.fromisoformat(stamp).timestamp() note = ExternalNoteEntry(timestamp=RecordedTimestamp(stamp)) @@ -153,3 +153,43 @@ async def serve(reader, writer): print( f"actual native clocks={expected}; ACP snapshot + rebuild exact; native history/proof unchanged" ) + + +@pytest.mark.parametrize('source', ('claude', 'codex')) +@pytest.mark.parametrize('stamp', (None, 'missing')) +def test_external_source_without_a_time_refuses_before_event_construction(source, stamp): + from pydantic import ValidationError + from agent_comms.cli_bridges.claude_protocol import TimedRecord + from agent_comms.cli_bridges.codex_rollout import ROLLOUT_RECORD + from agent_comms.recorded_time import UnrecordedTimeError + + record = ({'uuid': 'record', 'position': 0} if source == 'claude' else + {'type': 'response_item', 'payload': {'type': 'message', 'role': 'assistant', 'content': []}}) + if stamp is None: + record['timestamp'] = None + decode = TimedRecord.model_validate if source == 'claude' else ROLLOUT_RECORD.validate_python + with pytest.raises(ValidationError) as refused: + decode(record) + assert any(isinstance(error.get('ctx', {}).get('error'), UnrecordedTimeError) + for error in refused.value.errors()) + + +def test_codex_rollout_projects_the_enclosing_record_clock(): + from agent_comms.cli_bridges.codex_rollout import ROLLOUT_RECORD, RolloutState + + record = ROLLOUT_RECORD.validate_python({ + 'type': 'response_item', 'timestamp': '2026-10-19T18:00:00Z', + 'payload': {'type': 'message', 'role': 'assistant', 'id': 'item', + 'content': [{'text': 'saved source text'}]}, + }) + update, = record.updates(RolloutState(), 17) + assert update.events[0].timestamp == datetime.fromisoformat('2026-10-19T18:00:00Z').timestamp() + + +def test_malformed_shell_words_remain_visible_to_the_install_rule(): + from agent_comms.commander.send_guard import InstallCommand, simple_commands + + command = 'uv pip install package "unterminated' + words, = simple_commands(command) + assert words == tuple(command.split()) + assert InstallCommand.runs(words) From e95aeb9aa32d994c6e695a3c42483d34914d9110 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 18:24:49 -0400 Subject: [PATCH 02/66] Date the external events saved before event time became required #2011 made every transcript event carry its time but left 45,458 saved Claude tool events undated, so every rotate crashed decoding them. The carry takes each event's time from its Claude Code transcript (all 45,458 found, 6 s) and refuses if any stays undated; the unjudged query loses its undated branch. Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/external_transcript.py | 15 +---- tools/cutover/carry_external_event_times.py | 73 +++++++++++++++++++++ 2 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 tools/cutover/carry_external_event_times.py diff --git a/src/agent_comms/external_transcript.py b/src/agent_comms/external_transcript.py index d2eb3a25b0..d254b81736 100644 --- a/src/agent_comms/external_transcript.py +++ b/src/agent_comms/external_transcript.py @@ -212,19 +212,8 @@ def unjudged_query(cls, thread, todo): judged = tuple(verdict.tool_call_id for verdict in todo.verdicts) placeholders = lambda values: ",".join("?" for _ in values) since = todo.order.at.recorded_at("An order") - # SQL NULL is the original undated fact, decoded through FieldCodec - # as UnrecordedEventTime when a row is read. No time is substituted. - table = cls.declared_name - selected = f'u.thread=? AND u.kind=? AND u.tool IN ({placeholders(tools)})' - # Two ranges keep timestamp in the index search, rather than scanning - # every historical edit to evaluate an OR for each row. - where = ( - f'e.rowid IN (SELECT u.rowid FROM "{table}" u WHERE {selected} AND u.timestamp>=? ' - f'UNION ALL SELECT u.rowid FROM "{table}" u WHERE {selected} ' - 'AND u.timestamp IS NULL)' - ) - scope = (thread, ToolStartTranscript.declared_name, *tools) - parameters = (*scope, since, *scope) + where = f'e.thread=? AND e.kind=? AND e.tool IN ({placeholders(tools)}) AND e.timestamp>=?' + parameters = (thread, ToolStartTranscript.declared_name, *tools, since) if judged: where += f' AND e.call_id NOT IN ({placeholders(judged)})' parameters += judged diff --git a/tools/cutover/carry_external_event_times.py b/tools/cutover/carry_external_event_times.py new file mode 100644 index 0000000000..6355896a72 --- /dev/null +++ b/tools/cutover/carry_external_event_times.py @@ -0,0 +1,73 @@ +"""Date the external events saved before #2011 made every transcript event carry its time. + +Those events are Claude tool calls; their Claude Code transcripts record when each +tool_use and tool_result happened. Each undated event takes that time; an event the +transcripts cannot date stops the carry, since readers no longer accept undated events. +""" +from __future__ import annotations + +import json +import re +from contextlib import closing +from datetime import datetime +from pathlib import Path + +from agent_comms.coordination_database import CoordinationStore +from agent_comms.external_transcript import ExternalEventRow, ExternalTranscript +from agent_comms.transcript_events import ToolEndTranscript, ToolStartTranscript + +from .carry import Carried, StoreCarry + +TRANSCRIPTS = Path.home() / ".claude" / "projects" +CALL = re.compile(r'"(?:id|tool_use_id)":"(toolu_\w+)"') +# Which transcript content item records each saved event kind. +RECORDED_BY = {"tool_use": (ToolStartTranscript.declared_name, "id"), + "tool_result": (ToolEndTranscript.declared_name, "tool_use_id")} + + +def recorded_times(wanted: set[str]) -> dict[tuple[str, str], float]: + """(kind, call id) to the time its transcript line records, for the wanted calls.""" + times: dict[tuple[str, str], float] = {} + for path in TRANSCRIPTS.glob("**/*.jsonl"): + with path.open(errors="replace") as lines: + for line in lines: + if "toolu_" not in line or not wanted.intersection(CALL.findall(line)): + continue + record = json.loads(line) + at = datetime.fromisoformat(record["timestamp"]).timestamp() + for item in record["message"]["content"]: + kind, key = RECORDED_BY.get(item.get("type"), (None, None)) + if kind and item[key] in wanted: + times.setdefault((kind, item[key]), at) + return times + + +class ExternalEventTimes(StoreCarry): + store = ExternalTranscript + + @classmethod + def carry(cls, root: Path, *, apply: bool) -> Carried: + path = cls.store.path_in(root) + if not path.exists(): + return Carried(0) + table = ExternalEventRow.declared_name + with closing(CoordinationStore.writer(path)) as db: + db.execute("BEGIN IMMEDIATE") + undated = db.execute( + f'SELECT DISTINCT kind, call_id FROM "{table}" WHERE timestamp IS NULL').fetchall() + times = recorded_times({call for _, call in undated}) + missing = [event for event in undated if tuple(event) not in times] + if missing: + db.rollback() + raise ValueError(f"{len(missing)} saved events have no transcript time, e.g. {missing[:3]}") + for kind, call in undated: + db.execute(f'UPDATE "{table}" SET event=json_set(event, \'$.timestamp\', ?) ' + 'WHERE timestamp IS NULL AND kind=? AND call_id=?', + (times[kind, call], kind, call)) + db.commit() if apply else db.rollback() + return Carried(len(undated), {"events": len(undated)}, + (path.name,) if apply and undated else ()) + + +if __name__ == "__main__": + ExternalEventTimes.main() From db384b26609f1277b9dfd6410634a0d6df9dc9be Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 18:39:25 -0400 Subject: [PATCH 03/66] Carry reads tool blocks through Claude's decoders and dates all rows in one update Co-Authored-By: Claude Opus 5.5 --- tools/cutover/carry_external_event_times.py | 66 ++++++++++++++------- 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/tools/cutover/carry_external_event_times.py b/tools/cutover/carry_external_event_times.py index 6355896a72..647c258bff 100644 --- a/tools/cutover/carry_external_event_times.py +++ b/tools/cutover/carry_external_event_times.py @@ -1,17 +1,21 @@ """Date the external events saved before #2011 made every transcript event carry its time. -Those events are Claude tool calls; their Claude Code transcripts record when each -tool_use and tool_result happened. Each undated event takes that time; an event the -transcripts cannot date stops the carry, since readers no longer accept undated events. +Those events are Claude tool calls. Replaying each Claude Code transcript through the +bridge's own reader yields the same events with their recorded times; each undated row +takes its event's time. An event no transcript dates stops the carry, since readers no +longer accept undated events. """ + from __future__ import annotations +from contextlib import closing +from pathlib import Path + import json import re -from contextlib import closing from datetime import datetime -from pathlib import Path +from agent_comms.cli_bridges.claude_protocol import ToolResult, ToolUse from agent_comms.coordination_database import CoordinationStore from agent_comms.external_transcript import ExternalEventRow, ExternalTranscript from agent_comms.transcript_events import ToolEndTranscript, ToolStartTranscript @@ -20,13 +24,16 @@ TRANSCRIPTS = Path.home() / ".claude" / "projects" CALL = re.compile(r'"(?:id|tool_use_id)":"(toolu_\w+)"') -# Which transcript content item records each saved event kind. -RECORDED_BY = {"tool_use": (ToolStartTranscript.declared_name, "id"), - "tool_result": (ToolEndTranscript.declared_name, "tool_use_id")} +# Claude's own block decoders: which saved event each block dates. +BLOCKS = { + ToolUse.wire_tag: (ToolUse, ToolStartTranscript, lambda block: block.id), + ToolResult.wire_tag: (ToolResult, ToolEndTranscript, lambda block: block.tool_use_id), +} def recorded_times(wanted: set[str]) -> dict[tuple[str, str], float]: - """(kind, call id) to the time its transcript line records, for the wanted calls.""" + """(event kind, call id) to the time its transcript line records, for the wanted calls. + The bridge's reader replays only a recent window, so the full file is read here.""" times: dict[tuple[str, str], float] = {} for path in TRANSCRIPTS.glob("**/*.jsonl"): with path.open(errors="replace") as lines: @@ -34,11 +41,16 @@ def recorded_times(wanted: set[str]) -> dict[tuple[str, str], float]: if "toolu_" not in line or not wanted.intersection(CALL.findall(line)): continue record = json.loads(line) + content = record.get("message", {}).get("content") + if "timestamp" not in record or not isinstance(content, list): + continue # snapshots and queue lines quote ids but time nothing at = datetime.fromisoformat(record["timestamp"]).timestamp() - for item in record["message"]["content"]: - kind, key = RECORDED_BY.get(item.get("type"), (None, None)) - if kind and item[key] in wanted: - times.setdefault((kind, item[key]), at) + for item in content: + block, event, call_of = BLOCKS.get(item.get("type"), (None, None, None)) + if block is not None: + call = call_of(block.model_validate(item)) + if call in wanted: + times.setdefault((event.declared_name, call), at) return times @@ -54,19 +66,31 @@ def carry(cls, root: Path, *, apply: bool) -> Carried: with closing(CoordinationStore.writer(path)) as db: db.execute("BEGIN IMMEDIATE") undated = db.execute( - f'SELECT DISTINCT kind, call_id FROM "{table}" WHERE timestamp IS NULL').fetchall() + f'SELECT DISTINCT kind, call_id FROM "{table}" WHERE timestamp IS NULL' + ).fetchall() times = recorded_times({call for _, call in undated}) missing = [event for event in undated if tuple(event) not in times] if missing: db.rollback() - raise ValueError(f"{len(missing)} saved events have no transcript time, e.g. {missing[:3]}") - for kind, call in undated: - db.execute(f'UPDATE "{table}" SET event=json_set(event, \'$.timestamp\', ?) ' - 'WHERE timestamp IS NULL AND kind=? AND call_id=?', - (times[kind, call], kind, call)) + raise ValueError( + f"{len(missing)} saved events have no transcript time, e.g. {missing[:3]}" + ) + # One pass over the table: per-event updates would each scan it, since no index leads with call_id. + db.execute( + "CREATE TEMP TABLE dated (kind TEXT, call_id TEXT, at REAL, PRIMARY KEY (kind, call_id))" + ) + db.executemany( + "INSERT INTO dated VALUES (?, ?, ?)", + ((kind, call, at) for (kind, call), at in times.items()), + ) + db.execute( + f"UPDATE \"{table}\" SET event=json_set(event, '$.timestamp', (SELECT at FROM dated d " + f'WHERE d.kind="{table}".kind AND d.call_id="{table}".call_id)) WHERE timestamp IS NULL' + ) db.commit() if apply else db.rollback() - return Carried(len(undated), {"events": len(undated)}, - (path.name,) if apply and undated else ()) + return Carried( + len(undated), {"events": len(undated)}, (path.name,) if apply and undated else () + ) if __name__ == "__main__": From bfa9807a5eb6bd5c0b71556801d81d6928783234 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 18:49:05 -0400 Subject: [PATCH 04/66] Wrap the carry's SQL and look up block decoders by membership Co-Authored-By: Claude Opus 5.5 --- tools/cutover/carry_external_event_times.py | 32 ++++++++++++--------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/tools/cutover/carry_external_event_times.py b/tools/cutover/carry_external_event_times.py index 647c258bff..d18bcc2e99 100644 --- a/tools/cutover/carry_external_event_times.py +++ b/tools/cutover/carry_external_event_times.py @@ -8,12 +8,12 @@ from __future__ import annotations -from contextlib import closing -from pathlib import Path - import json import re +from collections.abc import Callable +from contextlib import closing from datetime import datetime +from pathlib import Path from agent_comms.cli_bridges.claude_protocol import ToolResult, ToolUse from agent_comms.coordination_database import CoordinationStore @@ -25,9 +25,11 @@ TRANSCRIPTS = Path.home() / ".claude" / "projects" CALL = re.compile(r'"(?:id|tool_use_id)":"(toolu_\w+)"') # Claude's own block decoders: which saved event each block dates. -BLOCKS = { - ToolUse.wire_tag: (ToolUse, ToolStartTranscript, lambda block: block.id), - ToolResult.wire_tag: (ToolResult, ToolEndTranscript, lambda block: block.tool_use_id), +BLOCKS: dict[str, tuple[tuple[type[ToolUse | ToolResult], str, Callable[..., str]], ...]] = { + ToolUse.wire_tag: ((ToolUse, ToolStartTranscript.declared_name, lambda block: block.id),), + ToolResult.wire_tag: ( + (ToolResult, ToolEndTranscript.declared_name, lambda block: block.tool_use_id), + ), } @@ -45,12 +47,11 @@ def recorded_times(wanted: set[str]) -> dict[tuple[str, str], float]: if "timestamp" not in record or not isinstance(content, list): continue # snapshots and queue lines quote ids but time nothing at = datetime.fromisoformat(record["timestamp"]).timestamp() - for item in content: - block, event, call_of = BLOCKS.get(item.get("type"), (None, None, None)) - if block is not None: + for item in (item for item in content if item["type"] in BLOCKS): + for block, kind, call_of in BLOCKS[item["type"]]: call = call_of(block.model_validate(item)) if call in wanted: - times.setdefault((event.declared_name, call), at) + times.setdefault((kind, call), at) return times @@ -75,17 +76,20 @@ def carry(cls, root: Path, *, apply: bool) -> Carried: raise ValueError( f"{len(missing)} saved events have no transcript time, e.g. {missing[:3]}" ) - # One pass over the table: per-event updates would each scan it, since no index leads with call_id. + # One pass over the table: per-event updates would each scan it, + # since no index leads with call_id. db.execute( - "CREATE TEMP TABLE dated (kind TEXT, call_id TEXT, at REAL, PRIMARY KEY (kind, call_id))" + "CREATE TEMP TABLE dated" + " (kind TEXT, call_id TEXT, at REAL, PRIMARY KEY (kind, call_id))" ) db.executemany( "INSERT INTO dated VALUES (?, ?, ?)", ((kind, call, at) for (kind, call), at in times.items()), ) db.execute( - f"UPDATE \"{table}\" SET event=json_set(event, '$.timestamp', (SELECT at FROM dated d " - f'WHERE d.kind="{table}".kind AND d.call_id="{table}".call_id)) WHERE timestamp IS NULL' + f'UPDATE "{table}" AS t SET event=json_set(event, \'$.timestamp\',' + " (SELECT at FROM dated d WHERE d.kind=t.kind AND d.call_id=t.call_id))" + " WHERE timestamp IS NULL" ) db.commit() if apply else db.rollback() return Carried( From 4534a64e7aa5cf0301684ff53220186759a54b97 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 18:53:18 -0400 Subject: [PATCH 05/66] Decode each transcript line with the bridge's record decoder Co-Authored-By: Claude Opus 5.5 --- tools/cutover/carry_external_event_times.py | 52 ++++++++++++--------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/tools/cutover/carry_external_event_times.py b/tools/cutover/carry_external_event_times.py index d18bcc2e99..93787efa27 100644 --- a/tools/cutover/carry_external_event_times.py +++ b/tools/cutover/carry_external_event_times.py @@ -12,10 +12,17 @@ import re from collections.abc import Callable from contextlib import closing -from datetime import datetime from pathlib import Path -from agent_comms.cli_bridges.claude_protocol import ToolResult, ToolUse +from pydantic import ValidationError + +from agent_comms.cli_bridges.claude_protocol import ( + CLAUDE_RECORD, + AssistantRecord, + ToolResult, + ToolUse, + UserRecord, +) from agent_comms.coordination_database import CoordinationStore from agent_comms.external_transcript import ExternalEventRow, ExternalTranscript from agent_comms.transcript_events import ToolEndTranscript, ToolStartTranscript @@ -24,34 +31,35 @@ TRANSCRIPTS = Path.home() / ".claude" / "projects" CALL = re.compile(r'"(?:id|tool_use_id)":"(toolu_\w+)"') -# Claude's own block decoders: which saved event each block dates. -BLOCKS: dict[str, tuple[tuple[type[ToolUse | ToolResult], str, Callable[..., str]], ...]] = { - ToolUse.wire_tag: ((ToolUse, ToolStartTranscript.declared_name, lambda block: block.id),), - ToolResult.wire_tag: ( - (ToolResult, ToolEndTranscript.declared_name, lambda block: block.tool_use_id), - ), -} +# Claude's own block types, and which saved event each one dates. +BLOCKS: tuple[tuple[type[ToolUse | ToolResult], str, Callable[..., str]], ...] = ( + (ToolUse, ToolStartTranscript.declared_name, lambda block: block.id), + (ToolResult, ToolEndTranscript.declared_name, lambda block: block.tool_use_id), +) def recorded_times(wanted: set[str]) -> dict[tuple[str, str], float]: """(event kind, call id) to the time its transcript line records, for the wanted calls. - The bridge's reader replays only a recent window, so the full file is read here.""" + The bridge's reader replays only a recent window, so every line is decoded here with + the bridge's record decoder.""" times: dict[tuple[str, str], float] = {} for path in TRANSCRIPTS.glob("**/*.jsonl"): with path.open(errors="replace") as lines: - for line in lines: + for position, line in enumerate(lines): if "toolu_" not in line or not wanted.intersection(CALL.findall(line)): continue - record = json.loads(line) - content = record.get("message", {}).get("content") - if "timestamp" not in record or not isinstance(content, list): - continue # snapshots and queue lines quote ids but time nothing - at = datetime.fromisoformat(record["timestamp"]).timestamp() - for item in (item for item in content if item["type"] in BLOCKS): - for block, kind, call_of in BLOCKS[item["type"]]: - call = call_of(block.model_validate(item)) - if call in wanted: - times.setdefault((kind, call), at) + try: + record = CLAUDE_RECORD.validate_python( + {**json.loads(line), "position": position} + ) + except ValidationError: + continue # snapshots and queue lines quote call ids but are no timed record + if not isinstance(record, AssistantRecord | UserRecord): + continue + for block in record.message.content: + for decoded, kind, call_of in BLOCKS: + if isinstance(block, decoded) and call_of(block) in wanted: + times.setdefault((kind, call_of(block)), record.timestamp) return times @@ -87,7 +95,7 @@ def carry(cls, root: Path, *, apply: bool) -> Carried: ((kind, call, at) for (kind, call), at in times.items()), ) db.execute( - f'UPDATE "{table}" AS t SET event=json_set(event, \'$.timestamp\',' + f"UPDATE \"{table}\" AS t SET event=json_set(event, '$.timestamp'," " (SELECT at FROM dated d WHERE d.kind=t.kind AND d.call_id=t.call_id))" " WHERE timestamp IS NULL" ) From 4038452f6fabb71f01b6046dd1b5fe2667cc6e68 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 20:17:50 -0400 Subject: [PATCH 06/66] P265: retain malformed shell input after integrating the source-backed carry --- src/agent_comms/commander/send_guard.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/agent_comms/commander/send_guard.py b/src/agent_comms/commander/send_guard.py index 642e93b2e8..d302f042d4 100644 --- a/src/agent_comms/commander/send_guard.py +++ b/src/agent_comms/commander/send_guard.py @@ -35,11 +35,14 @@ def simple_commands(command: str) -> tuple[Words, ...]: lexer = shlex.shlex(command, posix=True, punctuation_chars=True) lexer.whitespace_split = True commands: list[list[str]] = [[]] - for word in lexer: - if word and set(word) <= set(';&|()'): - commands.append([]) - else: - commands[-1].append(word) + try: + for word in lexer: + if word and set(word) <= set(';&|()'): + commands.append([]) + else: + commands[-1].append(word) + except ValueError: + return (tuple(command.split()),) return tuple(tuple(words) for words in commands if words) From 47a5a1e3fc7810a95820d7bdd998e705b92e85e9 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 20:20:09 -0400 Subject: [PATCH 07/66] P265: verify the source-backed carry on a snapshot of the actual event store --- tests/verify_external_event_times.py | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/verify_external_event_times.py diff --git a/tests/verify_external_event_times.py b/tests/verify_external_event_times.py new file mode 100644 index 0000000000..e1d80b326a --- /dev/null +++ b/tests/verify_external_event_times.py @@ -0,0 +1,39 @@ +"""Run the source-backed carry on a SQLite snapshot, preserving the live store.""" +from __future__ import annotations + +import json +import sys +from contextlib import closing +from pathlib import Path + +from agent_comms.commander.order import COMMS +from agent_comms.coordination_database import CoordinationStore +from agent_comms.external_transcript import ExternalEventRow, ExternalTranscript +from agent_comms.field_codec import FieldCodec +from agent_comms.transcript_events import TranscriptEvent +from cutover.carry_external_event_times import ExternalEventTimes + + +root = Path(sys.argv[1]) +root.mkdir(parents=True, exist_ok=False) +source = ExternalTranscript.path_in(COMMS.root) +copy = root / source.name +with closing(CoordinationStore.read_snapshot(source)) as reader: + with closing(CoordinationStore.writer(copy)) as writer: + reader.backup(writer) +with CoordinationStore.reading(copy) as connection: + before, = connection.execute( + f'SELECT count(*) FROM "{ExternalEventRow.declared_name}" WHERE timestamp IS NULL' + ).fetchone() +print('copied undated rows:', before, flush=True) +result = ExternalEventTimes.carry(root, apply=True) +print(json.dumps(result.report('carry_external_event_times', True)), flush=True) +assert ExternalEventTimes.carry(root, apply=True).changed == 0 +count = 0 +with CoordinationStore.reading(copy) as connection: + for encoded, in connection.execute(f'SELECT event FROM "{ExternalEventRow.declared_name}"'): + event = FieldCodec.decode(TranscriptEvent, FieldCodec.parse(encoded)) + assert isinstance(event.timestamp, float) + count += 1 +print('all copied events decoded with recorded float timestamps:', count) +print('source-backed carry idempotent; live store opened read-only') From d9c3c5db96134d6a42d074b17d7566260b34dc6f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 20:30:23 -0400 Subject: [PATCH 08/66] P281: bind maintenance holds to detached process identities --- src/agent_comms/maintenance_barrier.py | 98 +++++++++++++++------ src/agent_comms/todos.py | 7 ++ tools/cutover/carry_maintenance_identity.py | 86 ++++++++++++++++++ tools/publish_runtime.py | 3 +- tools/upgrade_bus.py | 43 +++++++-- 5 files changed, 200 insertions(+), 37 deletions(-) create mode 100644 tools/cutover/carry_maintenance_identity.py diff --git a/src/agent_comms/maintenance_barrier.py b/src/agent_comms/maintenance_barrier.py index 735965d87d..0c70b2a26c 100644 --- a/src/agent_comms/maintenance_barrier.py +++ b/src/agent_comms/maintenance_barrier.py @@ -15,13 +15,14 @@ from abc import ABC, abstractmethod from collections.abc import AsyncIterator, Callable, Iterator from contextlib import AbstractAsyncContextManager, asynccontextmanager, contextmanager -from dataclasses import dataclass +from dataclasses import dataclass, replace from pathlib import Path -from typing import Annotated, Literal +from typing import Literal, Self +from .child_process import ProcessIdentity from .declared_family import DeclaredFamily from .errors import RelationViolationError, ToolRefusedError -from .field_codec import FieldCodec, Id128Text, MalformedRecord, TextRepresentation +from .field_codec import FieldCodec, Id128Text, MalformedRecord from .lifecycle import LifecycleState from .send_outcome import GrantedSend, HeldSend from .store_files import ( @@ -72,6 +73,10 @@ def require_open(): class MaintenancePhase(DeclaredFamily, LifecycleState, affix="Phase"): new_sends = running_sends = HeldSends + @classmethod + def holder_dead(cls, state: MaintenanceState) -> bool: + return not state.operator.alive() + @classmethod def hold_for(cls, barrier: MaintenanceBarrier, state: MaintenanceStatus, operator: MaintenanceOperator) -> MaintenanceState: @@ -123,6 +128,10 @@ def successors(cls): class ReadyPhase(MaintenancePhase): new_sends = running_sends = GrantedSends + @classmethod + def holder_dead(cls, state: MaintenanceState) -> bool: + return False + @classmethod def hold_for(cls, barrier: MaintenanceBarrier, state: MaintenanceStatus, operator: MaintenanceOperator) -> MaintenanceState: @@ -141,6 +150,10 @@ def hold_restore(cls, barrier: MaintenanceBarrier, operator: MaintenanceOperator drained() +class DeadHolderReleasedPhase(ReadyPhase): + """The stored operator is the dead incarnation whose hold this record released.""" + + # The holding operation hands its nonce to its own child processes through this variable. HOLDER_NONCE = "AGENT_COMMS_MAINTENANCE_HOLDER" @@ -155,6 +168,12 @@ class MaintenanceStatus: def turn_pending(self) -> bool: return self.phase.new_sends.pending() + def observed(self, barrier: MaintenanceBarrier) -> MaintenanceStatus: + return self + + async def observed_async(self, barrier: MaintenanceBarrier) -> MaintenanceStatus: + return self + def require_open(self) -> None: self.phase.new_sends.require_open() @@ -163,31 +182,19 @@ def require_open_to(self, holder: str) -> None: self.require_open() -@dataclass(frozen=True) -class MaintenanceOperator: - value: str +@dataclass(frozen=True, eq=False) +class MaintenanceOperator(ProcessIdentity, declared_name=None): + """A hold belongs to this exact process, using the existing identity codec.""" - def __post_init__(self) -> None: - if not self.value or len(self.value) > 128: - raise ValueError('Maintenance operator must have bounded identity') - - -class MaintenanceOperatorText(TextRepresentation[MaintenanceOperator]): @classmethod - def from_text(cls, value: str) -> MaintenanceOperator: - return MaintenanceOperator(value) - - @classmethod - def encode(cls, value: MaintenanceOperator) -> str: - return value.value - - -MaintenanceOperatorField = Annotated[MaintenanceOperator, MaintenanceOperatorText] + def current(cls) -> Self: + identity = ProcessIdentity.capture(os.getpid()) + return cls(identity.pid, identity.start_time) @dataclass(frozen=True, kw_only=True) class MaintenanceMarker: - version: Literal[1] + version: Literal[2] root: str generation: int @@ -198,7 +205,7 @@ def __post_init__(self): @dataclass(frozen=True, kw_only=True) class MaintenanceState(MaintenanceMarker, MaintenanceStatus): - operator: MaintenanceOperatorField + operator: MaintenanceOperator nonce: str phase: type[MaintenancePhase] @@ -206,6 +213,16 @@ def __post_init__(self): super().__post_init__() Id128Text.decode(self.nonce) + def observed(self, barrier: MaintenanceBarrier) -> MaintenanceStatus: + if self.phase.holder_dead(self): + return barrier.release_dead(self) + return self + + async def observed_async(self, barrier: MaintenanceBarrier) -> MaintenanceStatus: + if self.phase.holder_dead(self): + return await barrier.release_dead_async(self) + return self + def require_marker(self, marker: MaintenanceMarker, root: Path) -> MaintenanceState: if ( marker @@ -293,7 +310,7 @@ def _read(path: Path, declaration): os.close(descriptor) return value - def current_unlocked(self) -> MaintenanceStatus: + def _current_unlocked(self) -> MaintenanceStatus: """Check the phase under the registry lock OR the wire lock. Clients may read it before taking locks only to refuse work, never to @@ -310,6 +327,32 @@ def current_unlocked(self) -> MaintenanceStatus: raise RelationViolationError("Maintenance witness incomplete; new turns held") return state.require_marker(marker, self.registry_path.parent) + def current_unlocked(self) -> MaintenanceStatus: + return self._current_unlocked().observed(self) + + def _release_dead_unlocked(self, expected: MaintenanceState) -> MaintenanceStatus: + current = self._current_unlocked() + if current != expected: + return current + self._write_unlocked(replace( + expected, generation=expected.generation + 1, nonce=Id128Text.mint(), + phase=DeadHolderReleasedPhase, + )) + raise MaintenanceHeldError( + f"Maintenance released dead holder pid={expected.operator.pid} " + f"start_time={expected.operator.start_time}; retry the input" + ) + + def release_dead(self, expected: MaintenanceState) -> MaintenanceStatus: + # Registry acquisition already serializes every maintenance write. It is + # reentrant for a registry borrower and never upgrades a shared wire OFD. + with _store_lock(self.registry_path): + return self._release_dead_unlocked(expected) + + async def release_dead_async(self, expected: MaintenanceState) -> MaintenanceStatus: + async with _async_store_lock(self.registry_path): + return self._release_dead_unlocked(expected) + def assert_open_unlocked(self) -> MaintenanceStatus: hold = self.current_unlocked() hold.require_open_to(os.environ.get(HOLDER_NONCE, "")) @@ -333,9 +376,8 @@ def begin(self, operator: MaintenanceOperator) -> MaintenanceState: """Hold new turns before fleet drain; an uncertain write stays closed.""" with _store_lock(self.wire_path), _store_lock(self.registry_path): current = self.current_unlocked() - if current.phase is not ReadyPhase: - raise RelationViolationError('Maintenance already active or uncertain') - hold = MaintenanceState(version=1, root=str(self.registry_path.parent.resolve()), + current.require_open() + hold = MaintenanceState(version=2, root=str(self.registry_path.parent.resolve()), generation=current.generation + 1, operator=operator, nonce=Id128Text.mint(), phase=DrainingPhase) self._write_unlocked(hold) @@ -405,4 +447,4 @@ async def ingress_async(self) -> AsyncIterator[MaintenanceStatus]: pipe drain or a native response. """ async with _async_store_lock(self.wire_path, mode=SharedLock()): - yield self.current_unlocked() + yield await self._current_unlocked().observed_async(self) diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index ea58133091..8c91ebda00 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -37,6 +37,7 @@ from .channel_targets import CommandTarget from .goals import CheckedCommit, CountCheckResult, OrderCheck, ProblemGoalOrder from .recorded_time import EventTime, RecordedEventTime, UnrecordedEventTime +from .maintenance_barrier import MaintenanceBarrier from .thread_identity import RecordedThreadIncarnation, ThreadIncarnation from .threads import Thread from .typed_table import Column, Index, JsonStorage, TypedStore, TypedTable @@ -2537,6 +2538,12 @@ def writing(self) -> Iterator[sqlite3.Connection]: f"{self.path}: {type(error).__name__}: {error}" ) from error + @contextmanager + def writer_connection(self, *, check_same_thread: bool = True) -> Iterator[sqlite3.Connection]: + with MaintenanceBarrier(self.root / "registry.json").allow_input(): + with super().writer_connection(check_same_thread=check_same_thread) as connection: + yield connection + def record_hunt(self, commander: ThreadIncarnation, hunt: Hunt) -> HuntRecord: hunt.require_commander(self, commander) recorded = HuntRecord(uuid4().hex, commander, hunt) diff --git a/tools/cutover/carry_maintenance_identity.py b/tools/cutover/carry_maintenance_identity.py new file mode 100644 index 0000000000..e5150c7382 --- /dev/null +++ b/tools/cutover/carry_maintenance_identity.py @@ -0,0 +1,86 @@ +"""Carry an old READY maintenance witness from a name to an exact process record. + +A legacy active hold contains no PID or birth time. It cannot honestly be called +abandoned; this one-time converter refuses it rather than fabricating an owner. +There is no legacy reader in the runtime barrier. +""" +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Literal + +from agent_comms.errors import RelationViolationError +from agent_comms.field_codec import FieldCodec +from agent_comms.maintenance_barrier import ( + MaintenanceBarrier, + MaintenanceOperator, + MaintenanceState, + ReadyPhase, +) +from agent_comms.field_codec import Id128Text +from .carry import Carried, Carry + + +@dataclass(frozen=True, kw_only=True) +class OldMarker: + version: Literal[1] + root: str + generation: int + + +@dataclass(frozen=True, kw_only=True) +class OldReadyState(OldMarker): + operator: str + nonce: str + phase: type[ReadyPhase] + + def carried(self, barrier: MaintenanceBarrier, *, apply: bool) -> Carried: + marker = FieldCodec.decode(OldMarker, FieldCodec.parse(barrier.marker_path.read_text())) + expected = OldMarker(version=1, root=self.root, generation=self.generation) + if marker != expected or self.root != str(barrier.registry_path.parent.resolve()): + raise RelationViolationError("Legacy maintenance witnesses do not identify this root") + converted = MaintenanceState( + version=2, root=self.root, generation=self.generation + 1, + operator=MaintenanceOperator.current(), nonce=Id128Text.mint(), phase=ReadyPhase, + ) + if apply: + barrier._write_unlocked(converted) + return Carried(1, rewritten=tuple(path.name for path in barrier.witness_paths)) + + +@dataclass(frozen=True, kw_only=True) +class IdentifiedState(MaintenanceState): + def carried(self, barrier: MaintenanceBarrier, *, apply: bool) -> Carried: + barrier._current_unlocked() + return Carried(0) + + +class MaintenanceIdentity(Carry): + @classmethod + def precedes(cls, later: type[Carry]) -> bool: + return later is not cls + + @classmethod + def backup_files(cls, root: Path) -> tuple[Path, ...]: + return MaintenanceBarrier(root / "registry.json").witness_paths + + @classmethod + def before_stop(cls, root: Path, *, apply: bool) -> Carried: + return cls.carry(root, apply=apply) + + @classmethod + def carry(cls, root: Path, *, apply: bool) -> Carried: + barrier = MaintenanceBarrier(root / "registry.json") + if not barrier.state_path.exists(): + if barrier.marker_path.exists(): + raise RelationViolationError("Maintenance has a marker but no stored holder") + return Carried(0) + state = FieldCodec.decode( + IdentifiedState | OldReadyState, FieldCodec.parse(barrier.state_path.read_text()) + ) + return state.carried(barrier, apply=apply) + + +if __name__ == "__main__": + MaintenanceIdentity.main() diff --git a/tools/publish_runtime.py b/tools/publish_runtime.py index d5d15cf99f..c024104744 100644 --- a/tools/publish_runtime.py +++ b/tools/publish_runtime.py @@ -133,8 +133,7 @@ def publish(self, root: Path, *, checkout: Path, timeout: float = 1200, raise ValueError('Publication requires the selected active root') comms = wire(route) barrier = comms.owners.maintenance - operator = MaintenanceOperator('runtime-publication:' + hashlib.sha256( - os.fsencode(self.runtime.resolve())).hexdigest()) + operator = MaintenanceOperator.current() intent_path = root / 'runtime-publication.json' with _store_lock(root / 'runtime-publication'): previous = StoppedOwnerInstallation.current_pointer().resolve(strict=True) diff --git a/tools/upgrade_bus.py b/tools/upgrade_bus.py index 8e7c9a6eb7..884b2b3c23 100644 --- a/tools/upgrade_bus.py +++ b/tools/upgrade_bus.py @@ -49,7 +49,7 @@ import agent_comms.activity_checkpoint # noqa: F401 (declares the activity runtime files) from agent_comms.external_transcript import ExternalTranscript -from agent_comms.field_codec import FieldCodec +from agent_comms.field_codec import FieldCodec, Id128Text from agent_comms.history_result import EverythingHistoryPageResult from agent_comms.host_services import HostService from agent_comms.maintenance_barrier import HOLDER_NONCE, MaintenanceBarrier, MaintenanceOperator @@ -261,7 +261,7 @@ def rehearse(root: Path, pending: tuple[Step, ...], stack: Stack, revisions: Rev stack.prepare_native() # Hold the copy before reading it, exactly as the live run holds the bus. os.environ[HOLDER_NONCE] = MaintenanceBarrier(copy / "registry.json").begin( - MaintenanceOperator("upgrade-bus")).nonce + MaintenanceOperator.current()).nonce with transcripts_held(copy): before = bus_files(copy) stopping = before_stop(copy, pending, stack, revisions) @@ -366,15 +366,15 @@ def live(checkout: Path, root: Path, current: str, target: str, pending: tuple[S installed: Stack, staged: Stack, revisions: Revisions, installer: str) -> dict: """The rehearsal's passes under the rehearsal's stack; ``installed`` only stops, starts and reads back its own owners.""" - running = [thread["name"] for thread in installed.cli(root, "threads")["threads"] - if thread["role"] == "agent" and thread["status"] == "running"] before_stop(root, pending, staged, revisions) + running = [thread["name"] for thread in staged.cli(root, "threads")["threads"] + if thread["role"] == "agent" and thread["status"] == "running"] maintenance = MaintenanceBarrier(RegistryStore.at(root).path) - held = maintenance.begin(MaintenanceOperator("upgrade-bus")) + held = maintenance.begin(MaintenanceOperator.current()) os.environ[HOLDER_NONCE] = held.nonce # this run's own CLI calls read under its hold lock = installed.lock try: - installed.owners(root, "stop", running) + staged.owners(root, "stop", running) with transcripts_held(root): backup = Backup.take(root, ROUTE, (root,), root.parent / f"{root.name}.upgrade-{time.strftime('%Y%m%dT%H%M%S')}") @@ -396,7 +396,7 @@ def live(checkout: Path, root: Path, current: str, target: str, pending: tuple[S PlannedInstallDeadline.after(installer, time.time()).plan(root) except BaseException as error: state = maintenance.read() - state.phase.hold_for(maintenance, state, MaintenanceOperator("upgrade-bus")) + state.phase.hold_for(maintenance, state, MaintenanceOperator.current()) print(f"upgrade-bus: stopped after the backup ({type(error).__name__}: {error}); " f"maintenance held, checkout and bus preserved; backup: {backup.path}", file=sys.stderr) @@ -406,6 +406,31 @@ def live(checkout: Path, root: Path, current: str, target: str, pending: tuple[S "backup": str(backup.path)} +def detached_live(command: tuple[str, ...], root: Path) -> int: + """The live installer is a new session, not the monitor's owned child group. + + Its output descriptors belong to files, not a caller that may disappear. + Deliberately do not borrow a turn-owned child lifetime or use Popen's cleanup + context: retiring the observer must not terminate this independent install. + """ + logs = root.parent / f".{root.name}.installer-{Id128Text.mint()}" + logs.mkdir(mode=0o700) + output, errors = logs / "result.json", logs / "stderr.log" + with output.open("xb") as stdout, errors.open("xb") as stderr: + process = subprocess.Popen( + command, stdin=subprocess.DEVNULL, stdout=stdout, stderr=stderr, + start_new_session=True, + ) + print(f"upgrade-bus: detached installer pid={process.pid}; diagnostics {errors}", + file=sys.stderr, flush=True) + code = process.wait() + if code: + print(f"upgrade-bus: detached installer failed ({code}); see {errors}", file=sys.stderr) + else: + print(output.read_text(), end="") + return code + + def stopped(signum: int, frame: object) -> None: """A stop signal runs the same abort as a failure; a second one cannot cut that abort short.""" for each in (signal.SIGTERM, signal.SIGINT): @@ -423,6 +448,10 @@ def main() -> None: help="The unit running this install; the plan names it") parser.add_argument("--rehearse", action="store_true", help="Carry and read a copy of the bus; change nothing") args = parser.parse_args() + if not args.rehearse and os.getsid(0) != os.getpid(): + raise SystemExit(detached_live( + (sys.executable, str(Path(__file__).resolve()), *sys.argv[1:]), args.root, + )) for signum in (signal.SIGTERM, signal.SIGINT): signal.signal(signum, stopped) try: From 81d831be99554033fd69d031dc7cfdad9e26098f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 20:38:47 -0400 Subject: [PATCH 09/66] P281: migrate maintenance consumers and cover real holder death --- docs/maintenance-holder.md | 41 ++++++ tests/test_carry.py | 2 +- tests/test_cli_watch_exit.py | 4 +- tests/test_connector.py | 2 +- tests/test_maintenance_barrier.py | 45 +++--- tests/test_maintenance_holder.py | 152 ++++++++++++++++++++ tests/test_maintenance_native_boundary.py | 4 +- tests/test_registration_process_identity.py | 2 +- tests/test_stall_watcher_strike.py | 2 +- tests/test_thread_identity.py | 4 +- tests/verify_connector_maintenance.py | 2 +- 11 files changed, 226 insertions(+), 34 deletions(-) create mode 100644 docs/maintenance-holder.md create mode 100644 tests/test_maintenance_holder.py diff --git a/docs/maintenance-holder.md b/docs/maintenance-holder.md new file mode 100644 index 0000000000..51f3ef0919 --- /dev/null +++ b/docs/maintenance-holder.md @@ -0,0 +1,41 @@ +# Process-owned maintenance (P281) + +The barrier stores the holder's PID and kernel birth time through the existing +`ProcessIdentity`/`FieldCodec` implementation. Names are not identities. READY +records describe completed work and do not depend on the old operator staying +alive. + +A non-READY read checks that exact process. Recovery compares the observed +record under the existing registry lock, writes `dead_holder_released` with the +dead PID/birth time, and refuses the detecting input. The following input uses +READY behavior. The asynchronous ingress borrows the asynchronous registry +lock; neither path upgrades an admitted shared wire descriptor. + +Live installs run in a separate session with private file-backed output, so +losing the calling session does not kill the installer or break its output +pipe. The worker records its own identity. Rehearsals remain synchronous. + +`TodoStore.writer_connection` admits all todo writers before opening SQLite, +including commander first-call, hunt, overdue and reconciliation writes. + +One registered carry converts version-1 READY witnesses. An old active record +contains only a name: its holder's death cannot be established. The carry +refuses that record rather than fabricating a PID or retaining a name fallback. +The runtime reads only version 2. + +## Delivery checklist + +- [x] Implementation checkpoint pushed on `fix/p281-maintenance-holder`. +- [x] Production AST inventory: 381 Core modules and 39 tools; no parse omissions. +- [x] Migrate all discovered producer and test callers off string operators. +- [ ] Focused checks, tools ratchet and src ratchet against `origin/main`. +- [ ] Copied-bus live-phase proof using existing installed Python; no build, + environment creation or `uv sync` is authorized for this proof. +- [ ] Prove calling-session death leaves the worker alive, then SIGKILL the + worker mid-drain; record first refusal, second acceptance and release JSON. +- [ ] Open P281's PR through the commander's canonical PR command. + +The inventory includes declarations, inheritance, imports, writes, decisions +and references throughout both production roots. Generated test child programs +are additionally located by source search; AST does not resolve their runtime +imports or prove behavior. diff --git a/tests/test_carry.py b/tests/test_carry.py index 0e67a12d56..a5d72d71e0 100644 --- a/tests/test_carry.py +++ b/tests/test_carry.py @@ -751,7 +751,7 @@ def test_a_writer_during_the_drain_is_refused_or_its_file_is_declared(comms, mon output.status(ActivityState.WORKING, "before the drain") with transcript.writing(): pass - MaintenanceBarrier(comms.root / "registry.json").begin(MaintenanceOperator("drain-test")) + MaintenanceBarrier(comms.root / "registry.json").begin(MaintenanceOperator.current()) before = bus_files(comms.root) monkeypatch.setattr(CoordinationStore, "writer_timeout", 0.1) with transcripts_held(comms.root): diff --git a/tests/test_cli_watch_exit.py b/tests/test_cli_watch_exit.py index 41ce78fb5d..da0d0f7d9c 100644 --- a/tests/test_cli_watch_exit.py +++ b/tests/test_cli_watch_exit.py @@ -192,7 +192,7 @@ async def deliver(self, message): try: assert await asyncio.wait_for(arrived.get(), 20) == 'first' maintenance = MaintenanceBarrier(comms.registry.store.path) - held = maintenance.begin(MaintenanceOperator('upgrade-bus')) + held = maintenance.begin(MaintenanceOperator.current()) assert await asyncio.wait_for(children[0].wait(), 20) is not None (tmp_path / 'checkout/src').mkdir(parents=True) repo = Repository(tmp_path / 'checkout') @@ -230,7 +230,7 @@ async def test_a_bridge_started_during_a_drain_waits_and_then_receives(tmp_path, comms.registry.declare(replace(recipient, execution=ExternalThreadExecution)) monkeypatch.setenv('HOME', str(tmp_path / 'home')) maintenance = MaintenanceBarrier(comms.registry.store.path) - held = maintenance.begin(MaintenanceOperator('upgrade-bus')) + held = maintenance.begin(MaintenanceOperator.current()) arrived: asyncio.Queue[str] = asyncio.Queue() children = [] start = AttachedChild.start diff --git a/tests/test_connector.py b/tests/test_connector.py index 4b6be05fbd..f519a77151 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -306,7 +306,7 @@ async def test_maintenance_refuses_before_locked_stores_on_a_copied_bus( async with TestClient(TestServer(connector.application())) as reader: headers = {"Authorization": f"Bearer {await approved_token(reader, store)}"} barrier = MaintenanceBarrier(copied_root / "registry.json") - held = barrier.begin(MaintenanceOperator("connector-refusal-proof")) + held = barrier.begin(MaintenanceOperator.current()) for phase in phases: held = barrier.advance(held, phase) with _store_lock(barrier.wire_path), _store_lock(barrier.registry_path), ExitStack() as locks: diff --git a/tests/test_maintenance_barrier.py b/tests/test_maintenance_barrier.py index 84446c15f2..368d6f449d 100644 --- a/tests/test_maintenance_barrier.py +++ b/tests/test_maintenance_barrier.py @@ -20,7 +20,7 @@ DrainingPhase, InstallingPhase, MaintenanceBarrier, - MaintenanceOperatorField, + MaintenanceOperator, MaintenancePhase, PausedPhase, ReadyPhase, @@ -44,7 +44,7 @@ async def test_channel_post_queues_without_start_during_cutover(tmp_path: Path, # A post pending when the drain begins; the drain refuses new publication. # Use the actual scratch bus, not a constructed message pretending to be a post. post = comms.messaging.send('sender', '#team', 'mid-drain channel post').message - hold = control.begin(FieldCodec.decode(MaintenanceOperatorField, 'scratch-cutover')) + hold = control.begin(MaintenanceOperator.current()) queued = ScheduledTurn.incoming(post) starts = [] @@ -83,7 +83,7 @@ async def start(*args, **kwargs): assert next_sealed_assignment(store, RecordedThreadIncarnation("fixture", beta.created_at).lookup, beta.name).wire_seq == post.seq # Selection scheduled just before another drain must still refuse at the # effect boundary without consuming that durable assignment. - another_drain = control.begin(FieldCodec.decode(MaintenanceOperatorField, 'second-cutover')) + another_drain = control.begin(MaintenanceOperator.current()) assert await selected[0].run() == another_drain with Coordination(comms.root / 'coordination.sqlite3') as store: assert next_sealed_assignment(store, RecordedThreadIncarnation("fixture", beta.created_at).lookup, beta.name).wire_seq == post.seq @@ -108,7 +108,7 @@ async def test_cutover_clear_delivers_pending_channel_post(tmp_path: Path, monke comms, agent, _root_id = _session(tmp_path) gate = comms.owners.maintenance post = comms.messaging.send('sender', '#team', 'while draining').message - hold = gate.begin(FieldCodec.decode(MaintenanceOperatorField, 'scratch-cutover')) + hold = gate.begin(MaintenanceOperator.current()) assert await agent.inputs.drain_inbox('beta') == 0 assert await agent.inputs.drain_inbox('beta') == 0 beta = comms.registry.require('beta') @@ -133,12 +133,12 @@ async def test_cutover_clear_delivers_pending_channel_post(tmp_path: Path, monke @pytest.mark.parametrize('text', [None, '', 'x' * 129, 42]) def test_maintenance_operator_rejects_invalid_external_text(text: object) -> None: with pytest.raises((ValueError, TypeError)): - FieldCodec.decode(MaintenanceOperatorField, text) + FieldCodec.decode(MaintenanceOperator, text) def test_draining_lends_existing_turn_but_holds_new_prompt(tmp_path: Path) -> None: gate = MaintenanceBarrier(tmp_path / 'wire' / 'registry.json') - hold = gate.begin(FieldCodec.decode(MaintenanceOperatorField, 'operator')) + hold = gate.begin(MaintenanceOperator.current()) assert hold.phase is DrainingPhase with gate.read().phase.send_new(None) as new: assert isinstance(new, HeldSend) @@ -153,14 +153,14 @@ def test_draining_lends_existing_turn_but_holds_new_prompt(tmp_path: Path) -> No def test_phase_owner_is_default_off_and_clear_is_exact(tmp_path: Path) -> None: gate = Comms(tmp_path / 'fresh').owners.maintenance assert gate.read().phase is ReadyPhase - hold = gate.begin(FieldCodec.decode(MaintenanceOperatorField, 'operator')) + hold = gate.begin(MaintenanceOperator.current()) assert gate.read() == hold ready = gate.clear(hold) assert ready.phase is ReadyPhase and gate.read().phase is ReadyPhase with pytest.raises(RelationViolationError, match='generation/operator'): gate.clear(hold) assert gate.marker_path.is_file() and gate.state_path.is_file() - assert gate.read().operator == FieldCodec.decode(MaintenanceOperatorField, 'operator') + assert gate.read().operator == MaintenanceOperator.current() def test_only_hold_bound_cutover_may_retire_during_draining(tmp_path: Path) -> None: @@ -175,7 +175,7 @@ def complete(self, stopped): raise AssertionError('No process should be stopped by a phase check') comms = Comms(tmp_path / 'wire') - hold = comms.owners.maintenance.begin(FieldCodec.decode(MaintenanceOperatorField, 'scratch-cutover')) + hold = comms.owners.maintenance.begin(MaintenanceOperator.current()) cutover = ScratchCutover() cutover.maintenance_hold = hold with _store_lock(comms.root / 'wire'): @@ -193,13 +193,13 @@ def complete(self, stopped): def test_clear_stopped_uses_acquired_wire_descriptor(tmp_path: Path) -> None: gate = MaintenanceBarrier(tmp_path / 'wire' / 'registry.json') - hold = gate.begin(FieldCodec.decode(MaintenanceOperatorField, 'stopped-cutover')) + hold = gate.begin(MaintenanceOperator.current()) with _store_lock(gate.wire_path) as lock: assert gate.clear_stopped(hold, lock.descriptor).phase is ReadyPhase assert gate.read().phase is ReadyPhase with _store_lock(tmp_path / 'unrelated') as other: with pytest.raises(RelationViolationError, match='wire lock changed'): - next_hold = gate.begin(FieldCodec.decode(MaintenanceOperatorField, 'new-cutover')) + next_hold = gate.begin(MaintenanceOperator.current()) gate.clear_stopped(next_hold, other.descriptor) @@ -246,7 +246,7 @@ def test_default_off_then_close_reopen_and_no_stale_transition(tmp_path: Path) - assert comms.agents.begin_turn("owner", "before") comms.agents.finish_turn(comms.registry.require("owner").turn_lease) control = gate - first = control.begin(FieldCodec.decode(MaintenanceOperatorField, "operator-one")) + first = control.begin(MaintenanceOperator.current()) assert first.phase == MaintenancePhase.member_named("draining") and first.generation == 1 assert MaintenanceBarrier(comms.registry.store.path).read() == first with pytest.raises(RelationViolationError, match="Maintenance"): @@ -278,7 +278,7 @@ def test_state_generation_requires_exact_matching_positive_int( ) -> None: gate = MaintenanceBarrier(tmp_path / "wire" / "registry.json") assert gate.read().phase is ReadyPhase # Fresh roots remain default OFF. - gate.begin(FieldCodec.decode(MaintenanceOperatorField, "disposable-fixture")) + gate.begin(MaintenanceOperator.current()) state = json.loads(gate.state_path.read_text()) state["phase"] = "ready" # Disposable READY detects a false-open parser result. state["generation"] = generation @@ -303,7 +303,7 @@ def test_state_generation_requires_exact_matching_positive_int( ) def test_enabled_witness_damage_never_restores_default_off(tmp_path: Path, fault: str) -> None: gate = MaintenanceBarrier(tmp_path / "wire" / "registry.json") - gate.begin(FieldCodec.decode(MaintenanceOperatorField, "operator")) + gate.begin(MaintenanceOperator.current()) if fault == "missing-state": gate.state_path.unlink() elif fault == "missing-marker": @@ -332,7 +332,7 @@ async def test_direct_claim_and_bind_denied_after_phase_ack_in_other_process(tmp comms.registry.declare( comms.registry.new_thread(name='owner', tags=frozenset(), worktree=str(tmp_path), process_identity=ProcessIdentity.capture(proc.pid)) ) - hold = comms.owners.maintenance.begin(FieldCodec.decode(MaintenanceOperatorField, "operator")) + hold = comms.owners.maintenance.begin(MaintenanceOperator.current()) ready.set() proc.join(10) assert proc.exitcode == 0 @@ -362,7 +362,7 @@ def uncertain_write(path: Path, text: str, *, fsync_parent: bool = False) -> Non monkeypatch.setattr(barrier_module, "_atomic_write_text", uncertain_write) with pytest.raises(OSError, match="injected"): - gate.begin(FieldCodec.decode(MaintenanceOperatorField, "operator")) + gate.begin(MaintenanceOperator.current()) with pytest.raises(RelationViolationError, match="Maintenance"): gate.assert_open_unlocked() if fault_at == "before-state": @@ -380,7 +380,7 @@ def test_rename_and_stopped_same_pid_cannot_reactivate_under_gate(tmp_path: Path comms.registry.declare(owner) comms.registry.rename("owner", "renamed") comms.registry.unregister("renamed") - comms.owners.maintenance.begin(FieldCodec.decode(MaintenanceOperatorField, "operator")) + comms.owners.maintenance.begin(MaintenanceOperator.current()) with pytest.raises(RelationViolationError, match="Maintenance"): comms.owners.start("owner") with pytest.raises(RelationViolationError, match="Maintenance"): @@ -400,7 +400,7 @@ def test_cross_process_claim_races_pause_at_registry_lock(tmp_path: Path) -> Non comms.registry.new_thread(name='owner', tags=frozenset(), worktree=str(tmp_path), process_identity=ProcessIdentity.capture(child.pid)) ) ready.set() - hold = comms.owners.maintenance.begin(FieldCodec.decode(MaintenanceOperatorField, "operator")) + hold = comms.owners.maintenance.begin(MaintenanceOperator.current()) child.join(10) assert child.exitcode == 0 result, _ = q.get(timeout=2) @@ -423,7 +423,7 @@ async def test_actual_native_held_input_resumes_from_existing_observer(native_ba async with owner.open_owner(runtime_enabled=True) as (agent, session_id): async with owner.original_input(agent, session_id, "Held original input") as turn: gate = agent._comms.owners.maintenance - hold = gate.begin(FieldCodec.decode(MaintenanceOperatorField, "operator")) + hold = gate.begin(MaintenanceOperator.current()) sending = asyncio.create_task(turn.run_native()) try: await wait_for(lambda: sending in TurnSession.active and @@ -484,7 +484,7 @@ async def fake_send() -> None: assert release.wait(5) def pause() -> None: - control.begin(FieldCodec.decode(MaintenanceOperatorField, "operator")) + control.begin(MaintenanceOperator.current()) changed.set() sender = threading.Thread(target=lambda: asyncio.run(fake_send())) @@ -517,7 +517,7 @@ def written() -> tuple[bytes, ...]: before = written() gate = MaintenanceBarrier(tmp_path / 'registry.json') - hold = gate.begin(FieldCodec.decode(MaintenanceOperatorField, 'upgrade-bus')) + hold = gate.begin(MaintenanceOperator.current()) for argv in (('send', '--from', 'a', '--to', 'b', '--body', 'hi'), ('activity', '--name', 'a', '--state', 'working')): code, out = cli(*argv) @@ -531,7 +531,6 @@ async def test_bus_waits_before_mutating_a_transcript_batch(tmp_path: Path) -> N from functools import partial from agent_comms.activity import ActivityState from agent_comms.runtime_updates import AppendDraftItem, StartedItem, TextDraft, ThreadOutput - from agent_comms.maintenance_barrier import MaintenanceOperator from agent_comms.recorded_time import RecordedEventTime from agent_comms.transcript_events import AssistantTranscript @@ -542,7 +541,7 @@ async def test_bus_waits_before_mutating_a_transcript_batch(tmp_path: Path) -> N TextDraft.from_parts(AssistantTranscript, ('prefix',), RecordedEventTime(1.), 'item')), AppendDraftItem('turn', 'item', ' once')] barrier = MaintenanceBarrier(comms.registry.store.path) - hold = barrier.begin(MaintenanceOperator('batch-check')) + hold = barrier.begin(MaintenanceOperator.current()) pending = asyncio.create_task(comms.bus.when_open(partial(output.publish_batch, updates))) try: with pytest.raises(TimeoutError): diff --git a/tests/test_maintenance_holder.py b/tests/test_maintenance_holder.py new file mode 100644 index 0000000000..884c2d82c1 --- /dev/null +++ b/tests/test_maintenance_holder.py @@ -0,0 +1,152 @@ +"""Exact, real process death releases maintenance without a timeout or PID table fake.""" +from __future__ import annotations + +import asyncio +import multiprocessing as mp +import os +from contextlib import contextmanager +from pathlib import Path + +import pytest + +from agent_comms.field_codec import FieldCodec, MalformedRecord +from agent_comms.maintenance_barrier import ( + DeadHolderReleasedPhase, DrainingPhase, InstallingPhase, MaintenanceBarrier, + MaintenanceHeldError, MaintenanceOperator, MaintenancePhase, MaintenanceState, PausedPhase, ReadyPhase, +) +from agent_comms.store_files import ExclusiveLock, SharedLock, _async_store_lock, _store_lock +from agent_comms.todos import GoalRef, RecordFirstCallTodo, TodoStore +from agent_comms.recorded_time import RecordedEventTime +from cutover.carry_maintenance_identity import MaintenanceIdentity +from test_todos import order, thread + + +def _hold(root: Path, phase: type[MaintenancePhase], ready, finish) -> None: + barrier = MaintenanceBarrier(root / 'registry.json') + held = barrier.begin(MaintenanceOperator.current()) + for successor in (PausedPhase, InstallingPhase): + if held.phase is phase: + break + held = barrier.advance(held, successor) + ready.send(FieldCodec.dumps(held)) + ready.close() + finish.wait(20) + + +@contextmanager +def real_holder(root: Path, phase: type[MaintenancePhase] = DrainingPhase): + receive, send = mp.Pipe(duplex=False) + finish = mp.Event() + child = mp.Process(target=_hold, args=(root, phase, send, finish)) + child.start() + send.close() + try: + assert receive.poll(10), 'Real holder did not publish its hold' + held = FieldCodec.decode(MaintenanceState, FieldCodec.parse(receive.recv())) + assert held.operator.pid == child.pid and held.operator.alive() + yield child, held + finally: + finish.set() + if child.is_alive(): + child.kill() + child.join(5) + receive.close() + assert not child.is_alive() + + +def test_operator_uses_process_identity_codec() -> None: + operator = MaintenanceOperator.current() + assert operator.pid == os.getpid() and operator.start_time > 0 + assert FieldCodec.encode(operator) == {'pid': operator.pid, 'start_time': operator.start_time} + assert FieldCodec.decode(MaintenanceOperator, FieldCodec.encode(operator)) == operator + with pytest.raises(MalformedRecord): + FieldCodec.decode(MaintenanceOperator, 'upgrade-bus') + + +@pytest.mark.parametrize('phase', [DrainingPhase, PausedPhase, InstallingPhase]) +@pytest.mark.parametrize('file,mode', [('wire', SharedLock()), ('registry.json', ExclusiveLock())]) +def test_dead_holder_refuses_detecting_input_then_opens(tmp_path, phase, file, mode): + root = tmp_path / 'bus' + barrier = MaintenanceBarrier(root / 'registry.json') + with real_holder(root, phase) as (child, held): + child.kill() + child.join(5) + with _store_lock(root / file, mode=mode): + with pytest.raises(MaintenanceHeldError, match=f'pid={held.operator.pid} start_time={held.operator.start_time}'): + barrier.assert_open_unlocked() + released = barrier.read() + assert released.phase is DeadHolderReleasedPhase + assert released.operator == held.operator + assert released.generation == held.generation + 1 + with barrier.allow_input(): + pass + assert barrier.read() == released + # A released record is READY behavior, not a special-case phase table. + replacement = barrier.begin(MaintenanceOperator.current()) + assert replacement.phase is DrainingPhase + barrier.clear(replacement) + + +async def test_async_recovery_borrows_registry_without_blocking_event_loop(tmp_path: Path): + root = tmp_path / 'bus' + barrier = MaintenanceBarrier(root / 'registry.json') + with real_holder(root) as (child, _held): + child.kill() + child.join(5) + + async def incoming(): + async with barrier.ingress_async(): + pytest.fail('The detecting input must be refused') + + async with _async_store_lock(root / 'registry.json'): + attempt = asyncio.create_task(incoming()) + await asyncio.sleep(.025) + assert not attempt.done() + with pytest.raises(MaintenanceHeldError, match='released dead holder'): + await asyncio.wait_for(attempt, 5) + async with barrier.ingress_async() as status: + status.require_open() + + +def test_commander_first_call_write_is_admitted_before_sqlite_mutation(tmp_path: Path): + store = TodoStore(tmp_path) + lead = thread('lead', tmp_path, 1.) + original = store.create( + 'P1', 'org/repo', 'Fix maintenance', creator=lead, owner=lead, + generation='one', goal=GoalRef('lead', 1., 'request'), + deadline=RecordedEventTime(1000), order=order(tmp_path), + ) + barrier = MaintenanceBarrier(tmp_path / 'registry.json') + held = barrier.begin(MaintenanceOperator.current()) + before = store.path.read_bytes() + with pytest.raises(MaintenanceHeldError): + store.apply('P1', RecordFirstCallTodo(original.revision, lead.incarnation, 'observed')) + assert store.path.read_bytes() == before and store.get('P1') == original + barrier.clear(held) + assert store.apply('P1', RecordFirstCallTodo(original.revision, lead.incarnation, 'observed')).revision == original.revision + 1 + + +def test_legacy_ready_has_one_registered_conversion(tmp_path: Path): + barrier = MaintenanceBarrier(tmp_path / 'registry.json') + tmp_path.mkdir(exist_ok=True) + marker = {'version': 1, 'root': str(tmp_path.resolve()), 'generation': 4} + state = {**marker, 'operator': 'upgrade-bus', 'nonce': 'a' * 32, 'phase': 'ready'} + barrier.marker_path.write_text(FieldCodec.dumps(marker)) + barrier.state_path.write_text(FieldCodec.dumps(state)) + assert MaintenanceIdentity.carry(tmp_path, apply=False).changed == 1 + assert MaintenanceIdentity.carry(tmp_path, apply=True).changed == 1 + assert barrier.read().phase is ReadyPhase + assert barrier.read().operator == MaintenanceOperator.current() + assert MaintenanceIdentity.carry(tmp_path, apply=True).changed == 0 + + +def test_legacy_active_name_is_not_fabricated_into_a_process(tmp_path: Path): + barrier = MaintenanceBarrier(tmp_path / 'registry.json') + marker = {'version': 1, 'root': str(tmp_path.resolve()), 'generation': 4} + state = {**marker, 'operator': 'upgrade-bus', 'nonce': 'a' * 32, 'phase': 'draining'} + barrier.marker_path.write_text(FieldCodec.dumps(marker)) + barrier.state_path.write_text(FieldCodec.dumps(state)) + before = barrier.state_path.read_bytes() + with pytest.raises(MalformedRecord): + MaintenanceIdentity.carry(tmp_path, apply=True) + assert barrier.state_path.read_bytes() == before diff --git a/tests/test_maintenance_native_boundary.py b/tests/test_maintenance_native_boundary.py index b12b4e3502..ee252d01b6 100644 --- a/tests/test_maintenance_native_boundary.py +++ b/tests/test_maintenance_native_boundary.py @@ -32,12 +32,12 @@ async def test_private_native_raw_prompt_refused_after_pause_ack(tmp_path: Path, "from pathlib import Path\n" "from agent_comms.field_codec import FieldCodec\n" "from agent_comms.maintenance_barrier import (\n" - " MaintenanceBarrier, MaintenanceOperatorField, PausedPhase)\n" + " MaintenanceBarrier, MaintenanceOperator, PausedPhase)\n" f"Path({str(child_pid)!r}).write_text(str(os.getpid()))\n" "request = json.loads(sys.stdin.readline())\n" f"with open({str(received)!r}, 'a') as log: log.write(json.dumps(request) + '\\n')\n" f"control = MaintenanceBarrier(Path({str(root / 'registry.json')!r}))\n" - "first = control.begin(FieldCodec.decode(MaintenanceOperatorField, 'disposable-operator'))\n" + "first = control.begin(MaintenanceOperator.current())\n" "assert control.advance(first, PausedPhase).phase is PausedPhase\n" "file = sys.argv[1]\n" f"reply = {{'type':'response','id':request['id'],'command':'get_state','success':True," diff --git a/tests/test_registration_process_identity.py b/tests/test_registration_process_identity.py index eec476e3be..6ebbc32728 100644 --- a/tests/test_registration_process_identity.py +++ b/tests/test_registration_process_identity.py @@ -28,7 +28,7 @@ def test_bridge_releases_during_maintenance_without_allowing_a_new_owner(tmp_pat gate = comms.owners.maintenance process = ProcessIdentity.capture(os.getpid()) with registry.external_process('bridge', process): - hold = gate.begin(MaintenanceOperator('bridge-release')) + hold = gate.begin(MaintenanceOperator.current()) for phase in phases: hold = gate.advance(hold, phase) with pytest.raises(RelationViolationError, match='Maintenance'): diff --git a/tests/test_stall_watcher_strike.py b/tests/test_stall_watcher_strike.py index 5fdf42d87d..fe2747a0ed 100644 --- a/tests/test_stall_watcher_strike.py +++ b/tests/test_stall_watcher_strike.py @@ -176,7 +176,7 @@ async def test_three_connector_passes_allow_real_store_maintenance_between_passe from agent_comms.maintenance_barrier import MaintenanceBarrier, MaintenanceOperator from agent_comms.store_files import _store_lock barrier = MaintenanceBarrier(Path(sys.argv[1]) / 'registry.json') -held = barrier.begin(MaintenanceOperator('P717 copied-root proof')) +held = barrier.begin(MaintenanceOperator.current()) try: with _store_lock(barrier.wire_path), _store_lock(barrier.registry_path): directory = os.open(active_route_path().parent, os.O_RDONLY | os.O_DIRECTORY) diff --git a/tests/test_thread_identity.py b/tests/test_thread_identity.py index dda305b003..4426095827 100644 --- a/tests/test_thread_identity.py +++ b/tests/test_thread_identity.py @@ -13,7 +13,7 @@ from agent_comms.coordinator import Coordination from agent_comms.errors import RelationViolationError from agent_comms.field_codec import FieldCodec -from agent_comms.maintenance_barrier import MaintenanceBarrier, MaintenanceOperatorField +from agent_comms.maintenance_barrier import MaintenanceBarrier, MaintenanceOperator from agent_comms.messages import InfoKind, Message from agent_comms.read_basis import Conversation from agent_comms.read_ledger import ReadDocument @@ -304,7 +304,7 @@ def test_each_thread_role_member_owns_its_role_behaviour( barrier = MaintenanceBarrier(tmp_path / "wire" / "registry.json") role.require_maintenance_open(barrier, True) - barrier.begin(FieldCodec.decode(MaintenanceOperatorField, "operator")) + barrier.begin(MaintenanceOperator.current()) role.require_maintenance_open(barrier, False) if executes: with pytest.raises(RelationViolationError, match="under maintenance"): diff --git a/tests/verify_connector_maintenance.py b/tests/verify_connector_maintenance.py index 71a36975a0..a3b853124a 100644 --- a/tests/verify_connector_maintenance.py +++ b/tests/verify_connector_maintenance.py @@ -66,7 +66,7 @@ def prove(scratch: Path) -> None: try: wait_started(service, server_log) barrier = MaintenanceBarrier(copied / "registry.json") - held = barrier.begin(MaintenanceOperator("connector-maintenance-proof")) + held = barrier.begin(MaintenanceOperator.current()) held = barrier.advance(held, PausedPhase) held = barrier.advance(held, InstallingPhase) with store.granted("private maintenance proof") as token: From c2b8cb854c6f79f01aea6d0b0f02c654adfade20 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 21:36:30 -0400 Subject: [PATCH 10/66] P281: publish recovery proof and wire installed harness refresh --- src/agent_comms/harness_install.py | 6 +- tests/test_maintenance_holder.py | 94 +++++---- tests/verify_maintenance_holder.py | 206 ++++++++++++++++++++ tools/cutover/carry.py | 3 +- tools/cutover/carry_maintenance_identity.py | 9 +- tools/publish_runtime.py | 17 +- tools/upgrade_bus.py | 7 + 7 files changed, 293 insertions(+), 49 deletions(-) create mode 100644 tests/verify_maintenance_holder.py diff --git a/src/agent_comms/harness_install.py b/src/agent_comms/harness_install.py index 98bd666c24..04d49d69ac 100644 --- a/src/agent_comms/harness_install.py +++ b/src/agent_comms/harness_install.py @@ -101,9 +101,11 @@ def install(self, comms: Comms, commander: str, project: Path) -> HarnessInstall @classmethod def published_links(cls, home: Path) -> dict[Path, Path]: - """Installed links of every harness, which a runtime publication moves to the new runtime.""" + """Refresh present harnesses and retain links of previously installed harnesses.""" + available = {type(harness) for harness in cls.present(home)} return {link: target for member in cls.members_with(cls) - for link, target in member(home).links().items() if link.is_symlink()} + for link, target in member(home).links().items() + if member in available or link.is_symlink()} @dataclass(frozen=True) diff --git a/tests/test_maintenance_holder.py b/tests/test_maintenance_holder.py index 884c2d82c1..4d944cfdf8 100644 --- a/tests/test_maintenance_holder.py +++ b/tests/test_maintenance_holder.py @@ -1,7 +1,9 @@ """Exact, real process death releases maintenance without a timeout or PID table fake.""" + from __future__ import annotations import asyncio +import json import multiprocessing as mp import os from contextlib import contextmanager @@ -11,24 +13,32 @@ from agent_comms.field_codec import FieldCodec, MalformedRecord from agent_comms.maintenance_barrier import ( - DeadHolderReleasedPhase, DrainingPhase, InstallingPhase, MaintenanceBarrier, - MaintenanceHeldError, MaintenanceOperator, MaintenancePhase, MaintenanceState, PausedPhase, ReadyPhase, + DeadHolderReleasedPhase, + DrainingPhase, + InstallingPhase, + MaintenanceBarrier, + MaintenanceHeldError, + MaintenanceOperator, + MaintenancePhase, + MaintenanceState, + PausedPhase, + ReadyPhase, ) from agent_comms.store_files import ExclusiveLock, SharedLock, _async_store_lock, _store_lock -from agent_comms.todos import GoalRef, RecordFirstCallTodo, TodoStore +from agent_comms.todos import GoalRef, ObservedFirstCallEvent, RecordFirstCallTodo, TodoStore from agent_comms.recorded_time import RecordedEventTime from cutover.carry_maintenance_identity import MaintenanceIdentity from test_todos import order, thread def _hold(root: Path, phase: type[MaintenancePhase], ready, finish) -> None: - barrier = MaintenanceBarrier(root / 'registry.json') + barrier = MaintenanceBarrier(root / "registry.json") held = barrier.begin(MaintenanceOperator.current()) for successor in (PausedPhase, InstallingPhase): if held.phase is phase: break held = barrier.advance(held, successor) - ready.send(FieldCodec.dumps(held)) + ready.send(json.dumps(FieldCodec.encode(held))) ready.close() finish.wait(20) @@ -41,7 +51,7 @@ def real_holder(root: Path, phase: type[MaintenancePhase] = DrainingPhase): child.start() send.close() try: - assert receive.poll(10), 'Real holder did not publish its hold' + assert receive.poll(10), "Real holder did not publish its hold" held = FieldCodec.decode(MaintenanceState, FieldCodec.parse(receive.recv())) assert held.operator.pid == child.pid and held.operator.alive() yield child, held @@ -57,22 +67,25 @@ def real_holder(root: Path, phase: type[MaintenancePhase] = DrainingPhase): def test_operator_uses_process_identity_codec() -> None: operator = MaintenanceOperator.current() assert operator.pid == os.getpid() and operator.start_time > 0 - assert FieldCodec.encode(operator) == {'pid': operator.pid, 'start_time': operator.start_time} + assert FieldCodec.encode(operator) == {"pid": operator.pid, "start_time": operator.start_time} assert FieldCodec.decode(MaintenanceOperator, FieldCodec.encode(operator)) == operator with pytest.raises(MalformedRecord): - FieldCodec.decode(MaintenanceOperator, 'upgrade-bus') + FieldCodec.decode(MaintenanceOperator, "upgrade-bus") -@pytest.mark.parametrize('phase', [DrainingPhase, PausedPhase, InstallingPhase]) -@pytest.mark.parametrize('file,mode', [('wire', SharedLock()), ('registry.json', ExclusiveLock())]) +@pytest.mark.parametrize("phase", [DrainingPhase, PausedPhase, InstallingPhase]) +@pytest.mark.parametrize("file,mode", [("wire", SharedLock()), ("registry.json", ExclusiveLock())]) def test_dead_holder_refuses_detecting_input_then_opens(tmp_path, phase, file, mode): - root = tmp_path / 'bus' - barrier = MaintenanceBarrier(root / 'registry.json') + root = tmp_path / "bus" + barrier = MaintenanceBarrier(root / "registry.json") with real_holder(root, phase) as (child, held): child.kill() child.join(5) with _store_lock(root / file, mode=mode): - with pytest.raises(MaintenanceHeldError, match=f'pid={held.operator.pid} start_time={held.operator.start_time}'): + with pytest.raises( + MaintenanceHeldError, + match=f"pid={held.operator.pid} start_time={held.operator.start_time}", + ): barrier.assert_open_unlocked() released = barrier.read() assert released.phase is DeadHolderReleasedPhase @@ -88,21 +101,21 @@ def test_dead_holder_refuses_detecting_input_then_opens(tmp_path, phase, file, m async def test_async_recovery_borrows_registry_without_blocking_event_loop(tmp_path: Path): - root = tmp_path / 'bus' - barrier = MaintenanceBarrier(root / 'registry.json') + root = tmp_path / "bus" + barrier = MaintenanceBarrier(root / "registry.json") with real_holder(root) as (child, _held): child.kill() child.join(5) async def incoming(): async with barrier.ingress_async(): - pytest.fail('The detecting input must be refused') + pytest.fail("The detecting input must be refused") - async with _async_store_lock(root / 'registry.json'): + async with _async_store_lock(root / "registry.json"): attempt = asyncio.create_task(incoming()) - await asyncio.sleep(.025) + await asyncio.sleep(0.025) assert not attempt.done() - with pytest.raises(MaintenanceHeldError, match='released dead holder'): + with pytest.raises(MaintenanceHeldError, match="released dead holder"): await asyncio.wait_for(attempt, 5) async with barrier.ingress_async() as status: status.require_open() @@ -110,29 +123,36 @@ async def incoming(): def test_commander_first_call_write_is_admitted_before_sqlite_mutation(tmp_path: Path): store = TodoStore(tmp_path) - lead = thread('lead', tmp_path, 1.) + lead = thread("lead", tmp_path, 1.0) original = store.create( - 'P1', 'org/repo', 'Fix maintenance', creator=lead, owner=lead, - generation='one', goal=GoalRef('lead', 1., 'request'), - deadline=RecordedEventTime(1000), order=order(tmp_path), + "P1", + "org/repo", + "Fix maintenance", + creator=lead, + owner=lead, + generation="one", + goal=GoalRef("lead", 1.0, "request"), + deadline=RecordedEventTime(1000), + order=order(tmp_path), ) - barrier = MaintenanceBarrier(tmp_path / 'registry.json') + barrier = MaintenanceBarrier(tmp_path / "registry.json") held = barrier.begin(MaintenanceOperator.current()) before = store.path.read_bytes() + notification = RecordFirstCallTodo(original.revision, lead.incarnation, ObservedFirstCallEvent) with pytest.raises(MaintenanceHeldError): - store.apply('P1', RecordFirstCallTodo(original.revision, lead.incarnation, 'observed')) - assert store.path.read_bytes() == before and store.get('P1') == original + store.apply("P1", notification) + assert store.path.read_bytes() == before and store.get("P1") == original barrier.clear(held) - assert store.apply('P1', RecordFirstCallTodo(original.revision, lead.incarnation, 'observed')).revision == original.revision + 1 + assert store.apply("P1", notification).revision == original.revision + 1 def test_legacy_ready_has_one_registered_conversion(tmp_path: Path): - barrier = MaintenanceBarrier(tmp_path / 'registry.json') + barrier = MaintenanceBarrier(tmp_path / "registry.json") tmp_path.mkdir(exist_ok=True) - marker = {'version': 1, 'root': str(tmp_path.resolve()), 'generation': 4} - state = {**marker, 'operator': 'upgrade-bus', 'nonce': 'a' * 32, 'phase': 'ready'} - barrier.marker_path.write_text(FieldCodec.dumps(marker)) - barrier.state_path.write_text(FieldCodec.dumps(state)) + marker = {"version": 1, "root": str(tmp_path.resolve()), "generation": 4} + state = {**marker, "operator": "upgrade-bus", "nonce": "a" * 32, "phase": "ready"} + barrier.marker_path.write_text(json.dumps(marker)) + barrier.state_path.write_text(json.dumps(state)) assert MaintenanceIdentity.carry(tmp_path, apply=False).changed == 1 assert MaintenanceIdentity.carry(tmp_path, apply=True).changed == 1 assert barrier.read().phase is ReadyPhase @@ -141,11 +161,11 @@ def test_legacy_ready_has_one_registered_conversion(tmp_path: Path): def test_legacy_active_name_is_not_fabricated_into_a_process(tmp_path: Path): - barrier = MaintenanceBarrier(tmp_path / 'registry.json') - marker = {'version': 1, 'root': str(tmp_path.resolve()), 'generation': 4} - state = {**marker, 'operator': 'upgrade-bus', 'nonce': 'a' * 32, 'phase': 'draining'} - barrier.marker_path.write_text(FieldCodec.dumps(marker)) - barrier.state_path.write_text(FieldCodec.dumps(state)) + barrier = MaintenanceBarrier(tmp_path / "registry.json") + marker = {"version": 1, "root": str(tmp_path.resolve()), "generation": 4} + state = {**marker, "operator": "upgrade-bus", "nonce": "a" * 32, "phase": "draining"} + barrier.marker_path.write_text(json.dumps(marker)) + barrier.state_path.write_text(json.dumps(state)) before = barrier.state_path.read_bytes() with pytest.raises(MalformedRecord): MaintenanceIdentity.carry(tmp_path, apply=True) diff --git a/tests/verify_maintenance_holder.py b/tests/verify_maintenance_holder.py new file mode 100644 index 0000000000..fa6c0fdbcf --- /dev/null +++ b/tests/verify_maintenance_holder.py @@ -0,0 +1,206 @@ +"""Real detached live-phase/SIGKILL proof; reuse installed dependencies, never stage/build. + +This exercises upgrade_bus.live, its detached launcher and the real Core CLI on +an isolated copied bus. Full upgrade/rehearsal preparation belongs to the merger. +""" + +from __future__ import annotations + +import hashlib +import os +import signal +import subprocess +import sys +import time +from collections.abc import Callable +from dataclasses import dataclass +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path[:0] = [str(ROOT / "src"), str(ROOT / "tools")] + +from agent_comms.child_process import Platform, ProcessIdentity +from agent_comms.comms import Comms +from agent_comms.field_codec import FieldCodec +from agent_comms.maintenance_barrier import DeadHolderReleasedPhase, MaintenanceState +from upgrade_bus import Revisions, Stack, copy_bus, detached_live, live + +SCRATCH = Path("/home/ts/.cache/agent-scratch/pi-install-gate/p281-live-proof") +BUS = SCRATCH / "copy" + + +@dataclass(frozen=True) +class ExistingStack(Stack): + @property + def python(self) -> Path: + return Path(sys.executable) + + +STACK = ExistingStack(ROOT, SCRATCH / "home") + + +def command(operation: Callable[[], None]) -> tuple[str, ...]: + return ( + sys.executable, + "-c", + f"from verify_maintenance_holder import {operation.__name__}; {operation.__name__}()", + ) + + +def environment() -> dict[str, str]: + return STACK.environment(ROOT / "tests", ROOT / "tools") + + +def busy_owner() -> None: + comms = Comms(BUS) + owner = comms.registry.new_thread( + "busy", frozenset(), str(ROOT), process_identity=ProcessIdentity.capture(os.getpid()) + ) + comms.registry.declare(owner) + assert comms.agents.begin_turn("busy", "p281-real-busy-turn") + signal.signal(signal.SIGTERM, lambda *_: None) + print("busy", flush=True) + sys.stdin.read() + + +def live_worker() -> None: + # Exactly the live installation phase, not a rehearse-only run. Empty pending + # steps need no new revision worktree. The real busy owner holds its drain. + head = subprocess.check_output(('git', 'rev-parse', 'HEAD'), cwd=ROOT, text=True).strip() + live(ROOT, BUS, head, head, (), STACK, STACK, + Revisions(ROOT, SCRATCH / 'unused-revisions'), 'pi-install-gate') + + +def caller() -> None: + raise SystemExit(detached_live(command(live_worker), BUS)) + + +def cli(*arguments: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ( + sys.executable, + "-c", + "from agent_comms.cli import main; raise SystemExit(main())", + "--root", + str(BUS), + *arguments, + ), + cwd=ROOT, + env=environment(), + capture_output=True, + text=True, + timeout=30, + ) + + +def digest(root: Path) -> str: + value = hashlib.sha256() + for path in sorted(root.rglob("*")): + if path.is_file(): + value.update(str(path.relative_to(root)).encode()) + value.update(path.read_bytes()) + return value.hexdigest() + + +def probe() -> None: + SCRATCH.mkdir(mode=0o700) + STACK.home.mkdir(mode=0o700) + original = SCRATCH / "original" + source = Comms(original) + for name in ("sender", "receiver"): + source.registry.declare(source.registry.new_thread(name, frozenset(), str(ROOT))) + before = digest(original) + copy_bus(original, BUS) + busy = subprocess.Popen( + command(busy_owner), + env=environment(), + cwd=ROOT, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + start_new_session=True, + ) + observer = None + members: tuple[ProcessIdentity, ...] = () + try: + assert busy.stdout is not None and busy.stdout.readline().strip() == "busy" + observer = subprocess.Popen( + command(caller), + env=environment(), + cwd=ROOT, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + start_new_session=True, + ) + # Use the barrier's declared path, not a second hold file. + from agent_comms.maintenance_barrier import MaintenanceBarrier + + state_path = MaintenanceBarrier(BUS / "registry.json").state_path + deadline = time.monotonic() + 30 + while not state_path.exists(): + assert time.monotonic() < deadline, "Live installer did not enter drain" + assert observer.poll() is None, "Caller exited before entering drain" + time.sleep(0.01) + held = FieldCodec.decode(MaintenanceState, FieldCodec.parse(state_path.read_text())) + assert held.phase.declared_name == "draining" + worker = ProcessIdentity.capture(held.operator.pid) + assert worker == held.operator and worker.pid != observer.pid + assert os.getsid(worker.pid) == worker.pid != os.getsid(observer.pid) + # Kill the calling session first: its separately recorded worker survives. + os.killpg(observer.pid, signal.SIGKILL) + observer.wait(timeout=5) + assert worker.alive(), "Calling-session death killed the detached installer" + print( + f"caller killed; detached holder alive pid={worker.pid} start_time={worker.start_time}" + ) + # Freeze the actual installer job at its recorded drain, then SIGKILL the + # job. No stopped CLI descendant can retain a lock or consume recovery. + os.killpg(worker.pid, signal.SIGSTOP) + members = Platform.current().group_members(worker) + stopped = FieldCodec.decode(MaintenanceState, FieldCodec.parse(state_path.read_text())) + assert stopped.phase.declared_name == "draining" + os.killpg(worker.pid, signal.SIGKILL) + while worker.alive(): + assert time.monotonic() < deadline, "SIGKILL did not retire holder" + time.sleep(0.01) + first = cli("send", "--from", "sender", "--to", "receiver", "--body", "p281-refused-input") + print(f"first send rc={first.returncode}: {first.stdout.strip()}") + assert first.returncode != 0 and "released dead holder" in first.stdout + release_bytes = state_path.read_bytes() + released = FieldCodec.decode(MaintenanceState, FieldCodec.parse(release_bytes.decode())) + assert released.phase is DeadHolderReleasedPhase and released.operator == held.operator + second = cli( + "send", "--from", "sender", "--to", "receiver", "--body", "p281-accepted-input" + ) + print(f"second send rc={second.returncode}: {second.stdout.strip()}") + assert second.returncode == 0, second.stderr + history = cli("history", "--limit", "10") + assert history.returncode == 0 and "p281-accepted-input" in history.stdout + assert "p281-refused-input" not in history.stdout + assert state_path.read_bytes() == release_bytes + assert digest(original) == before + print("release record: " + release_bytes.decode().strip()) + print( + "original unchanged; accepted message retained; rehearsal/environment preparation not run" + ) + finally: + for identity in members: + if identity.alive(): + Platform.current().send(identity, signal.SIGKILL) + if observer is not None and observer.poll() is None: + observer.kill() + observer.wait(timeout=5) + if busy.poll() is None: + busy.kill() + busy.wait(timeout=5) + if busy.stdin is not None: + busy.stdin.close() + if busy.stdout is not None: + busy.stdout.close() + if busy.stderr is not None: + busy.stderr.close() + + +if __name__ == "__main__": + probe() diff --git a/tools/cutover/carry.py b/tools/cutover/carry.py index 20fdcda02f..a95a0f8853 100644 --- a/tools/cutover/carry.py +++ b/tools/cutover/carry.py @@ -541,8 +541,7 @@ def publish(self, publication, comms, intent, hold, resources, timeout: float) - # No data or route changes: existing readers can keep their shared lock. with comms.route.hold_client_route(): print('Acquired shared route lock for unchanged-format switch', flush=True) - if not publication.published(): - publication.publish_links() + publication.publish_links() publication.release_if_safe(self, comms.owners.maintenance, hold) # Existing exact-incarnation queue owns idle admission and settlement. for original in OwnerRestartSelection.live(intent.owners, comms.registry.snapshot()): diff --git a/tools/cutover/carry_maintenance_identity.py b/tools/cutover/carry_maintenance_identity.py index e5150c7382..eba6d4284b 100644 --- a/tools/cutover/carry_maintenance_identity.py +++ b/tools/cutover/carry_maintenance_identity.py @@ -4,6 +4,7 @@ abandoned; this one-time converter refuses it rather than fabricating an owner. There is no legacy reader in the runtime barrier. """ + from __future__ import annotations from dataclasses import dataclass @@ -41,8 +42,12 @@ def carried(self, barrier: MaintenanceBarrier, *, apply: bool) -> Carried: if marker != expected or self.root != str(barrier.registry_path.parent.resolve()): raise RelationViolationError("Legacy maintenance witnesses do not identify this root") converted = MaintenanceState( - version=2, root=self.root, generation=self.generation + 1, - operator=MaintenanceOperator.current(), nonce=Id128Text.mint(), phase=ReadyPhase, + version=2, + root=self.root, + generation=self.generation + 1, + operator=MaintenanceOperator.current(), + nonce=Id128Text.mint(), + phase=ReadyPhase, ) if apply: barrier._write_unlocked(converted) diff --git a/tools/publish_runtime.py b/tools/publish_runtime.py index c024104744..317a9a3d22 100644 --- a/tools/publish_runtime.py +++ b/tools/publish_runtime.py @@ -195,13 +195,14 @@ def selected_route(self, root: Path): os.close(descriptor) def publish_links(self) -> None: - """Move declarations and the pointer, without killing unrelated processes.""" + """Refresh the declared installation, including an already-current runtime.""" current = StoppedOwnerInstallation.current_pointer() - if self.published(): - raise ValueError(f'Publication would retire the runtime it publishes: {self.runtime}') + selected = self.runtime.resolve(strict=True) for link, target in self.links.items(): replace_link(link, current / target.relative_to(self.runtime)) - replace_link(current, self.runtime) + replace_link(current, selected) + for harness in Harness.present(Path.home()): + harness.configure() @dataclass(frozen=True) @@ -263,8 +264,7 @@ def complete(self, wire_descriptor: int, route_descriptor: int) -> tuple[OwnerRe if StatusCliCommand().run(comms, output): raise RuntimeError('Converted bus status failed: ' + output.getvalue()) print('Converted bus status: ' + output.getvalue().strip(), flush=True) - if not publication.published(): - publication.publish_links() + publication.publish_links() stopped = StoppedOwnerBatch.accept(comms.owners, wire_descriptor, self.handoff) comms.owners.maintenance.clear_stopped(self.maintenance_hold, wire_descriptor) fcntl.flock(route_descriptor, fcntl.LOCK_UN) @@ -275,6 +275,8 @@ def main() -> None: parser = argparse.ArgumentParser(description=__doc__) actions = parser.add_mutually_exclusive_group(required=True) actions.add_argument('--check', action='store_true') + actions.add_argument('--refresh', action='store_true', + help='Refresh this installed runtime\'s declared launchers and harness config') actions.add_argument('--complete-stopped', type=int, nargs=2, metavar=('WIRE_FD', 'ROUTE_FD')) options = parser.parse_args() if options.complete_stopped is not None: @@ -284,6 +286,9 @@ def main() -> None: print(json.dumps(FieldCodec.encode(result))) return publication = RuntimePublication.installed() + if options.refresh: + publication.publish_links() + return route = read_active_route() if route is None: raise ValueError('Runtime publication requires the installed route') diff --git a/tools/upgrade_bus.py b/tools/upgrade_bus.py index 884b2b3c23..151de765f0 100644 --- a/tools/upgrade_bus.py +++ b/tools/upgrade_bus.py @@ -122,6 +122,11 @@ def imports(self) -> None: def prepare_native(self) -> None: run(self.checkout / "stack/bin/prepare-pi-native", cwd=self.checkout) + def publish(self) -> None: + """The selected interpreter owns console metadata and harness declarations.""" + run(self.python, "-m", "tools.publish_runtime", "--refresh", cwd=self.checkout, + env=self.environment(self.checkout)) + def environment(self, *paths: Path) -> dict: """``paths`` first; then this checkout's ``src``. ``home`` holds the route its clients read.""" return {**os.environ, "HOME": str(self.home), @@ -358,6 +363,7 @@ def rehearsal(self) -> dict: return {"unchanged": self.target} def live(self) -> dict: + self.installed.publish() PlannedInstallDeadline.after(self.installer, time.time()).plan(self.root) return {"unchanged": self.target} @@ -391,6 +397,7 @@ def live(checkout: Path, root: Path, current: str, target: str, pending: tuple[S installed.relock() installed.prepare_native() passed = carry_and_read(root, backup.files(), pending, staged, installed, revisions, []) + installed.publish() maintenance.clear(held) services = HostService.restart_running(installed.python) PlannedInstallDeadline.after(installer, time.time()).plan(root) From fdb5e9e9696cb8d262086964ed172d66a02b8790 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 21:47:15 -0400 Subject: [PATCH 11/66] Verify declaration-driven hook publication with installed programs --- tests/test_maintenance_holder.py | 9 +- tests/test_runtime_publication.py | 16 ++-- tests/verify_install_publication.py | 122 ++++++++++++++++++++++++++++ tests/verify_maintenance_holder.py | 2 +- 4 files changed, 136 insertions(+), 13 deletions(-) create mode 100644 tests/verify_install_publication.py diff --git a/tests/test_maintenance_holder.py b/tests/test_maintenance_holder.py index 4d944cfdf8..2d2d06d416 100644 --- a/tests/test_maintenance_holder.py +++ b/tests/test_maintenance_holder.py @@ -6,6 +6,7 @@ import json import multiprocessing as mp import os +import signal from contextlib import contextmanager from pathlib import Path @@ -31,7 +32,7 @@ from test_todos import order, thread -def _hold(root: Path, phase: type[MaintenancePhase], ready, finish) -> None: +def _hold(root: Path, phase: type[MaintenancePhase], ready) -> None: barrier = MaintenanceBarrier(root / "registry.json") held = barrier.begin(MaintenanceOperator.current()) for successor in (PausedPhase, InstallingPhase): @@ -40,14 +41,13 @@ def _hold(root: Path, phase: type[MaintenancePhase], ready, finish) -> None: held = barrier.advance(held, successor) ready.send(json.dumps(FieldCodec.encode(held))) ready.close() - finish.wait(20) + signal.pause() @contextmanager def real_holder(root: Path, phase: type[MaintenancePhase] = DrainingPhase): receive, send = mp.Pipe(duplex=False) - finish = mp.Event() - child = mp.Process(target=_hold, args=(root, phase, send, finish)) + child = mp.Process(target=_hold, args=(root, phase, send), daemon=True) child.start() send.close() try: @@ -56,7 +56,6 @@ def real_holder(root: Path, phase: type[MaintenancePhase] = DrainingPhase): assert held.operator.pid == child.pid and held.operator.alive() yield child, held finally: - finish.set() if child.is_alive(): child.kill() child.join(5) diff --git a/tests/test_runtime_publication.py b/tests/test_runtime_publication.py index 0d95d50910..a4b43fa0b9 100644 --- a/tests/test_runtime_publication.py +++ b/tests/test_runtime_publication.py @@ -38,7 +38,7 @@ def test_cli_failure_leaves_links_untouched(tmp_path, monkeypatch, command): def test_pending_carries_refuse_before_cli_or_maintenance(tmp_path, monkeypatch): - monkeypatch.setenv('HOME', str(tmp_path)) + monkeypatch.setenv("HOME", str(tmp_path)) current = StoppedOwnerInstallation.current_pointer() current.parent.mkdir(parents=True) current.symlink_to(Path(sys.executable).parent.resolve().parent) @@ -50,8 +50,7 @@ def test_pending_carries_refuse_before_cli_or_maintenance(tmp_path, monkeypatch) assert not (root / "runtime-publication.json").exists() -def test_pointer_publication_does_not_kill_unselected_runtime_processes( - tmp_path, monkeypatch): +def test_pointer_publication_does_not_kill_unselected_runtime_processes(tmp_path, monkeypatch): monkeypatch.setenv("HOME", str(tmp_path)) runtimes = {name: tmp_path / name for name in ("previous-runtime", "runtime")} for runtime in runtimes.values(): @@ -60,14 +59,17 @@ def test_pointer_publication_does_not_kill_unselected_runtime_processes( current = StoppedOwnerInstallation.current_pointer() current.parent.mkdir(parents=True) current.symlink_to(runtimes["previous-runtime"]) - retired, kept = (subprocess.Popen([runtime / "bin/python", "-c", "import time; time.sleep(60)"]) - for runtime in runtimes.values()) + retired, kept = ( + subprocess.Popen([runtime / "bin/python", "-c", "import time; time.sleep(60)"]) + for runtime in runtimes.values() + ) try: RuntimePublication(runtimes["runtime"].resolve(), {}).publish_links() assert retired.poll() is None assert kept.poll() is None and current.readlink() == runtimes["runtime"].resolve() - with pytest.raises(ValueError, match="would retire the runtime it publishes"): - RuntimePublication(runtimes["runtime"].resolve(), {}).publish_links() + RuntimePublication(runtimes["runtime"].resolve(), {}).publish_links() + assert retired.poll() is None and kept.poll() is None + assert current.resolve() == runtimes["runtime"].resolve() finally: for child in (retired, kept): child.kill() diff --git a/tests/verify_install_publication.py b/tests/verify_install_publication.py new file mode 100644 index 0000000000..0afa4f9fe8 --- /dev/null +++ b/tests/verify_install_publication.py @@ -0,0 +1,122 @@ +"""Exercise candidate Stack publication with installed packages and a private HOME. + +No runtime installation, environment creation, real harness configuration, or +service restart occurs. The actual installed SessionStart executable is invoked +through the privately published PATH and its written hook declaration. +""" + +from __future__ import annotations + +import json +import os +import shlex +import shutil +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +MAIN = Path("/home/ts/code/projects/agent-comms") +sys.path[:0] = [str(MAIN / "src"), str(ROOT), str(ROOT / "tools")] + +from agent_comms.commander.config import CommanderConfig +from agent_comms.field_codec import FieldCodec +from install_gate import ExternalRecord +from agent_comms_hooks import ClaudeHookProgram +from agent_comms_hooks.skill import SkillText +from tools.upgrade_bus import Stack + + +@dataclass(frozen=True) +class InstalledCoreStack(Stack): + @property + def python(self) -> Path: + return MAIN / "stack/.venv/bin/python" + + def environment(self, *paths: Path) -> dict[str, str]: + return super().environment(MAIN / "src", *paths) + + +@dataclass(frozen=True) +class WrittenCommand(ExternalRecord): + command: str + + +@dataclass(frozen=True) +class WrittenRule(ExternalRecord): + hooks: tuple[WrittenCommand, ...] + + +@dataclass(frozen=True) +class WrittenSettings(ExternalRecord): + hooks: dict[str, tuple[WrittenRule, ...]] + retained_user_setting: str + + +@dataclass(frozen=True) +class HookContext: + hookEventName: str + additionalContext: str + + +@dataclass(frozen=True) +class HookResult: + hookSpecificOutput: HookContext + + +def main() -> None: + home = Path("/home/ts/.cache/agent-scratch/pi-install-gate/p275-publication-proof") + home.mkdir(mode=0o700) + settings = home / ".claude/settings.json" + settings.parent.mkdir() + settings.write_text(json.dumps({"retained_user_setting": "keep", "hooks": {}})) + stack = InstalledCoreStack(ROOT, home) + stack.publish() + first = settings.read_bytes() + stack.publish() + assert settings.read_bytes() == first, "Repeated refresh duplicated or changed configuration" + written = FieldCodec.decode(WrittenSettings, FieldCodec.parse(first)) + assert written.retained_user_setting == "keep" + environment = stack.environment(ROOT) + environment["PATH"] = str(home / ".local/bin") + os.pathsep + os.environ["PATH"] + for program in ClaudeHookProgram.members(): + name = shlex.split(program.command())[0] + selected = shutil.which(name, path=environment["PATH"]) + assert selected == str(home / ".local/bin" / name), name + for event in program.events: + commands = tuple( + hook.command for entry in written.hooks.get(event, ()) for hook in entry.hooks + ) + assert commands.count(program.command()) == 1, (event, program.command(), commands) + print(f"published/configured: {program.command()} {program.events}") + invoked = subprocess.run( + shlex.split(SkillText.command()), + input=json.dumps( + {"session_id": "private-publication-proof", "hook_event_name": SkillText.events[0]} + ), + env=environment, + capture_output=True, + text=True, + timeout=30, + ) + assert invoked.returncode == 0, invoked.stderr + result = FieldCodec.decode(HookResult, FieldCodec.parse(invoked.stdout)) + assert result.hookSpecificOutput.hookEventName == SkillText.events[0] + sources = (CommanderConfig.packaged_skill(), *CommanderConfig.packaged_rules_files()) + for source in sources: + assert source.name in result.hookSpecificOutput.additionalContext + assert ( + source.read_text(encoding="utf-8", newline="") + in result.hookSpecificOutput.additionalContext + ) + print( + f"SessionStart rc=0; all {len(sources)} packaged files retained; refresh idempotent; user setting preserved" + ) + print( + "Only the private HOME was changed; production publication and service restart were not run" + ) + + +if __name__ == "__main__": + main() diff --git a/tests/verify_maintenance_holder.py b/tests/verify_maintenance_holder.py index fa6c0fdbcf..45e61747e5 100644 --- a/tests/verify_maintenance_holder.py +++ b/tests/verify_maintenance_holder.py @@ -25,7 +25,7 @@ from agent_comms.maintenance_barrier import DeadHolderReleasedPhase, MaintenanceState from upgrade_bus import Revisions, Stack, copy_bus, detached_live, live -SCRATCH = Path("/home/ts/.cache/agent-scratch/pi-install-gate/p281-live-proof") +SCRATCH = Path("/home/ts/.cache/agent-scratch/pi-install-gate/p281-live-proof-v2") BUS = SCRATCH / "copy" From 554988af584fe50cfd92bc892c0249a4e325aba5 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 21:51:33 -0400 Subject: [PATCH 12/66] Keep publication process fixture independent of host harnesses --- docs/maintenance-holder.md | 31 ++++++++++++++++++++++++------ tests/test_runtime_publication.py | 2 ++ tests/verify_maintenance_holder.py | 2 +- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/docs/maintenance-holder.md b/docs/maintenance-holder.md index 51f3ef0919..7a0139ee92 100644 --- a/docs/maintenance-holder.md +++ b/docs/maintenance-holder.md @@ -28,12 +28,31 @@ The runtime reads only version 2. - [x] Implementation checkpoint pushed on `fix/p281-maintenance-holder`. - [x] Production AST inventory: 381 Core modules and 39 tools; no parse omissions. - [x] Migrate all discovered producer and test callers off string operators. -- [ ] Focused checks, tools ratchet and src ratchet against `origin/main`. -- [ ] Copied-bus live-phase proof using existing installed Python; no build, - environment creation or `uv sync` is authorized for this proof. -- [ ] Prove calling-session death leaves the worker alive, then SIGKILL the - worker mid-drain; record first refusal, second acceptance and release JSON. -- [ ] Open P281's PR through the commander's canonical PR command. +- [x] Eleven focused real-process/ingress/todo/carry checks passed. +- [ ] Tools ratchet and src ratchet against `origin/main`. +- [x] Copied-bus live-phase proof with existing Python, without a build, + environment creation or `uv sync`. +- [x] Calling-session death left the worker alive; SIGKILL of its job mid-drain + gave a named `held` refusal, then an accepted and retained second message. + The release record retained the dead PID/birth time; original bus unchanged. +- [x] Draft PR #2072 opened through the commander's canonical PR command. + +## Installation wiring follow-up + +Normal and unchanged live installs now use the selected interpreter to refresh +`RuntimePublication`. That existing owner publishes console/skill links and +calls the declared `Harness.configure` implementations; repeated refreshes are +safe. Consumers no longer skip refresh merely because the pointer is current. + +Actual installed programs, with a private HOME and candidate publication code, +verified every declared hook on PATH/configuration, including P275 SessionStart +and P285 UserPromptSubmit. SessionStart exited zero with all 45 packaged files; +repeated refresh preserved user settings without duplicate hooks. Production +configuration and services were not changed. + +This does not yet fence external bridge watches or session hooks across mutable +checkout/schema updates. The reported transitional-reader failure remains +unfixed; publication success is not acceptance of that separate lifecycle gap. The inventory includes declarations, inheritance, imports, writes, decisions and references throughout both production roots. Generated test child programs diff --git a/tests/test_runtime_publication.py b/tests/test_runtime_publication.py index a4b43fa0b9..d1e2a537df 100644 --- a/tests/test_runtime_publication.py +++ b/tests/test_runtime_publication.py @@ -52,6 +52,8 @@ def test_pending_carries_refuse_before_cli_or_maintenance(tmp_path, monkeypatch) def test_pointer_publication_does_not_kill_unselected_runtime_processes(tmp_path, monkeypatch): monkeypatch.setenv("HOME", str(tmp_path)) + # These tiny process-lifetime runtimes contain no packaged harness commands. + monkeypatch.setenv("PATH", str(tmp_path / "no-harnesses")) runtimes = {name: tmp_path / name for name in ("previous-runtime", "runtime")} for runtime in runtimes.values(): (runtime / "bin").mkdir(parents=True) diff --git a/tests/verify_maintenance_holder.py b/tests/verify_maintenance_holder.py index 45e61747e5..f331465574 100644 --- a/tests/verify_maintenance_holder.py +++ b/tests/verify_maintenance_holder.py @@ -25,7 +25,7 @@ from agent_comms.maintenance_barrier import DeadHolderReleasedPhase, MaintenanceState from upgrade_bus import Revisions, Stack, copy_bus, detached_live, live -SCRATCH = Path("/home/ts/.cache/agent-scratch/pi-install-gate/p281-live-proof-v2") +SCRATCH = Path("/home/ts/.cache/agent-scratch/pi-install-gate/p281-live-proof-v3") BUS = SCRATCH / "copy" From 7e2651d92adc795d653cf1b34d96719e970d21b8 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 22:01:03 -0400 Subject: [PATCH 13/66] Use canonical publication module and typed carry schema at the boundary --- src/agent_comms/todos.py | 4 ++- tests/verify_install_publication.py | 2 +- tests/verify_maintenance_holder.py | 2 +- tools/cutover/carry_maintenance_identity.py | 11 +++--- tools/publish_runtime.py | 37 +++++++++++++-------- tools/upgrade_bus.py | 11 +++--- 6 files changed, 42 insertions(+), 25 deletions(-) diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 8c91ebda00..b33aff16b5 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -2539,7 +2539,9 @@ def writing(self) -> Iterator[sqlite3.Connection]: ) from error @contextmanager - def writer_connection(self, *, check_same_thread: bool = True) -> Iterator[sqlite3.Connection]: + def writer_connection( + self, *, check_same_thread: bool = True + ) -> Iterator[sqlite3.Connection]: with MaintenanceBarrier(self.root / "registry.json").allow_input(): with super().writer_connection(check_same_thread=check_same_thread) as connection: yield connection diff --git a/tests/verify_install_publication.py b/tests/verify_install_publication.py index 0afa4f9fe8..867038657d 100644 --- a/tests/verify_install_publication.py +++ b/tests/verify_install_publication.py @@ -66,7 +66,7 @@ class HookResult: def main() -> None: - home = Path("/home/ts/.cache/agent-scratch/pi-install-gate/p275-publication-proof") + home = Path("/home/ts/.cache/agent-scratch/pi-install-gate/p275-publication-proof-final") home.mkdir(mode=0o700) settings = home / ".claude/settings.json" settings.parent.mkdir() diff --git a/tests/verify_maintenance_holder.py b/tests/verify_maintenance_holder.py index f331465574..ca2d538e10 100644 --- a/tests/verify_maintenance_holder.py +++ b/tests/verify_maintenance_holder.py @@ -25,7 +25,7 @@ from agent_comms.maintenance_barrier import DeadHolderReleasedPhase, MaintenanceState from upgrade_bus import Revisions, Stack, copy_bus, detached_live, live -SCRATCH = Path("/home/ts/.cache/agent-scratch/pi-install-gate/p281-live-proof-v3") +SCRATCH = Path("/home/ts/.cache/agent-scratch/pi-install-gate/p281-live-proof-v4") BUS = SCRATCH / "copy" diff --git a/tools/cutover/carry_maintenance_identity.py b/tools/cutover/carry_maintenance_identity.py index eba6d4284b..eae63b2031 100644 --- a/tools/cutover/carry_maintenance_identity.py +++ b/tools/cutover/carry_maintenance_identity.py @@ -12,14 +12,14 @@ from typing import Literal from agent_comms.errors import RelationViolationError -from agent_comms.field_codec import FieldCodec +from agent_comms.field_codec import FieldCodec, Id128Text, compile_decoder from agent_comms.maintenance_barrier import ( MaintenanceBarrier, MaintenanceOperator, MaintenanceState, ReadyPhase, ) -from agent_comms.field_codec import Id128Text + from .carry import Carried, Carry @@ -61,6 +61,9 @@ def carried(self, barrier: MaintenanceBarrier, *, apply: bool) -> Carried: return Carried(0) +type StoredMaintenanceState = IdentifiedState | OldReadyState + + class MaintenanceIdentity(Carry): @classmethod def precedes(cls, later: type[Carry]) -> bool: @@ -81,8 +84,8 @@ def carry(cls, root: Path, *, apply: bool) -> Carried: if barrier.marker_path.exists(): raise RelationViolationError("Maintenance has a marker but no stored holder") return Carried(0) - state = FieldCodec.decode( - IdentifiedState | OldReadyState, FieldCodec.parse(barrier.state_path.read_text()) + state: StoredMaintenanceState = compile_decoder(StoredMaintenanceState).decode( + FieldCodec.parse(barrier.state_path.read_text()) ) return state.carried(barrier, apply=apply) diff --git a/tools/publish_runtime.py b/tools/publish_runtime.py index 317a9a3d22..857475af57 100644 --- a/tools/publish_runtime.py +++ b/tools/publish_runtime.py @@ -6,33 +6,44 @@ from __future__ import annotations import argparse -from contextlib import AbstractContextManager, ExitStack, contextmanager, nullcontext, redirect_stdout -from dataclasses import dataclass, field -from abc import abstractmethod -import hashlib import fcntl -from io import StringIO -from importlib.metadata import distribution +import hashlib import json import os -from pathlib import Path import subprocess import sys +from abc import abstractmethod +from contextlib import ( + AbstractContextManager, + ExitStack, + contextmanager, + nullcontext, + redirect_stdout, +) +from dataclasses import dataclass, field +from importlib.metadata import distribution +from io import StringIO +from pathlib import Path from typing import Annotated -from agent_comms.active_route import read_active_route, active_route_path, acquire_exclusive_route_lock -from agent_comms.comms import wire +from agent_comms.active_route import ( + acquire_exclusive_route_lock, + active_route_path, + read_active_route, +) from agent_comms.cli_commands import StatusCliCommand -from agent_comms.field_codec import FieldCodec, PathText, SubclassUnion, NullMember, RecordMember +from agent_comms.comms import wire +from agent_comms.field_codec import FieldCodec, NullMember, PathText, RecordMember, SubclassUnion from agent_comms.harness_install import Harness, replace_link from agent_comms.launch_runtime import LaunchRuntime from agent_comms.maintenance_barrier import MaintenanceOperator, MaintenanceState -from agent_comms.owner_cutover import StoppedOwnerInstallation, MaintainedOwnerCutover -from agent_comms.owner_lifecycle import OwnerRestartSelection, OwnerRestartResult +from agent_comms.owner_cutover import MaintainedOwnerCutover, StoppedOwnerInstallation +from agent_comms.owner_lifecycle import OwnerRestartResult, OwnerRestartSelection from agent_comms.owner_restart import OwnerRestartHandoff, OwnerRestartRequest, StoppedOwnerBatch from agent_comms.store_files import _atomic_write_text, _store_lock -from .cutover.carry import PendingConversion, PreparedCarries + from .cutover.backup import StoreBackup +from .cutover.carry import PendingConversion, PreparedCarries class PublicationIntent(SubclassUnion, affix=''): diff --git a/tools/upgrade_bus.py b/tools/upgrade_bus.py index 151de765f0..b150c25e36 100644 --- a/tools/upgrade_bus.py +++ b/tools/upgrade_bus.py @@ -42,10 +42,12 @@ # This file's tree is the target revision: the tool and every step it runs import that # tree's src, never the src the checkout being upgraded has installed. -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) +REPO = Path(__file__).resolve().parents[1] +sys.path[:0] = [str(REPO / "src"), str(REPO)] -from cutover.carry import Carried, CarryChain, CarryLedger, Step, TodoColumnCarry -from cutover.backup import Backup, StoreBackup, UpgradeRefused, bus_files, transcripts_held +from tools import publish_runtime +from tools.cutover.carry import Carried, CarryChain, CarryLedger, Step, TodoColumnCarry +from tools.cutover.backup import Backup, StoreBackup, UpgradeRefused, bus_files, transcripts_held import agent_comms.activity_checkpoint # noqa: F401 (declares the activity runtime files) from agent_comms.external_transcript import ExternalTranscript @@ -57,7 +59,6 @@ from agent_comms.store_files import _store_lock from agent_comms.todos import PlannedInstallDeadline -REPO = Path(__file__).resolve().parents[1] CLIENT_IMPORTS = "import toad.app, agent_comms.acp" ROUTE = Path.home() / ".local/state/agent-comms/active-route.json" @@ -124,7 +125,7 @@ def prepare_native(self) -> None: def publish(self) -> None: """The selected interpreter owns console metadata and harness declarations.""" - run(self.python, "-m", "tools.publish_runtime", "--refresh", cwd=self.checkout, + run(self.python, "-m", publish_runtime.__name__, "--refresh", cwd=self.checkout, env=self.environment(self.checkout)) def environment(self, *paths: Path) -> dict: From d02b1fecd16fb286259216628a4d5fb5fdc501ac Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 22:01:45 -0400 Subject: [PATCH 14/66] Preserve standalone installer bootstrap with canonical module execution --- tools/upgrade_bus.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tools/upgrade_bus.py b/tools/upgrade_bus.py index b150c25e36..4fb12b3b43 100644 --- a/tools/upgrade_bus.py +++ b/tools/upgrade_bus.py @@ -42,12 +42,8 @@ # This file's tree is the target revision: the tool and every step it runs import that # tree's src, never the src the checkout being upgraded has installed. -REPO = Path(__file__).resolve().parents[1] -sys.path[:0] = [str(REPO / "src"), str(REPO)] - -from tools import publish_runtime -from tools.cutover.carry import Carried, CarryChain, CarryLedger, Step, TodoColumnCarry -from tools.cutover.backup import Backup, StoreBackup, UpgradeRefused, bus_files, transcripts_held +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) import agent_comms.activity_checkpoint # noqa: F401 (declares the activity runtime files) from agent_comms.external_transcript import ExternalTranscript @@ -58,7 +54,11 @@ from agent_comms.registry_store import RegistryStore from agent_comms.store_files import _store_lock from agent_comms.todos import PlannedInstallDeadline +from tools import publish_runtime +from tools.cutover.backup import Backup, StoreBackup, UpgradeRefused, bus_files, transcripts_held +from tools.cutover.carry import Carried, CarryChain, CarryLedger, Step, TodoColumnCarry +REPO = Path(__file__).resolve().parents[1] CLIENT_IMPORTS = "import toad.app, agent_comms.acp" ROUTE = Path.home() / ".local/state/agent-comms/active-route.json" @@ -125,8 +125,10 @@ def prepare_native(self) -> None: def publish(self) -> None: """The selected interpreter owns console metadata and harness declarations.""" - run(self.python, "-m", publish_runtime.__name__, "--refresh", cwd=self.checkout, - env=self.environment(self.checkout)) + run( + self.python, "-m", publish_runtime.__name__, "--refresh", + cwd=self.checkout, env=self.environment(self.checkout), + ) def environment(self, *paths: Path) -> dict: """``paths`` first; then this checkout's ``src``. ``home`` holds the route its clients read.""" From 11d359fdd63ffed7d9b3d8c48c21fcc233d7abaf Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 22:03:37 -0400 Subject: [PATCH 15/66] Compile the stored maintenance choice as its runtime union --- tools/cutover/carry_maintenance_identity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/cutover/carry_maintenance_identity.py b/tools/cutover/carry_maintenance_identity.py index eae63b2031..0d970b0f51 100644 --- a/tools/cutover/carry_maintenance_identity.py +++ b/tools/cutover/carry_maintenance_identity.py @@ -61,7 +61,7 @@ def carried(self, barrier: MaintenanceBarrier, *, apply: bool) -> Carried: return Carried(0) -type StoredMaintenanceState = IdentifiedState | OldReadyState +StoredMaintenanceState = IdentifiedState | OldReadyState class MaintenanceIdentity(Carry): From 8366bd73ad301bbd7bceae1dd114aacce719479d Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 22:15:03 -0400 Subject: [PATCH 16/66] Use one installer module identity and compose the writer contexts --- src/agent_comms/todos.py | 8 +++++--- tests/test_runtime_publication.py | 2 +- tests/verify_install_publication.py | 6 +++--- tests/verify_maintenance_holder.py | 4 ++-- tools/publish_runtime.py | 11 +++++------ tools/upgrade_bus.py | 9 ++++----- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index b33aff16b5..41d3f7a2ed 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -2542,9 +2542,11 @@ def writing(self) -> Iterator[sqlite3.Connection]: def writer_connection( self, *, check_same_thread: bool = True ) -> Iterator[sqlite3.Connection]: - with MaintenanceBarrier(self.root / "registry.json").allow_input(): - with super().writer_connection(check_same_thread=check_same_thread) as connection: - yield connection + with ( + MaintenanceBarrier(self.root / "registry.json").allow_input(), + super().writer_connection(check_same_thread=check_same_thread) as connection, + ): + yield connection def record_hunt(self, commander: ThreadIncarnation, hunt: Hunt) -> HuntRecord: hunt.require_commander(self, commander) diff --git a/tests/test_runtime_publication.py b/tests/test_runtime_publication.py index d1e2a537df..7c8ccfe273 100644 --- a/tests/test_runtime_publication.py +++ b/tests/test_runtime_publication.py @@ -9,7 +9,7 @@ from agent_comms.active_route import LocalRoute from agent_comms.owner_cutover import StoppedOwnerInstallation -from tools.publish_runtime import RuntimePublication +from publish_runtime import RuntimePublication @pytest.mark.parametrize("command", ["exit 17", "echo broken >&2; exit 0"]) diff --git a/tests/verify_install_publication.py b/tests/verify_install_publication.py index 867038657d..a722b85da0 100644 --- a/tests/verify_install_publication.py +++ b/tests/verify_install_publication.py @@ -18,14 +18,14 @@ ROOT = Path(__file__).resolve().parents[1] MAIN = Path("/home/ts/code/projects/agent-comms") -sys.path[:0] = [str(MAIN / "src"), str(ROOT), str(ROOT / "tools")] +sys.path[:0] = [str(MAIN / "src"), str(ROOT / "tools")] from agent_comms.commander.config import CommanderConfig from agent_comms.field_codec import FieldCodec from install_gate import ExternalRecord from agent_comms_hooks import ClaudeHookProgram from agent_comms_hooks.skill import SkillText -from tools.upgrade_bus import Stack +from upgrade_bus import Stack @dataclass(frozen=True) @@ -66,7 +66,7 @@ class HookResult: def main() -> None: - home = Path("/home/ts/.cache/agent-scratch/pi-install-gate/p275-publication-proof-final") + home = Path("/home/ts/.cache/agent-scratch/pi-worker-cpu/p281/publication-proof") home.mkdir(mode=0o700) settings = home / ".claude/settings.json" settings.parent.mkdir() diff --git a/tests/verify_maintenance_holder.py b/tests/verify_maintenance_holder.py index ca2d538e10..38007f2340 100644 --- a/tests/verify_maintenance_holder.py +++ b/tests/verify_maintenance_holder.py @@ -25,7 +25,7 @@ from agent_comms.maintenance_barrier import DeadHolderReleasedPhase, MaintenanceState from upgrade_bus import Revisions, Stack, copy_bus, detached_live, live -SCRATCH = Path("/home/ts/.cache/agent-scratch/pi-install-gate/p281-live-proof-v4") +SCRATCH = Path("/home/ts/.cache/agent-scratch/pi-worker-cpu/p281/holder-death") BUS = SCRATCH / "copy" @@ -68,7 +68,7 @@ def live_worker() -> None: # steps need no new revision worktree. The real busy owner holds its drain. head = subprocess.check_output(('git', 'rev-parse', 'HEAD'), cwd=ROOT, text=True).strip() live(ROOT, BUS, head, head, (), STACK, STACK, - Revisions(ROOT, SCRATCH / 'unused-revisions'), 'pi-install-gate') + Revisions(ROOT, SCRATCH / 'unused-revisions'), 'pi-worker-cpu') def caller() -> None: diff --git a/tools/publish_runtime.py b/tools/publish_runtime.py index 857475af57..6b3d8c1337 100644 --- a/tools/publish_runtime.py +++ b/tools/publish_runtime.py @@ -42,8 +42,8 @@ from agent_comms.owner_restart import OwnerRestartHandoff, OwnerRestartRequest, StoppedOwnerBatch from agent_comms.store_files import _atomic_write_text, _store_lock -from .cutover.backup import StoreBackup -from .cutover.carry import PendingConversion, PreparedCarries +from cutover.backup import StoreBackup +from cutover.carry import PendingConversion, PreparedCarries class PublicationIntent(SubclassUnion, affix=''): @@ -243,10 +243,9 @@ def complete(self, stopped: StoppedOwnerBatch) -> tuple[OwnerRestartResult, ...] stopped.handoff, self.maintenance_hold, self.carries.checkout) result = subprocess.run([ - self.runtime / 'bin/python', '-I', '-B', '-c', - 'import sys; sys.path.insert(0, sys.argv.pop(1)); ' - 'from tools.publish_runtime import main; main()', - str(request.checkout), '--complete-stopped', + self.runtime / 'bin/python', '-E', '-s', '-B', + str(request.checkout / Path(__file__).parent.name / Path(__file__).name), + '--complete-stopped', str(stopped.wire_descriptor), str(self.route_descriptor), ], input=json.dumps(FieldCodec.encode(request)), text=True, stdout=subprocess.PIPE, pass_fds=(stopped.wire_descriptor, self.route_descriptor), check=True) diff --git a/tools/upgrade_bus.py b/tools/upgrade_bus.py index 4fb12b3b43..7846cc9718 100644 --- a/tools/upgrade_bus.py +++ b/tools/upgrade_bus.py @@ -42,7 +42,6 @@ # This file's tree is the target revision: the tool and every step it runs import that # tree's src, never the src the checkout being upgraded has installed. -sys.path.insert(0, str(Path(__file__).resolve().parents[1])) sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) import agent_comms.activity_checkpoint # noqa: F401 (declares the activity runtime files) @@ -54,9 +53,9 @@ from agent_comms.registry_store import RegistryStore from agent_comms.store_files import _store_lock from agent_comms.todos import PlannedInstallDeadline -from tools import publish_runtime -from tools.cutover.backup import Backup, StoreBackup, UpgradeRefused, bus_files, transcripts_held -from tools.cutover.carry import Carried, CarryChain, CarryLedger, Step, TodoColumnCarry +import publish_runtime +from cutover.backup import Backup, StoreBackup, UpgradeRefused, bus_files, transcripts_held +from cutover.carry import Carried, CarryChain, CarryLedger, Step, TodoColumnCarry REPO = Path(__file__).resolve().parents[1] CLIENT_IMPORTS = "import toad.app, agent_comms.acp" @@ -127,7 +126,7 @@ def publish(self) -> None: """The selected interpreter owns console metadata and harness declarations.""" run( self.python, "-m", publish_runtime.__name__, "--refresh", - cwd=self.checkout, env=self.environment(self.checkout), + cwd=self.checkout, env=self.environment(self.checkout / "tools"), ) def environment(self, *paths: Path) -> dict: From 92d14fac8972ad41c96364565abdb0aff3c47925 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 22:18:54 -0400 Subject: [PATCH 17/66] Migrate installer test consumers to their canonical tool modules --- tests/runtime_publication_journey.py | 10 +++++----- tests/test_carry.py | 14 +++++++------- tests/test_cli_watch_exit.py | 2 +- tests/test_codex_earlier_pages.py | 2 +- tests/test_send_refusals.py | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/runtime_publication_journey.py b/tests/runtime_publication_journey.py index 972607958b..1759a13524 100644 --- a/tests/runtime_publication_journey.py +++ b/tests/runtime_publication_journey.py @@ -20,8 +20,8 @@ from agent_comms.owner_cutover import StoppedOwnerInstallation from agent_comms.maintenance_barrier import ReadyPhase from agent_comms.runtime import socket_path -from tools.publish_runtime import RuntimePublication -from tools.cutover.carry import CarryChain, CarryLedger +from publish_runtime import RuntimePublication +from cutover.carry import CarryChain, CarryLedger def main(): @@ -75,14 +75,14 @@ def main(): try: # Kill only the isolated publisher while it is waiting, then re-enter # against the real maintenance witness it left behind. - waiting = subprocess.Popen([target / 'bin/python', '-I', '-c', ''' + waiting = subprocess.Popen([target / 'bin/python', '-E', '-s', '-c', ''' import sys from pathlib import Path -from tools.publish_runtime import RuntimePublication +from publish_runtime import RuntimePublication from agent_comms.comms import Comms root=Path(sys.argv[2]);c=Comms(root) RuntimePublication(Path(sys.argv[3]),{}).publish(root,checkout=Path(sys.argv[1])) -''', str(source), str(root), str(target)], env=dict(os.environ)) +''', str(source), str(root), str(target)], env=dict(os.environ), cwd=source / 'tools') deadline = time.monotonic() + 10 while not comms.owners.maintenance.read().turn_pending: assert waiting.poll() is None and time.monotonic() < deadline diff --git a/tests/test_carry.py b/tests/test_carry.py index a5d72d71e0..386a5d05e1 100644 --- a/tests/test_carry.py +++ b/tests/test_carry.py @@ -40,7 +40,7 @@ from agent_comms.recorded_time import RecordedEventTime from test_todos import order, thread from cutover.backup import Backup, UpgradeRefused, bus_files, transcripts_held -from tools.upgrade_bus import ( +from upgrade_bus import ( Revisions, Stack, before_stop, @@ -502,7 +502,7 @@ def test_turn_records_drop_an_idle_stored_turn_and_refuse_a_held_one(tmp_path, m def test_upgrade_stops_and_starts_exactly_the_captured_set_concurrently(comms, tmp_path, monkeypatch): from threading import Barrier - import tools.upgrade_bus as upgrade_bus + import upgrade_bus from agent_comms.maintenance_barrier import ReadyPhase names = [f"worker-{number}" for number in range(19)] @@ -581,7 +581,7 @@ def cli(self, root, *arguments): def test_selected_revision_prepares_pins_and_cannot_return_a_failed_cached_tree(tmp_path, monkeypatch): - import tools.upgrade_bus as upgrade_bus + import upgrade_bus commands = [] @@ -608,7 +608,7 @@ def missing_dependency(*command): def test_failed_carry_preserves_changed_bus_and_main_under_maintenance(comms, tmp_path, monkeypatch): - import tools.upgrade_bus as upgrade_bus + import upgrade_bus from agent_comms.maintenance_barrier import ReadyPhase datum = comms.root / "kept.txt" @@ -685,12 +685,12 @@ def bridge_publishes(bus: Path) -> str: def rehearsed(stack: Stack, root: Path) -> dict: - from tools.upgrade_bus import rehearse + from upgrade_bus import rehearse return rehearse(root, (), stack, None) def installed_live(stack: Stack, root: Path) -> dict: - from tools.upgrade_bus import live + from upgrade_bus import live return live(stack.checkout, root, "old", "new", (), stack, stack, None, "worker") @@ -698,7 +698,7 @@ def installed_live(stack: Stack, root: Path) -> dict: def test_a_step_writing_the_transcript_store_runs_after_the_hold(comms, tmp_path, monkeypatch, install): """The d88a12185 rehearsal: ToolCallIndex's BEGIN IMMEDIATE timed out against the installer's own transcript hold, which then stayed up across every carry.""" - import tools.upgrade_bus as upgrade_bus + import upgrade_bus from cutover.carry_tool_call_index import ToolCallIndex unit = comms.registry.declare(comms.registry.new_thread( diff --git a/tests/test_cli_watch_exit.py b/tests/test_cli_watch_exit.py index da0d0f7d9c..3adb77f19f 100644 --- a/tests/test_cli_watch_exit.py +++ b/tests/test_cli_watch_exit.py @@ -164,7 +164,7 @@ async def test_a_watch_pauses_for_an_upgrade_and_resumes_after_its_carry(tmp_pat from cutover.carry import STEPS, CarryChain, CarryLedger from test_bus_route_counts_locking import _case from test_carry import REPO, CheckoutStack, Repository - from tools.upgrade_bus import Revisions, carry_all + from upgrade_bus import Revisions, carry_all from agent_comms.maintenance_barrier import MaintenanceBarrier, MaintenanceOperator comms = _case(tmp_path / 'wire', monkeypatch) diff --git a/tests/test_codex_earlier_pages.py b/tests/test_codex_earlier_pages.py index 35e0f47538..8db5bce116 100644 --- a/tests/test_codex_earlier_pages.py +++ b/tests/test_codex_earlier_pages.py @@ -165,7 +165,7 @@ def test_stopped_carry_preserves_external_items(tmp_path): from agent_comms.external_transcript import ExternalTranscript, NoEarlierPage from agent_comms.field_codec import FieldCodec from agent_comms.transcript_events import AssistantTranscript - from tools.cutover.carry_external_pages import ExternalPages + from cutover.carry_external_pages import ExternalPages root = tmp_path / 'root' root.mkdir() diff --git a/tests/test_send_refusals.py b/tests/test_send_refusals.py index cc21afebad..4dbc5ae12e 100644 --- a/tests/test_send_refusals.py +++ b/tests/test_send_refusals.py @@ -8,7 +8,7 @@ from agent_comms.coordination_tables.refusals import SendRefusal from agent_comms.errors import RelationViolationError, ToolRefusedError from agent_comms.notification_assignment import NotificationAssignment -from tools.cutover.carry_send_refusals import SendRefusalOutcomes +from cutover.carry_send_refusals import SendRefusalOutcomes def test_install_preserves_existing_coordination_rows(comms): From 0c298b394c949529973936db5dcf9b40306b4113 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 22:34:15 -0400 Subject: [PATCH 18/66] Use the typed codec boundary and execute the declared publication source --- tools/cutover/carry_maintenance_identity.py | 6 +++--- tools/upgrade_bus.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/cutover/carry_maintenance_identity.py b/tools/cutover/carry_maintenance_identity.py index 0d970b0f51..1d3b843409 100644 --- a/tools/cutover/carry_maintenance_identity.py +++ b/tools/cutover/carry_maintenance_identity.py @@ -12,7 +12,7 @@ from typing import Literal from agent_comms.errors import RelationViolationError -from agent_comms.field_codec import FieldCodec, Id128Text, compile_decoder +from agent_comms.field_codec import FieldCodec, Id128Text from agent_comms.maintenance_barrier import ( MaintenanceBarrier, MaintenanceOperator, @@ -84,8 +84,8 @@ def carry(cls, root: Path, *, apply: bool) -> Carried: if barrier.marker_path.exists(): raise RelationViolationError("Maintenance has a marker but no stored holder") return Carried(0) - state: StoredMaintenanceState = compile_decoder(StoredMaintenanceState).decode( - FieldCodec.parse(barrier.state_path.read_text()) + state = FieldCodec.decode( + StoredMaintenanceState, FieldCodec.parse(barrier.state_path.read_text()) ) return state.carried(barrier, apply=apply) diff --git a/tools/upgrade_bus.py b/tools/upgrade_bus.py index 7846cc9718..92eac349b1 100644 --- a/tools/upgrade_bus.py +++ b/tools/upgrade_bus.py @@ -125,8 +125,8 @@ def prepare_native(self) -> None: def publish(self) -> None: """The selected interpreter owns console metadata and harness declarations.""" run( - self.python, "-m", publish_runtime.__name__, "--refresh", - cwd=self.checkout, env=self.environment(self.checkout / "tools"), + self.python, publish_runtime.__file__, "--refresh", + cwd=self.checkout, env=self.environment(), ) def environment(self, *paths: Path) -> dict: From 2abb43cd44dc2dec6e9752a6d1807c0370e72132 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 22:36:14 -0400 Subject: [PATCH 19/66] Preserve the compiled union result at its validated type boundary --- tools/cutover/carry_maintenance_identity.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tools/cutover/carry_maintenance_identity.py b/tools/cutover/carry_maintenance_identity.py index 1d3b843409..4fb2d60792 100644 --- a/tools/cutover/carry_maintenance_identity.py +++ b/tools/cutover/carry_maintenance_identity.py @@ -9,10 +9,10 @@ from dataclasses import dataclass from pathlib import Path -from typing import Literal +from typing import Literal, cast from agent_comms.errors import RelationViolationError -from agent_comms.field_codec import FieldCodec, Id128Text +from agent_comms.field_codec import FieldCodec, Id128Text, compile_decoder from agent_comms.maintenance_barrier import ( MaintenanceBarrier, MaintenanceOperator, @@ -84,8 +84,11 @@ def carry(cls, root: Path, *, apply: bool) -> Carried: if barrier.marker_path.exists(): raise RelationViolationError("Maintenance has a marker but no stored holder") return Carried(0) - state = FieldCodec.decode( - StoredMaintenanceState, FieldCodec.parse(barrier.state_path.read_text()) + state = cast( + StoredMaintenanceState, + compile_decoder(StoredMaintenanceState).decode( + FieldCodec.parse(barrier.state_path.read_text()) + ), ) return state.carried(barrier, apply=apply) From 93c2b1ed9c8f627ad11703864d786cc757327a03 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 22:59:39 -0400 Subject: [PATCH 20/66] Assert native pause refusal before recovering its retired holder --- tests/test_maintenance_native_boundary.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/test_maintenance_native_boundary.py b/tests/test_maintenance_native_boundary.py index ee252d01b6..388f8f9389 100644 --- a/tests/test_maintenance_native_boundary.py +++ b/tests/test_maintenance_native_boundary.py @@ -13,7 +13,7 @@ import agent_comms.coordinated_runtime as runtime import agent_comms.native_pi as native_pi -from agent_comms.maintenance_barrier import MaintenancePhase +from agent_comms.maintenance_barrier import DeadHolderReleasedPhase, MaintenanceHeldError from agent_comms.child_process import ProcessIdentity from agent_comms.coordinator import Coordination from agent_comms.errors import RelationViolationError @@ -77,11 +77,20 @@ def launch(package, *, worktree, session, **_kwargs): opt_in=True, cursor_delivery=cursor_delivery, ).run() - assert comms.owners.maintenance.read().phase == MaintenancePhase.member_named("paused") writes = [json.loads(line) for line in received.read_text().splitlines()] assert [item["type"] for item in writes] == ["get_state"] + holder_pid = int(child_pid.read_text()) with pytest.raises(ProcessLookupError): - ProcessIdentity.capture(int(child_pid.read_text())) + ProcessIdentity.capture(holder_pid) + # The raw prompt was refused while its RPC holder was alive. Retirement + # killed that holder, so the detecting read refuses once and releases it. + with pytest.raises(MaintenanceHeldError, match=f"released dead holder pid={holder_pid} "): + comms.owners.maintenance.read() + released = comms.owners.maintenance.read() + assert released.phase is DeadHolderReleasedPhase + assert released.operator.pid == holder_pid and released.operator.start_time > 0 + with comms.owners.maintenance.allow_input(): + pass with Coordination(str(root / "coordination.sqlite3")) as store: assert ( store.session._connection.execute( From d85b0616fef855243f779862fefd237b95c367bd Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 22:45:14 -0400 Subject: [PATCH 21/66] P301 checkpoint: observe live native order progress without claiming workflow parity --- src/agent_comms/commander_parity/lifecycle.py | 43 +++++++++++++++++++ src/agent_comms/commander_parity/rows.py | 5 +++ 2 files changed, 48 insertions(+) create mode 100644 src/agent_comms/commander_parity/lifecycle.py diff --git a/src/agent_comms/commander_parity/lifecycle.py b/src/agent_comms/commander_parity/lifecycle.py new file mode 100644 index 0000000000..c7c9c0cd5d --- /dev/null +++ b/src/agent_comms/commander_parity/lifecycle.py @@ -0,0 +1,43 @@ +"""Live order-family observations; partial observations never authorize a switch.""" + +from __future__ import annotations + +import sys +from types import FrameType + +from ..commander.config import CommanderConfig +from ..commander.order import Commander +from ..todos import Moving, NoCommitProgress, OrderProgress, Todo +from . import ParityComparison, UnverifiedParity + + +def verify_order_progress() -> ParityComparison: + """Observe the real native measurements returned through the current reader. + + This first checkpoint deliberately reports incomplete coverage: matching a + read is not proof of scheduling, strike suppression or unit-facing delivery. + """ + context = Commander.read(CommanderConfig.path()) + observed: dict[str, str] = {} + + def observe(frame: FrameType, event: str, value: object) -> None: + if event == "return" and frame.f_code is OrderProgress.read.__func__.__code__: + todo = frame.f_locals["todo"] + if not isinstance(todo, Todo) or not isinstance(value, OrderProgress): + raise TypeError("The native progress reader returned an unexpected record") + observed[todo.id] = value.line + + previous = sys.getprofile() + sys.setprofile(observe) + try: + current = {key: value.line for key, value in context.progress().items()} + finally: + sys.setprofile(previous) + return UnverifiedParity( + f"Live OrderProgress.read: {len(observed)} native observations, " + f"current/native equal={bool(observed) and current == observed}; " + f"first-commit interval={NoCommitProgress.seconds}s, " + f"later-progress interval={Moving.seconds}s. " + "Scheduling, merged-work strike suppression and unit deadline visibility " + "still require their live workflow comparison." + ) diff --git a/src/agent_comms/commander_parity/rows.py b/src/agent_comms/commander_parity/rows.py index ab22a2feb2..d023928624 100644 --- a/src/agent_comms/commander_parity/rows.py +++ b/src/agent_comms/commander_parity/rows.py @@ -13,6 +13,7 @@ from . import ParityComparison, ParityRow, UnverifiedParity from .load import LoadComparison from .measurements import RuleComparison, StorageComparison, measured_load +from .lifecycle import verify_order_progress class CreateProblemParityRow(ParityRow): @@ -312,6 +313,10 @@ class OrderProgressParityRow(ParityRow): ) phases = (OneOrderOwnerPhase, ControlPlanePhase) + @classmethod + def verify(cls) -> ParityComparison: + return verify_order_progress() + class MultipleCommandersParityRow(ParityRow): number = 34 From 524cd61e7feac52fe27d06bcdf27f7b291a2e276 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:08:29 -0400 Subject: [PATCH 22/66] Exercise copied native creation assignment evidence and strikes; repair deadline projection owners --- src/agent_comms/commander/order.py | 4 +- src/agent_comms/commander_parity/lifecycle.py | 329 +++++++++++++++++- src/agent_comms/commander_parity/rows.py | 18 +- src/agent_comms/todos.py | 3 +- 4 files changed, 333 insertions(+), 21 deletions(-) diff --git a/src/agent_comms/commander/order.py b/src/agent_comms/commander/order.py index 33e6ee33de..e1cc9956cd 100644 --- a/src/agent_comms/commander/order.py +++ b/src/agent_comms/commander/order.py @@ -729,8 +729,8 @@ def due(self, pid: str, at: str, why: str) -> None: moved = deadline(at) self.call(CommsChangeProblemTool(problem=pid, operation=AssignTodo( todo.revision, todo.assignment, moved, todo.order, todo.goal))) - self.on_record(f'{time.strftime("%Y-%m-%d %H:%M")} {todo.holder} ({pid} order {todo.order.id} due ' - f'{hhmm(todo.deadline)} moved to {hhmm(moved)}): {why}', 'Deadline', + self.on_record(f'{time.strftime("%Y-%m-%d %H:%M")} {todo.holder} ({pid} order {todo.order.id} ' + f'deadline changed): {why}', 'Deadline', VerdictAudience.of(todo.held().owner, todo.goal)) print(f'{pid}: due {hhmm(moved)}') diff --git a/src/agent_comms/commander_parity/lifecycle.py b/src/agent_comms/commander_parity/lifecycle.py index c7c9c0cd5d..c35962539f 100644 --- a/src/agent_comms/commander_parity/lifecycle.py +++ b/src/agent_comms/commander_parity/lifecycle.py @@ -1,24 +1,323 @@ -"""Live order-family observations; partial observations never authorize a switch.""" +"""Exercise current order commands against real native owners on a selected bus copy.""" from __future__ import annotations +import contextlib +import io +import json +import os +import shlex import sys -from types import FrameType +import time +from abc import ABC, abstractmethod +from collections.abc import Callable, Iterator +from dataclasses import dataclass, replace +from pathlib import Path +from types import FrameType, FunctionType +from typing import ClassVar +from uuid import uuid4 +from ..change_report import FileLine +from ..check_root import CheckRoot from ..commander.config import CommanderConfig -from ..commander.order import Commander -from ..todos import Moving, NoCommitProgress, OrderProgress, Todo +from ..commander.order import Commander, OrderFile, hhmm +from ..commander.request import RequestAddVerb +from ..comms import Comms +from ..field_codec import FieldCodec +from ..goals import CountCheckResult, OrderCheck, ProblemGoalOrder +from ..tool_caller import ToolCaller +from ..tool_result import ToolResult +from ..tools import ( + CommsChangeProblemTool, CommsFireTool, CommsOrderTool, + CommsProblemCloseTool, CommsTodoCreateTool, ProblemOrdered, ThreadToolRequest, TodoChanged, TodoList, +) +from ..todos import ( + ApplicationOrderRun, MergedProgress, Moving, NoCommitProgress, + OrderProgress, PendingProblemOrder, Todo, +) from . import ParityComparison, UnverifiedParity -def verify_order_progress() -> ParityComparison: - """Observe the real native measurements returned through the current reader. +@dataclass(frozen=True) +class LifecycleObservation(ParityComparison): + owner: str + invocations: int + checks: tuple[tuple[str, bool], ...] + output: str + + @property + def passed(self) -> bool: + return bool(self.invocations and self.checks) and all(value for _, value in self.checks) + + def describe(self) -> str: + return f"owner={self.owner}; calls={self.invocations}; checks={dict(self.checks)}; output={self.output!r}" + + +@dataclass(frozen=True) +class LifecycleRun: + commander: Commander + worker: str + decisions: Path + document: Path + artifact: Path + + @classmethod + @contextlib.contextmanager + def open(cls) -> Iterator[LifecycleRun]: + config = CommanderConfig.read(CommanderConfig.path()) + source = Comms(config.root) + actor = ToolCaller.from_actor(os.environ.get("AGENT_COMMS_THREAD")).acting_name(config.commander) + worker = source.registry.require(actor) + if worker.name == config.commander: + raise ValueError("Lifecycle comparison requires an existing unit caller, not its commander") + directory = worker.storage_roots.scratch / "commander-parity" / uuid4().hex + with CheckRoot(source, (config.commander, worker.name), directory).open() as copied: + root = copied.root + commander = Commander( + commander=config.commander, root=root, cli=config.cli, + state=root / "commander-state", board=root / "board.md", + narrative=root / "narrative.md", repos=config.repos, + worktrees=root / "projects", scratch=root / "scratch", disk=root, + ) + with contextlib.redirect_stdout(io.StringIO()): + RequestAddVerb(text=( + "Compare current order commands with their real native owners in this " + "disposable copied bus, preserving the source bus and its sessions.")).run(commander) + decisions = root / "decisions.json" + decisions.write_text(json.dumps([worker.name])) + command = shlex.join((sys.executable, "-c", + "import json;from pathlib import Path;" + f"print(len(json.loads(Path({str(decisions)!r}).read_text())))")) + order = PendingProblemOrder( + owner=FileLine(str(Path(__file__).resolve()), 1), + check=OrderCheck(str(Path(__file__).resolve().parents[3]), command, CountCheckResult(0)), + edits=("Remove the copied workflow decision and verify its native order completes.",), + banned=("Do not change the source bus or its saved sessions.",), + report="Report the actual copied goal and native order state.", + run=ApplicationOrderRun( + "Read the copied native goal and confirm its recorded source location and assigned worker."), + ) + document = root / "order.json" + document.write_text(json.dumps(FieldCodec.encode(OrderFile( + order, "The copied workflow decision prevents the native completion check from reaching zero.", + "This is stupid because an unfinished copied decision must not count as delivered work.")))) + yield cls(commander, worker.name, decisions, document, root / "application.json") + + @property + def due(self) -> str: + return time.strftime("%H:%M", time.localtime(time.time() + 3600)) + + def create(self) -> Todo: + self.commander.problem(str(self.document), self.worker, self.due, + f"{Path(__file__).resolve()}:1") + rows = tuple(self.commander.comms.goals.todos.for_commander(self.commander.incarnation)) + if len(rows) != 1: + raise ValueError(f"A fresh copied workflow must create one problem, got {len(rows)}") + return rows[0] + + def complete(self, todo: Todo) -> Todo: + goal = self.commander.comms.registry.require(self.worker).goal + self.artifact.write_text(json.dumps(FieldCodec.encode(goal))) + self.decisions.write_text("[]") + self.commander.done(todo.id, "op", seen=str(self.artifact)) + return self.commander.comms.goals.todos.get(todo.id) + + +class LifecycleCase(ABC): + native_owner: ClassVar[type[ThreadToolRequest]] + additional_owners: ClassVar[tuple[type[ThreadToolRequest], ...]] = () + + @classmethod + @abstractmethod + def exercise(cls, run: LifecycleRun) -> tuple[Todo, ...]: ... + + @classmethod + @abstractmethod + def checks(cls, run: LifecycleRun, rows: tuple[Todo, ...], + native: tuple[ToolResult, ...], output: str) -> tuple[tuple[str, bool], ...]: ... + + @classmethod + def read(cls) -> ParityComparison: + owners = (cls.native_owner, *cls.additional_owners) + codes = set() + for owner in owners: + function = owner.apply + if not isinstance(function, FunctionType): + raise TypeError("A native tool must declare its executable apply method") + codes.add(function.__code__) + native: list[ToolResult] = [] + + def observe(frame: FrameType, event: str, value: object) -> None: + if event == "return" and frame.f_code in codes: + request = frame.f_locals["self"] + if type(request) in owners and isinstance(value, ToolResult): + native.append(value) + + with LifecycleRun.open() as run: + previous = sys.getprofile() + output = io.StringIO() + sys.setprofile(observe) + try: + with contextlib.redirect_stdout(output): + rows = cls.exercise(run) + finally: + sys.setprofile(previous) + text = output.getvalue() + return LifecycleObservation( + ", ".join(f"{owner.__module__}.{owner.__qualname__}" for owner in owners), len(native), + cls.checks(run, rows, tuple(native), text), text) + + +class CreateProblemCase(LifecycleCase): + native_owner = CommsTodoCreateTool + + @classmethod + def exercise(cls, run: LifecycleRun) -> tuple[Todo, ...]: + return (run.create(),) + + @classmethod + def checks(cls, run: LifecycleRun, rows: tuple[Todo, ...], + native: tuple[ToolResult, ...], output: str) -> tuple[tuple[str, bool], ...]: + if len(native) != 1 or not isinstance(native[0], ProblemOrdered): + return (("one native creation result", False),) + result, saved = native[0], rows[-1] + goal = run.commander.comms.registry.require(run.worker).goal + return (("printed native identity", output.rstrip().endswith(cls.printed_identity(result))), + ("native source retained", saved.order.checked().owner == result.todo.order.checked().owner), + ("native assignment retained", saved.assignment == result.todo.assignment), + ("native goal published", goal.require().id == result.goal.id)) + + @classmethod + def printed_identity(cls, result: ProblemOrdered) -> str: + return result.todo.id + - This first checkpoint deliberately reports incomplete coverage: matching a - read is not proof of scheduling, strike suppression or unit-facing delivery. - """ +class AssignOrderCase(CreateProblemCase): + native_owner = CommsOrderTool + + @classmethod + def exercise(cls, run: LifecycleRun) -> tuple[Todo, ...]: + before = run.create() + run.commander.fire(run.worker, "The copied workflow is released before its next native assignment.") + document = replace(OrderFile.read(run.document.read_text()), problems=(before.id,)) + run.commander.send(run.worker, run.due, document, clean=True) + return before, run.commander.comms.goals.todos.get(before.id) + + @classmethod + def checks(cls, run: LifecycleRun, rows: tuple[Todo, ...], + native: tuple[ToolResult, ...], output: str) -> tuple[tuple[str, bool], ...]: + inherited = super().checks(run, rows, native, output) + return (*inherited, ("same problem reassigned", rows[0].id == rows[-1].id), + ("prior assignment retained", bool(rows[-1].released))) + + @classmethod + def printed_identity(cls, result: ProblemOrdered) -> str: + return result.todo.order.checked().id + + +class InstalledEvidenceCase(LifecycleCase): + native_owner = CommsProblemCloseTool + + @classmethod + def exercise(cls, run: LifecycleRun) -> tuple[Todo, ...]: + created = run.create() + delivered = run.complete(created) + run.commander.close(created.id, + "Read the copied native goal and confirm its recorded source location and assigned worker.", + str(run.artifact)) + return delivered, run.commander.comms.goals.todos.get(created.id) + + @classmethod + def checks(cls, run: LifecycleRun, rows: tuple[Todo, ...], + native: tuple[ToolResult, ...], output: str) -> tuple[tuple[str, bool], ...]: + return (("delivery alone unaccepted", not rows[0].order.accepted), + ("installed evidence accepted", rows[-1].order.accepted), + ("closed in native store", not rows[-1].state.open), + ("actual artifact exists", run.artifact.is_file()), + ("current close printed", f"{rows[-1].id} closed" in output)) + + +class StrikeLimitCase(LifecycleCase): + native_owner = CommsChangeProblemTool + additional_owners = (CommsFireTool,) + + @classmethod + def exercise(cls, run: LifecycleRun) -> tuple[Todo, ...]: + created = run.create() + for number in range(ProblemGoalOrder.StrikeLimit): + run.commander.strike(run.worker, created.id, + f"The copied decision remains unfinished at its recorded review number {number + 1}.") + run.commander.fire(run.worker, + "The copied holder exhausted its declared live strikes on this problem.") + return created, run.commander.comms.goals.todos.get(created.id) + + @classmethod + def checks(cls, run: LifecycleRun, rows: tuple[Todo, ...], + native: tuple[ToolResult, ...], output: str) -> tuple[tuple[str, bool], ...]: + saved = rows[-1] + return (("declared limit recorded", saved.strike_count == ProblemGoalOrder.StrikeLimit), + ("one native fire", sum(isinstance(result, TodoList) for result in native) == 1), + ("assignment released", saved.holder is None), + ("current strike count printed", f"{ProblemGoalOrder.StrikeLimit} strike(s)" in output)) + + +class OrderProgressMutationCase(LifecycleCase): + native_owner = CommsChangeProblemTool + + @classmethod + def exercise(cls, run: LifecycleRun) -> tuple[Todo, ...]: + before = run.create() + moved = time.strftime("%H:%M", time.localtime(time.time() + 7200)) + run.commander.due(before.id, moved, + "The copied workflow needs time to record its actual native application evidence.") + return before, run.commander.comms.goals.todos.get(before.id) + + @classmethod + def checks(cls, run: LifecycleRun, rows: tuple[Todo, ...], + native: tuple[ToolResult, ...], output: str) -> tuple[tuple[str, bool], ...]: + before, after = rows + messages = run.commander.comms.bus.inbox(run.worker) + unit = run.commander.comms.registry.require(run.worker) + goal = unit.goal.require() + instructions = goal.order.instructions(goal.order.todos( + run.commander.comms.goals, unit.incarnation, goal.id)) + native_deadlines = tuple(result.todo.deadline for result in native if isinstance(result, TodoChanged)) + return (("native deadline retained", bool(native_deadlines) and native_deadlines[-1] == after.deadline), + ("deadline actually changed", before.deadline != after.deadline), + ("assignment unchanged", before.assignment == after.assignment), + ("unit got the actual update", bool(messages)), + ("unit messages hide clocks", all(hhmm(clock) not in message.body + for clock in (before.deadline, after.deadline) for message in messages)), + ("unit goal hides deadline", "Deadline:" not in instructions + and hhmm(after.deadline) not in instructions), + ("commander still sees deadline", f"due {hhmm(after.deadline)}" in output)) + + +@dataclass(frozen=True) +class ProgressComparison(ParityComparison): + current: dict[str, str] + native: dict[str, str] + first_seconds: int + later_seconds: int + merged_no_strikes: tuple[bool, ...] + workflow: ParityComparison + + @property + def passed(self) -> bool: + return (self.workflow.passed and bool(self.native) and self.current == self.native + and self.first_seconds == 600 and self.later_seconds == 900 + and bool(self.merged_no_strikes) and all(self.merged_no_strikes)) + + def describe(self) -> str: + return (f"{self.workflow.describe()}; live_progress={len(self.native)}; " + f"read_equal={self.current == self.native}; intervals={self.first_seconds}/{self.later_seconds}; " + f"live_merged_no_strikes={self.merged_no_strikes}") + + +def verify_order_progress() -> ParityComparison: context = Commander.read(CommanderConfig.path()) observed: dict[str, str] = {} + merged: list[bool] = [] def observe(frame: FrameType, event: str, value: object) -> None: if event == "return" and frame.f_code is OrderProgress.read.__func__.__code__: @@ -26,6 +325,8 @@ def observe(frame: FrameType, event: str, value: object) -> None: if not isinstance(todo, Todo) or not isinstance(value, OrderProgress): raise TypeError("The native progress reader returned an unexpected record") observed[todo.id] = value.line + if isinstance(value, MergedProgress): + merged.append(not value.stalled(todo.order.checked().at.recorded_at("An order"), time.time())) previous = sys.getprofile() sys.setprofile(observe) @@ -33,11 +334,5 @@ def observe(frame: FrameType, event: str, value: object) -> None: current = {key: value.line for key, value in context.progress().items()} finally: sys.setprofile(previous) - return UnverifiedParity( - f"Live OrderProgress.read: {len(observed)} native observations, " - f"current/native equal={bool(observed) and current == observed}; " - f"first-commit interval={NoCommitProgress.seconds}s, " - f"later-progress interval={Moving.seconds}s. " - "Scheduling, merged-work strike suppression and unit deadline visibility " - "still require their live workflow comparison." - ) + return ProgressComparison(current, observed, NoCommitProgress.seconds, Moving.seconds, + tuple(merged), OrderProgressMutationCase.read()) diff --git a/src/agent_comms/commander_parity/rows.py b/src/agent_comms/commander_parity/rows.py index d023928624..aed40ce0be 100644 --- a/src/agent_comms/commander_parity/rows.py +++ b/src/agent_comms/commander_parity/rows.py @@ -13,7 +13,7 @@ from . import ParityComparison, ParityRow, UnverifiedParity from .load import LoadComparison from .measurements import RuleComparison, StorageComparison, measured_load -from .lifecycle import verify_order_progress +from .lifecycle import AssignOrderCase, CreateProblemCase, InstalledEvidenceCase, StrikeLimitCase, verify_order_progress class CreateProblemParityRow(ParityRow): @@ -23,6 +23,10 @@ class CreateProblemParityRow(ParityRow): native = "`CommsTodoCreateTool`" phases = (OneOrderOwnerPhase,) + @classmethod + def verify(cls) -> ParityComparison: + return CreateProblemCase.read() + class AssignOrderParityRow(ParityRow): number = 2 @@ -31,6 +35,10 @@ class AssignOrderParityRow(ParityRow): native = "`CommsOrderTool`, `CommsTodoTool` assignment and release" phases = (OneOrderOwnerPhase,) + @classmethod + def verify(cls) -> ParityComparison: + return AssignOrderCase.read() + class DeliveredCommitParityRow(ParityRow): number = 3 @@ -47,6 +55,10 @@ class InstalledEvidenceParityRow(ParityRow): native = "`CommsProblemCloseTool`" phases = (OneOrderOwnerPhase,) + @classmethod + def verify(cls) -> ParityComparison: + return InstalledEvidenceCase.read() + class BoardParityRow(ParityRow): number = 5 @@ -71,6 +83,10 @@ class StrikeLimitParityRow(ParityRow): native = "`Strike` rows, author-declared `StrikeLimit`, one `comms_fire`" phases = (RecordPlanePhase,) + @classmethod + def verify(cls) -> ParityComparison: + return StrikeLimitCase.read() + class VerdictParityRow(ParityRow): number = 8 diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 896e0be57f..e65df2c3dd 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -727,6 +727,7 @@ def stalled_between(self, ordered: float, since: float, now: float) -> bool: @dataclass(frozen=True) class Moving(Stalling): """Work whose last commit, push or PR change was observed at ``at``.""" + seconds: ClassVar[int] = 15 * 60 at: float = field(kw_only=True) readings: DistanceReadings = field(default=DistanceReadings(), kw_only=True, compare=False) @@ -1513,7 +1514,7 @@ def work_instructions(self) -> tuple[str, ...]: def instructions(self, todo): return "\n".join((f"Problem: {todo.id}: {todo.text}", *self.finding.instructions(), f"Order: {self.id}", self.pull_request_instruction(todo.id), - *self.work_instructions(), f"Deadline: {FieldCodec.encode(todo.deadline)}")) + *self.work_instructions())) def delivered(self, delivery): delivery.at.require_delivery() From 356e332b753a71f2fa25371676c0a19d8ce767ea Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:24:33 -0400 Subject: [PATCH 23/66] Own candidate worker retirement and measure native calls through Commander inheritance --- src/agent_comms/candidate_maintenance.py | 112 +++++++++--------- src/agent_comms/check_root.py | 2 + src/agent_comms/commander_parity/lifecycle.py | 107 +++++++++-------- src/agent_comms/commander_parity/rows.py | 6 +- tests/test_candidate_maintenance.py | 34 +++--- ...est_candidate_publication_notifications.py | 17 +-- 6 files changed, 146 insertions(+), 132 deletions(-) diff --git a/src/agent_comms/candidate_maintenance.py b/src/agent_comms/candidate_maintenance.py index c2186860b6..ddd4e8e424 100644 --- a/src/agent_comms/candidate_maintenance.py +++ b/src/agent_comms/candidate_maintenance.py @@ -1,16 +1,10 @@ -"""Best-effort indexing of successful appends after the outer locks release. - -Losing a hint or crashing the daemon leaves the optional index unavailable; -canonical bus and sealed SQL still drive original delivery. Byte positions -include observations as well as messages, without inventing message sequences. -""" +"""Index successful appends after outer locks release, within the owning root's lifetime.""" from __future__ import annotations import logging import sqlite3 import threading -from contextlib import ExitStack from pathlib import Path from .bus_route_counts import BusRouteCounts @@ -22,7 +16,7 @@ _LOG = logging.getLogger(__name__) _MAX_ACTIVE_ROOTS = 32 _guard = threading.Lock() -_pending: dict[Path, tuple[WireLog, int]] = {} +_pending: dict[Path, CandidateMaintenance] = {} class CandidateMaintenanceFailed(RuntimeError): # noqa: N818 - named worker failure @@ -33,72 +27,74 @@ def __init__(self, root: Path) -> None: super().__init__(f"Candidate maintenance failed for {root}") -def schedule_candidate_catchup(log: WireLog, through_bytes: int) -> None: - """Prepare writer indexes, then signal their existing maintenance worker.""" - if through_bytes <= 0: - raise ValueError("candidate notification needs an actual committed byte position") - BusRouteCounts(log.path).prepare() - root = log.path.parent - with _guard: - current = _pending.get(root) - if current is not None: - _pending[root] = (current[0], max(current[1], through_bytes)) - return - if len(_pending) >= _MAX_ACTIVE_ROOTS: - raise CandidateMaintenanceFailed(root) - _pending[root] = (log, through_bytes) - try: - threading.Thread( - target=_drain_candidate, - args=(root,), - name="agent-comms-candidate-maintenance", - daemon=True, - ).start() - except RuntimeError as error: - _pending.pop(root, None) - raise CandidateMaintenanceFailed(root) from error +class CandidateMaintenance(threading.Thread): + """One root's coalesced cursor and actual worker; retirement joins this owner.""" + def __init__(self, log: WireLog, through_bytes: int) -> None: + super().__init__(name="agent-comms-candidate-maintenance", daemon=True) + self.log = log + self.through_bytes = through_bytes -def _retire_candidate(root: Path) -> None: - with _guard: - _pending.pop(root, None) - - -def _drain_candidate(root: Path) -> None: - with ExitStack() as cleanup: - cleanup.callback(_retire_candidate, root) + def run(self) -> None: + root = self.log.path.parent try: - with _guard: - log, _bytes = _pending[root] - # Source ownership is checked off the producer path. Maintenance cannot - # create the protocol marker or authorize delivery. - with log.checked_read() as source: + with self.log.checked_read() as source: metadata = source.marker root_id = metadata.root_id - index = WakeCandidateIndex(log) - routes = BusRouteCounts(log.path) + index = WakeCandidateIndex(self.log) + routes = BusRouteCounts(self.log.path) while True: with _guard: - target = _pending[root][1] + target = self.through_bytes hint = index.notify_committed_append(root_id=root_id, through_bytes=target) - result = index.catch_up_committed_append( - hint, start=FirstBuild() - ) - with log.opened_wire_snapshot(opening=CurrentSnapshotOpening()) as snapshot: + result = index.catch_up_committed_append(hint, start=FirstBuild()) + with self.log.opened_wire_snapshot(opening=CurrentSnapshotOpening()) as snapshot: route_result = routes.sync( snapshot, lambda record: DeliveryMessage.from_wire(record, root_id=snapshot.metadata.root_id), ) if result.caught_up and route_result.complete: with _guard: - if _pending[root][1] <= result.checkpoint_bytes: - # Disarm cleanup before a successor can own this root. - cleanup.pop_all() + if self.through_bytes <= result.checkpoint_bytes: _pending.pop(root) return - except ( - OSError, ValueError, sqlite3.Error, CoordinationError, ProjectionUnavailableError, - ) as error: + except (OSError, ValueError, sqlite3.Error, CoordinationError, ProjectionUnavailableError) as error: failure = CandidateMaintenanceFailed(root) _LOG.exception("%s", failure) raise failure from error + finally: + with _guard: + if root in _pending and _pending[root] is self: + _pending.pop(root) + + +def schedule_candidate_catchup(log: WireLog, through_bytes: int) -> None: + """Prepare writer indexes, then coalesce into the root's actual worker.""" + if through_bytes <= 0: + raise ValueError("candidate notification needs an actual committed byte position") + BusRouteCounts(log.path).prepare() + root = log.path.parent + with _guard: + if root in _pending: + worker = _pending[root] + worker.through_bytes = max(worker.through_bytes, through_bytes) + return + if len(_pending) >= _MAX_ACTIVE_ROOTS: + raise CandidateMaintenanceFailed(root) + worker = CandidateMaintenance(log, through_bytes) + _pending[root] = worker + try: + worker.start() + except RuntimeError as error: + _pending.pop(root) + raise CandidateMaintenanceFailed(root) from error + + +def drain_candidate_catchup(root: Path) -> None: + """A stopped root cannot be renamed or removed while its worker still writes.""" + while True: + with _guard: + if root not in _pending: + return + worker = _pending[root] + worker.join() diff --git a/src/agent_comms/check_root.py b/src/agent_comms/check_root.py index 02de2fb15a..b261755d01 100644 --- a/src/agent_comms/check_root.py +++ b/src/agent_comms/check_root.py @@ -15,6 +15,7 @@ from .child_process import NoProcess from .cli_result import CliResult from .comms import Comms +from .candidate_maintenance import drain_candidate_catchup from .fresh_private_session import create_fresh_private_session from .goals import NoGoal from .launch_runtime import RestartEnvironment, ThreadLaunch @@ -74,6 +75,7 @@ def open(self) -> Iterator[Comms]: ): copied.owners.stop(thread.name) finally: + drain_candidate_catchup(root) RecoveryMonitorCapability.retire_check_root(root) def _populate(self, copied: Comms) -> None: diff --git a/src/agent_comms/commander_parity/lifecycle.py b/src/agent_comms/commander_parity/lifecycle.py index c35962539f..4309985644 100644 --- a/src/agent_comms/commander_parity/lifecycle.py +++ b/src/agent_comms/commander_parity/lifecycle.py @@ -10,13 +10,14 @@ import sys import time from abc import ABC, abstractmethod -from collections.abc import Callable, Iterator +from collections.abc import Iterator from dataclasses import dataclass, replace from pathlib import Path -from types import FrameType, FunctionType from typing import ClassVar from uuid import uuid4 +from pydantic import PrivateAttr + from ..change_report import FileLine from ..check_root import CheckRoot from ..commander.config import CommanderConfig @@ -25,15 +26,16 @@ from ..comms import Comms from ..field_codec import FieldCodec from ..goals import CountCheckResult, OrderCheck, ProblemGoalOrder -from ..tool_caller import ToolCaller +from ..tool_caller import ThreadCaller, ToolCaller from ..tool_result import ToolResult from ..tools import ( CommsChangeProblemTool, CommsFireTool, CommsOrderTool, - CommsProblemCloseTool, CommsTodoCreateTool, ProblemOrdered, ThreadToolRequest, TodoChanged, TodoList, + CommsProblemCloseTool, CommsTodoCreateTool, CommsTodoListTool, ProblemOrdered, + ThreadToolRequest, TodoChanged, TodoList, ToolRequest, ) from ..todos import ( ApplicationOrderRun, MergedProgress, Moving, NoCommitProgress, - OrderProgress, PendingProblemOrder, Todo, + PendingProblemOrder, Todo, ) from . import ParityComparison, UnverifiedParity @@ -53,9 +55,30 @@ def describe(self) -> str: return f"owner={self.owner}; calls={self.invocations}; checks={dict(self.checks)}; output={self.output!r}" +@dataclass(frozen=True) +class NativeInvocation: + request: ToolRequest[ThreadCaller] + result: ToolResult + + +class MeasuredCommander(Commander): + """Keep actual native returns at the existing command boundary, not frame locals.""" + + _invocations: list[NativeInvocation] = PrivateAttr(default_factory=list) + + @property + def invocations(self) -> tuple[NativeInvocation, ...]: + return tuple(self._invocations) + + def call(self, tool: ToolRequest[ThreadCaller]) -> ToolResult: + result = super().call(tool) + self._invocations.append(NativeInvocation(tool, result)) + return result + + @dataclass(frozen=True) class LifecycleRun: - commander: Commander + commander: MeasuredCommander worker: str decisions: Path document: Path @@ -73,7 +96,7 @@ def open(cls) -> Iterator[LifecycleRun]: directory = worker.storage_roots.scratch / "commander-parity" / uuid4().hex with CheckRoot(source, (config.commander, worker.name), directory).open() as copied: root = copied.root - commander = Commander( + commander = MeasuredCommander( commander=config.commander, root=root, cli=config.cli, state=root / "commander-state", board=root / "board.md", narrative=root / "narrative.md", repos=config.repos, @@ -139,33 +162,16 @@ def checks(cls, run: LifecycleRun, rows: tuple[Todo, ...], @classmethod def read(cls) -> ParityComparison: owners = (cls.native_owner, *cls.additional_owners) - codes = set() - for owner in owners: - function = owner.apply - if not isinstance(function, FunctionType): - raise TypeError("A native tool must declare its executable apply method") - codes.add(function.__code__) - native: list[ToolResult] = [] - - def observe(frame: FrameType, event: str, value: object) -> None: - if event == "return" and frame.f_code in codes: - request = frame.f_locals["self"] - if type(request) in owners and isinstance(value, ToolResult): - native.append(value) - with LifecycleRun.open() as run: - previous = sys.getprofile() output = io.StringIO() - sys.setprofile(observe) - try: - with contextlib.redirect_stdout(output): - rows = cls.exercise(run) - finally: - sys.setprofile(previous) + with contextlib.redirect_stdout(output): + rows = cls.exercise(run) + native = tuple(invocation.result for invocation in run.commander.invocations + if type(invocation.request) in owners) text = output.getvalue() return LifecycleObservation( ", ".join(f"{owner.__module__}.{owner.__qualname__}" for owner in owners), len(native), - cls.checks(run, rows, tuple(native), text), text) + cls.checks(run, rows, native, text), text) class CreateProblemCase(LifecycleCase): @@ -237,6 +243,16 @@ def checks(cls, run: LifecycleRun, rows: tuple[Todo, ...], ("current close printed", f"{rows[-1].id} closed" in output)) +class BuildBoundProofCase(InstalledEvidenceCase): + @classmethod + def read(cls) -> ParityComparison: + closing = super().read() + return UnverifiedParity( + "Missing native owner: a build-bound application-run record linking the " + "runtime and Core/Toad revisions to closing evidence. The actual close " + "owner currently accepts a bare artifact path. " + closing.describe()) + + class StrikeLimitCase(LifecycleCase): native_owner = CommsChangeProblemTool additional_owners = (CommsFireTool,) @@ -315,24 +331,17 @@ def describe(self) -> str: def verify_order_progress() -> ParityComparison: - context = Commander.read(CommanderConfig.path()) - observed: dict[str, str] = {} - merged: list[bool] = [] - - def observe(frame: FrameType, event: str, value: object) -> None: - if event == "return" and frame.f_code is OrderProgress.read.__func__.__code__: - todo = frame.f_locals["todo"] - if not isinstance(todo, Todo) or not isinstance(value, OrderProgress): - raise TypeError("The native progress reader returned an unexpected record") - observed[todo.id] = value.line - if isinstance(value, MergedProgress): - merged.append(not value.stalled(todo.order.checked().at.recorded_at("An order"), time.time())) - - previous = sys.getprofile() - sys.setprofile(observe) - try: - current = {key: value.line for key, value in context.progress().items()} - finally: - sys.setprofile(previous) + context = MeasuredCommander.read(CommanderConfig.path()) + current = {key: value.line for key, value in context.progress().items()} + results = tuple(invocation.result for invocation in context.invocations + if type(invocation.request) is CommsTodoListTool) + if len(results) != 1 or not isinstance(results[0], TodoList): + return UnverifiedParity("Current progress did not return one native TodoList measurement") + result = results[0] + observed = {key: value.line for key, value in result.progress.items()} + merged = tuple(not result.progress[todo.id].stalled( + todo.order.checked().at.recorded_at("An order"), time.time()) + for todo in result.todos if todo.id in result.progress + and isinstance(result.progress[todo.id], MergedProgress)) return ProgressComparison(current, observed, NoCommitProgress.seconds, Moving.seconds, - tuple(merged), OrderProgressMutationCase.read()) + merged, OrderProgressMutationCase.read()) diff --git a/src/agent_comms/commander_parity/rows.py b/src/agent_comms/commander_parity/rows.py index aed40ce0be..0595f31cd0 100644 --- a/src/agent_comms/commander_parity/rows.py +++ b/src/agent_comms/commander_parity/rows.py @@ -13,7 +13,7 @@ from . import ParityComparison, ParityRow, UnverifiedParity from .load import LoadComparison from .measurements import RuleComparison, StorageComparison, measured_load -from .lifecycle import AssignOrderCase, CreateProblemCase, InstalledEvidenceCase, StrikeLimitCase, verify_order_progress +from .lifecycle import AssignOrderCase, BuildBoundProofCase, CreateProblemCase, InstalledEvidenceCase, StrikeLimitCase, verify_order_progress class CreateProblemParityRow(ParityRow): @@ -295,6 +295,10 @@ class BuildBoundProofParityRow(ParityRow): native = "Build run records runtime and Core/Toad revisions; close requires delivered commit" phases = (RecordPlanePhase,) + @classmethod + def verify(cls) -> ParityComparison: + return BuildBoundProofCase.read() + class DeletionTargetParityRow(ParityRow): number = 31 diff --git a/tests/test_candidate_maintenance.py b/tests/test_candidate_maintenance.py index 645154eaf8..2bde1e99f0 100644 --- a/tests/test_candidate_maintenance.py +++ b/tests/test_candidate_maintenance.py @@ -53,15 +53,15 @@ def test_ordinary_private_send_deferred_index_catches_up_without_wake_authority( _SCHEDULE, ) indexed = threading.Event() - original = maintenance._drain_candidate + original = maintenance.CandidateMaintenance.run - def observe(root): - original(root) + def observe(worker): + original(worker) # Candidate catch-up alone is not completion: the same worker # still owns route preparation and its final pending-root check. indexed.set() - monkeypatch.setattr(maintenance, "_drain_candidate", observe) + monkeypatch.setattr(maintenance.CandidateMaintenance, "run", observe) message = comms.messaging.send("sender", "beta", "selected original").message assert indexed.wait(timeout=5), "deferred checkpoint did not catch up" page = WakeCandidateIndex(comms.bus.log).page( @@ -111,14 +111,15 @@ def probe(): def test_failed_drain_is_named_and_retires_pending_state(monkeypatch, tmp_path, caplog) -> None: comms, _root_id = _wire(tmp_path) root = comms.bus.log.path.parent - maintenance._pending[root] = (comms.bus.log, 1) + worker = maintenance.CandidateMaintenance(comms.bus.log, 1) + maintenance._pending[root] = worker def fail(*args, **kwargs): raise ValueError("synthetic index failure") monkeypatch.setattr(WakeCandidateIndex, "catch_up_committed_append", fail) with pytest.raises(maintenance.CandidateMaintenanceFailed) as failed: - maintenance._drain_candidate(root) + worker.run() assert failed.value.root == root assert root not in maintenance._pending assert "Candidate maintenance failed for" in caplog.text @@ -152,8 +153,9 @@ def test_one_background_drain_catches_up_a_backlog_of_oversized_records( ).checkpoint_bytes == first_bytes through_bytes = index.log.path.stat().st_size hint = index.notify_committed_append(root_id=root_id, through_bytes=through_bytes) - maintenance._pending[index.log.path.parent] = (index.log, through_bytes) - maintenance._drain_candidate(comms.bus.log.path.parent) + worker = maintenance.CandidateMaintenance(index.log, through_bytes) + maintenance._pending[index.log.path.parent] = worker + worker.run() assert not maintenance._pending assert index.catch_up_committed_append(hint).caught_up page = index.page( @@ -172,7 +174,7 @@ def test_source_replacement_during_replay_does_not_kill_the_worker(monkeypatch, replay = WakeCandidateIndex._replay_prefix replaced, finished = threading.Event(), threading.Condition() failures = [] - drain = maintenance._drain_candidate + drain = maintenance.CandidateMaintenance.run def replace_once(cls, source, *args, **kwargs): batch = replay(source, *args, **kwargs) @@ -185,9 +187,9 @@ def replace_once(cls, source, *args, **kwargs): replaced.set() return batch - def observed(path): + def observed(worker): try: - drain(path) + drain(worker) except Exception as error: failures.append(error) finally: @@ -199,7 +201,7 @@ def retired(): return root not in maintenance._pending monkeypatch.setattr(WakeCandidateIndex, "_replay_prefix", classmethod(replace_once)) - monkeypatch.setattr(maintenance, "_drain_candidate", observed) + monkeypatch.setattr(maintenance.CandidateMaintenance, "run", observed) monkeypatch.setattr(maintenance, "schedule_candidate_catchup", _SCHEDULE) comms.messaging.send("sender", "beta", "first original") assert replaced.wait(5) @@ -215,11 +217,11 @@ def test_concurrent_ordinary_publication_keeps_maintenance_alive(monkeypatch, tm root = comms.bus.log.path.parent finished = threading.Condition() failures = [] - drain = maintenance._drain_candidate + drain = maintenance.CandidateMaintenance.run - def observed_drain(path): + def observed_drain(worker): try: - drain(path) + drain(worker) except Exception as error: failures.append(error) raise @@ -231,7 +233,7 @@ def retired(): with maintenance._guard: return root not in maintenance._pending - monkeypatch.setattr(maintenance, "_drain_candidate", observed_drain) + monkeypatch.setattr(maintenance.CandidateMaintenance, "run", observed_drain) monkeypatch.setattr(maintenance, "schedule_candidate_catchup", _SCHEDULE) messages = [comms.messaging.send("sender", "beta", str(n) + "x" * (128 * 1024)).message for n in range(128)] diff --git a/tests/test_candidate_publication_notifications.py b/tests/test_candidate_publication_notifications.py index 7c761dda2d..39d1a8a15a 100644 --- a/tests/test_candidate_publication_notifications.py +++ b/tests/test_candidate_publication_notifications.py @@ -65,11 +65,11 @@ def test_nested_release_scope_runs_only_after_outer_release(): def notifications(monkeypatch): def activate(comms): calls, finished = [], threading.Event() - drain = candidate_maintenance._drain_candidate + drain = candidate_maintenance.CandidateMaintenance.run - def complete(root): + def complete(worker): try: - drain(root) + drain(worker) finally: finished.set() @@ -94,7 +94,7 @@ def probe(): calls.append(through_bytes) _SCHEDULE(log, through_bytes) - monkeypatch.setattr(candidate_maintenance, "_drain_candidate", complete) + monkeypatch.setattr(candidate_maintenance.CandidateMaintenance, "run", complete) monkeypatch.setattr(candidate_maintenance, "schedule_candidate_catchup", notify) return calls, finished @@ -250,7 +250,8 @@ def test_retiring_worker_does_not_delete_successor_notification(tmp_path, monkey publish_initial(comms, "sender", "member000", "first") assert index.maintain(start=RebuildIndex()) root = index.log.path.parent - candidate_maintenance._pending[root] = (index.log, index.log.path.stat().st_size) + worker = candidate_maintenance.CandidateMaintenance(index.log, index.log.path.stat().st_size) + candidate_maintenance._pending[root] = worker original_guard = candidate_maintenance._guard successor = [] @@ -269,11 +270,11 @@ def __exit__(self, *_): monkeypatch.setattr(candidate_maintenance, "_guard", InterleavedLock()) monkeypatch.setattr(candidate_maintenance, "schedule_candidate_catchup", _SCHEDULE) monkeypatch.setattr(threading.Thread, "start", lambda _: None) - candidate_maintenance._drain_candidate(root) + worker.run() assert successor - assert candidate_maintenance._pending[root][1] == index.log.path.stat().st_size + assert candidate_maintenance._pending[root].through_bytes == index.log.path.stat().st_size # Complete the retained successor without launching another daemon. - candidate_maintenance._drain_candidate(root) + candidate_maintenance._pending[root].run() assert root not in candidate_maintenance._pending assert index._verified_checkpoint(root_id).last_seq == 2 finally: From 9a74e86c0e26c5937ce773d4beef41372937c75a Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:33:28 -0400 Subject: [PATCH 24/66] Let parity declarations own their comparison callable --- src/agent_comms/commander_parity/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/agent_comms/commander_parity/__init__.py b/src/agent_comms/commander_parity/__init__.py index 22073f86a4..efe7d431bc 100644 --- a/src/agent_comms/commander_parity/__init__.py +++ b/src/agent_comms/commander_parity/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import Iterable +from collections.abc import Callable, Iterable from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, ClassVar @@ -43,6 +43,7 @@ class ParityRow( current: ClassVar[str] native: ClassVar[str] phases: ClassVar[tuple[type[PlanPhase], ...]] + comparison: ClassVar[Callable[[], ParityComparison]] = UnverifiedParity @classmethod def compare(cls, todos: Iterable[Todo]) -> ParityComparison: @@ -51,7 +52,7 @@ def compare(cls, todos: Iterable[Todo]) -> ParityComparison: @classmethod def verify(cls) -> ParityComparison: """Run this declared workflow now; a run is not durable cutover authority.""" - return UnverifiedParity() + return cls.comparison() @classmethod def rows(cls) -> tuple[type[ParityRow], ...]: From 049e8017a04e2b34cac04d68e58f41b6d813c531 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:34:34 -0400 Subject: [PATCH 25/66] Declare comparisons once and keep lifecycle observations typed at their owners --- src/agent_comms/candidate_maintenance.py | 14 +- src/agent_comms/check_root.py | 2 +- src/agent_comms/commander_parity/lifecycle.py | 352 ++++++++++++------ src/agent_comms/commander_parity/rows.py | 33 +- 4 files changed, 274 insertions(+), 127 deletions(-) diff --git a/src/agent_comms/candidate_maintenance.py b/src/agent_comms/candidate_maintenance.py index ddd4e8e424..9a0fb16953 100644 --- a/src/agent_comms/candidate_maintenance.py +++ b/src/agent_comms/candidate_maintenance.py @@ -10,8 +10,8 @@ from .bus_route_counts import BusRouteCounts from .coordination_errors import CoordinationError from .routing import DeliveryMessage -from .wire_log import CurrentSnapshotOpening, WireLog from .wake_candidate_index import FirstBuild, ProjectionUnavailableError, WakeCandidateIndex +from .wire_log import CurrentSnapshotOpening, WireLog _LOG = logging.getLogger(__name__) _MAX_ACTIVE_ROOTS = 32 @@ -51,14 +51,22 @@ def run(self) -> None: with self.log.opened_wire_snapshot(opening=CurrentSnapshotOpening()) as snapshot: route_result = routes.sync( snapshot, - lambda record: DeliveryMessage.from_wire(record, root_id=snapshot.metadata.root_id), + lambda record: DeliveryMessage.from_wire( + record, root_id=snapshot.metadata.root_id + ), ) if result.caught_up and route_result.complete: with _guard: if self.through_bytes <= result.checkpoint_bytes: _pending.pop(root) return - except (OSError, ValueError, sqlite3.Error, CoordinationError, ProjectionUnavailableError) as error: + except ( + OSError, + ValueError, + sqlite3.Error, + CoordinationError, + ProjectionUnavailableError, + ) as error: failure = CandidateMaintenanceFailed(root) _LOG.exception("%s", failure) raise failure from error diff --git a/src/agent_comms/check_root.py b/src/agent_comms/check_root.py index b261755d01..7ace5f24d1 100644 --- a/src/agent_comms/check_root.py +++ b/src/agent_comms/check_root.py @@ -12,10 +12,10 @@ from typing import ClassVar from .attempt_recovery import RecoveryMonitorCapability +from .candidate_maintenance import drain_candidate_catchup from .child_process import NoProcess from .cli_result import CliResult from .comms import Comms -from .candidate_maintenance import drain_candidate_catchup from .fresh_private_session import create_fresh_private_session from .goals import NoGoal from .launch_runtime import RestartEnvironment, ThreadLaunch diff --git a/src/agent_comms/commander_parity/lifecycle.py b/src/agent_comms/commander_parity/lifecycle.py index 4309985644..e6309cd30a 100644 --- a/src/agent_comms/commander_parity/lifecycle.py +++ b/src/agent_comms/commander_parity/lifecycle.py @@ -26,16 +26,29 @@ from ..comms import Comms from ..field_codec import FieldCodec from ..goals import CountCheckResult, OrderCheck, ProblemGoalOrder +from ..registry_document import RosterVisibility +from ..todos import ( + ApplicationOrderRun, + MergedProgress, + Moving, + NoCommitProgress, + PendingProblemOrder, + Todo, +) from ..tool_caller import ThreadCaller, ToolCaller from ..tool_result import ToolResult from ..tools import ( - CommsChangeProblemTool, CommsFireTool, CommsOrderTool, - CommsProblemCloseTool, CommsTodoCreateTool, CommsTodoListTool, ProblemOrdered, - ThreadToolRequest, TodoChanged, TodoList, ToolRequest, -) -from ..todos import ( - ApplicationOrderRun, MergedProgress, Moving, NoCommitProgress, - PendingProblemOrder, Todo, + CommsChangeProblemTool, + CommsFireTool, + CommsOrderTool, + CommsProblemCloseTool, + CommsTodoCreateTool, + CommsTodoListTool, + ProblemOrdered, + ThreadToolRequest, + TodoChanged, + TodoList, + ToolRequest, ) from . import ParityComparison, UnverifiedParity @@ -52,7 +65,10 @@ def passed(self) -> bool: return bool(self.invocations and self.checks) and all(value for _, value in self.checks) def describe(self) -> str: - return f"owner={self.owner}; calls={self.invocations}; checks={dict(self.checks)}; output={self.output!r}" + return ( + f"owner={self.owner}; calls={self.invocations}; " + f"checks={dict(self.checks)}; output={self.output!r}" + ) @dataclass(frozen=True) @@ -71,7 +87,7 @@ def invocations(self) -> tuple[NativeInvocation, ...]: return tuple(self._invocations) def call(self, tool: ToolRequest[ThreadCaller]) -> ToolResult: - result = super().call(tool) + result: ToolResult = super().call(tool) self._invocations.append(NativeInvocation(tool, result)) return result @@ -89,41 +105,83 @@ class LifecycleRun: def open(cls) -> Iterator[LifecycleRun]: config = CommanderConfig.read(CommanderConfig.path()) source = Comms(config.root) - actor = ToolCaller.from_actor(os.environ.get("AGENT_COMMS_THREAD")).acting_name(config.commander) - worker = source.registry.require(actor) - if worker.name == config.commander: - raise ValueError("Lifecycle comparison requires an existing unit caller, not its commander") - directory = worker.storage_roots.scratch / "commander-parity" / uuid4().hex + actor = ToolCaller.from_actor(os.environ.get("AGENT_COMMS_THREAD")).acting_name( + config.commander + ) + caller = source.registry.require(actor) + candidates = tuple( + thread + for thread in source.registry.snapshot().visible_threads(RosterVisibility()) + if thread.parent == config.commander and thread.name != config.commander + ) + if not candidates: + raise ValueError("Lifecycle comparison requires an existing registered unit") + worker = ( + caller + if actor != config.commander + else sorted(candidates, key=lambda thread: thread.name)[0] + ) + directory = caller.storage_roots.scratch / "commander-parity" / uuid4().hex with CheckRoot(source, (config.commander, worker.name), directory).open() as copied: root = copied.root commander = MeasuredCommander( - commander=config.commander, root=root, cli=config.cli, - state=root / "commander-state", board=root / "board.md", - narrative=root / "narrative.md", repos=config.repos, - worktrees=root / "projects", scratch=root / "scratch", disk=root, + commander=config.commander, + root=root, + cli=config.cli, + state=root / "commander-state", + board=root / "board.md", + narrative=root / "narrative.md", + repos=config.repos, + worktrees=root / "projects", + scratch=root / "scratch", + disk=root, ) with contextlib.redirect_stdout(io.StringIO()): - RequestAddVerb(text=( - "Compare current order commands with their real native owners in this " - "disposable copied bus, preserving the source bus and its sessions.")).run(commander) + RequestAddVerb( + text=( + "Compare current order commands with their real native owners in this " + "disposable copied bus, preserving the source bus and its sessions." + ) + ).run(commander) decisions = root / "decisions.json" decisions.write_text(json.dumps([worker.name])) - command = shlex.join((sys.executable, "-c", - "import json;from pathlib import Path;" - f"print(len(json.loads(Path({str(decisions)!r}).read_text())))")) + command = shlex.join( + ( + sys.executable, + "-c", + "import json;from pathlib import Path;" + f"print(len(json.loads(Path({str(decisions)!r}).read_text())))", + ) + ) order = PendingProblemOrder( owner=FileLine(str(Path(__file__).resolve()), 1), - check=OrderCheck(str(Path(__file__).resolve().parents[3]), command, CountCheckResult(0)), - edits=("Remove the copied workflow decision and verify its native order completes.",), + check=OrderCheck( + str(Path(__file__).resolve().parents[3]), command, CountCheckResult(0) + ), + edits=( + "Remove the copied workflow decision and verify its native order completes.", + ), banned=("Do not change the source bus or its saved sessions.",), report="Report the actual copied goal and native order state.", run=ApplicationOrderRun( - "Read the copied native goal and confirm its recorded source location and assigned worker."), + "Read the copied native goal and confirm its recorded source location " + "and assigned worker." + ), ) document = root / "order.json" - document.write_text(json.dumps(FieldCodec.encode(OrderFile( - order, "The copied workflow decision prevents the native completion check from reaching zero.", - "This is stupid because an unfinished copied decision must not count as delivered work.")))) + document.write_text( + json.dumps( + FieldCodec.encode( + OrderFile( + order, + "The copied workflow decision prevents the native completion check " + "from reaching zero.", + "This is stupid because an unfinished copied decision must not count " + "as delivered work.", + ) + ) + ) + ) yield cls(commander, worker.name, decisions, document, root / "application.json") @property @@ -131,8 +189,9 @@ def due(self) -> str: return time.strftime("%H:%M", time.localtime(time.time() + 3600)) def create(self) -> Todo: - self.commander.problem(str(self.document), self.worker, self.due, - f"{Path(__file__).resolve()}:1") + self.commander.problem( + str(self.document), self.worker, self.due, f"{Path(__file__).resolve()}:1" + ) rows = tuple(self.commander.comms.goals.todos.for_commander(self.commander.incarnation)) if len(rows) != 1: raise ValueError(f"A fresh copied workflow must create one problem, got {len(rows)}") @@ -156,8 +215,9 @@ def exercise(cls, run: LifecycleRun) -> tuple[Todo, ...]: ... @classmethod @abstractmethod - def checks(cls, run: LifecycleRun, rows: tuple[Todo, ...], - native: tuple[ToolResult, ...], output: str) -> tuple[tuple[str, bool], ...]: ... + def checks( + cls, run: LifecycleRun, rows: tuple[Todo, ...], native: tuple[ToolResult, ...], output: str + ) -> tuple[tuple[str, bool], ...]: ... @classmethod def read(cls) -> ParityComparison: @@ -166,32 +226,44 @@ def read(cls) -> ParityComparison: output = io.StringIO() with contextlib.redirect_stdout(output): rows = cls.exercise(run) - native = tuple(invocation.result for invocation in run.commander.invocations - if type(invocation.request) in owners) + native = tuple( + invocation.result + for invocation in run.commander.invocations + if isinstance(invocation.request, owners) + ) text = output.getvalue() return LifecycleObservation( - ", ".join(f"{owner.__module__}.{owner.__qualname__}" for owner in owners), len(native), - cls.checks(run, rows, native, text), text) + ", ".join(f"{owner.__module__}.{owner.__qualname__}" for owner in owners), + len(native), + cls.checks(run, rows, native, text), + text, + ) class CreateProblemCase(LifecycleCase): - native_owner = CommsTodoCreateTool + native_owner: ClassVar[type[ThreadToolRequest]] = CommsTodoCreateTool @classmethod def exercise(cls, run: LifecycleRun) -> tuple[Todo, ...]: return (run.create(),) @classmethod - def checks(cls, run: LifecycleRun, rows: tuple[Todo, ...], - native: tuple[ToolResult, ...], output: str) -> tuple[tuple[str, bool], ...]: - if len(native) != 1 or not isinstance(native[0], ProblemOrdered): - return (("one native creation result", False),) - result, saved = native[0], rows[-1] + def checks( + cls, run: LifecycleRun, rows: tuple[Todo, ...], native: tuple[ToolResult, ...], output: str + ) -> tuple[tuple[str, bool], ...]: + if not native or not isinstance(native[-1], ProblemOrdered): + return (("native order result returned", False),) + result, saved = native[-1], rows[-1] goal = run.commander.comms.registry.require(run.worker).goal - return (("printed native identity", output.rstrip().endswith(cls.printed_identity(result))), - ("native source retained", saved.order.checked().owner == result.todo.order.checked().owner), - ("native assignment retained", saved.assignment == result.todo.assignment), - ("native goal published", goal.require().id == result.goal.id)) + return ( + ("printed native identity", output.rstrip().endswith(cls.printed_identity(result))), + ( + "native source retained", + saved.order.checked().owner == result.todo.order.checked().owner, + ), + ("native assignment retained", saved.assignment == result.todo.assignment), + ("native goal published", goal.require().id == result.goal.id), + ) @classmethod def printed_identity(cls, result: ProblemOrdered) -> str: @@ -204,17 +276,23 @@ class AssignOrderCase(CreateProblemCase): @classmethod def exercise(cls, run: LifecycleRun) -> tuple[Todo, ...]: before = run.create() - run.commander.fire(run.worker, "The copied workflow is released before its next native assignment.") + run.commander.fire( + run.worker, "The copied workflow is released before its next native assignment." + ) document = replace(OrderFile.read(run.document.read_text()), problems=(before.id,)) run.commander.send(run.worker, run.due, document, clean=True) return before, run.commander.comms.goals.todos.get(before.id) @classmethod - def checks(cls, run: LifecycleRun, rows: tuple[Todo, ...], - native: tuple[ToolResult, ...], output: str) -> tuple[tuple[str, bool], ...]: + def checks( + cls, run: LifecycleRun, rows: tuple[Todo, ...], native: tuple[ToolResult, ...], output: str + ) -> tuple[tuple[str, bool], ...]: inherited = super().checks(run, rows, native, output) - return (*inherited, ("same problem reassigned", rows[0].id == rows[-1].id), - ("prior assignment retained", bool(rows[-1].released))) + return ( + *inherited, + ("same problem reassigned", rows[0].id == rows[-1].id), + ("prior assignment retained", bool(rows[-1].released)), + ) @classmethod def printed_identity(cls, result: ProblemOrdered) -> str: @@ -228,19 +306,25 @@ class InstalledEvidenceCase(LifecycleCase): def exercise(cls, run: LifecycleRun) -> tuple[Todo, ...]: created = run.create() delivered = run.complete(created) - run.commander.close(created.id, - "Read the copied native goal and confirm its recorded source location and assigned worker.", - str(run.artifact)) + run.commander.close( + created.id, + "Read the copied native goal and confirm its recorded source location " + "and assigned worker.", + str(run.artifact), + ) return delivered, run.commander.comms.goals.todos.get(created.id) @classmethod - def checks(cls, run: LifecycleRun, rows: tuple[Todo, ...], - native: tuple[ToolResult, ...], output: str) -> tuple[tuple[str, bool], ...]: - return (("delivery alone unaccepted", not rows[0].order.accepted), - ("installed evidence accepted", rows[-1].order.accepted), - ("closed in native store", not rows[-1].state.open), - ("actual artifact exists", run.artifact.is_file()), - ("current close printed", f"{rows[-1].id} closed" in output)) + def checks( + cls, run: LifecycleRun, rows: tuple[Todo, ...], native: tuple[ToolResult, ...], output: str + ) -> tuple[tuple[str, bool], ...]: + return ( + ("delivery alone unaccepted", not rows[0].order.accepted), + ("installed evidence accepted", rows[-1].order.accepted), + ("closed in native store", not rows[-1].state.open), + ("actual artifact exists", run.artifact.is_file()), + ("current close printed", f"{rows[-1].id} closed" in output), + ) class BuildBoundProofCase(InstalledEvidenceCase): @@ -250,7 +334,8 @@ def read(cls) -> ParityComparison: return UnverifiedParity( "Missing native owner: a build-bound application-run record linking the " "runtime and Core/Toad revisions to closing evidence. The actual close " - "owner currently accepts a bare artifact path. " + closing.describe()) + "owner currently accepts a bare artifact path. " + closing.describe() + ) class StrikeLimitCase(LifecycleCase): @@ -261,20 +346,37 @@ class StrikeLimitCase(LifecycleCase): def exercise(cls, run: LifecycleRun) -> tuple[Todo, ...]: created = run.create() for number in range(ProblemGoalOrder.StrikeLimit): - run.commander.strike(run.worker, created.id, - f"The copied decision remains unfinished at its recorded review number {number + 1}.") - run.commander.fire(run.worker, - "The copied holder exhausted its declared live strikes on this problem.") + run.commander.strike( + run.worker, + created.id, + "The copied decision remains unfinished at its recorded review " + f"number {number + 1}.", + ) + run.commander.fire( + run.worker, "The copied holder exhausted its declared live strikes on this problem." + ) return created, run.commander.comms.goals.todos.get(created.id) @classmethod - def checks(cls, run: LifecycleRun, rows: tuple[Todo, ...], - native: tuple[ToolResult, ...], output: str) -> tuple[tuple[str, bool], ...]: + def checks( + cls, run: LifecycleRun, rows: tuple[Todo, ...], native: tuple[ToolResult, ...], output: str + ) -> tuple[tuple[str, bool], ...]: saved = rows[-1] - return (("declared limit recorded", saved.strike_count == ProblemGoalOrder.StrikeLimit), - ("one native fire", sum(isinstance(result, TodoList) for result in native) == 1), - ("assignment released", saved.holder is None), - ("current strike count printed", f"{ProblemGoalOrder.StrikeLimit} strike(s)" in output)) + return ( + ("declared limit recorded", saved.strike_count == ProblemGoalOrder.StrikeLimit), + ("one native fire", sum(isinstance(result, TodoList) for result in native) == 1), + ( + "assignment released", + saved.id + not in { + todo.id + for todo in run.commander.comms.goals.todos.held_for_commander( + run.commander.incarnation + ) + }, + ), + ("current strike count printed", f"{ProblemGoalOrder.StrikeLimit} strike(s)" in output), + ) class OrderProgressMutationCase(LifecycleCase): @@ -284,29 +386,49 @@ class OrderProgressMutationCase(LifecycleCase): def exercise(cls, run: LifecycleRun) -> tuple[Todo, ...]: before = run.create() moved = time.strftime("%H:%M", time.localtime(time.time() + 7200)) - run.commander.due(before.id, moved, - "The copied workflow needs time to record its actual native application evidence.") + run.commander.due( + before.id, + moved, + "The copied workflow needs time to record its actual native application evidence.", + ) return before, run.commander.comms.goals.todos.get(before.id) @classmethod - def checks(cls, run: LifecycleRun, rows: tuple[Todo, ...], - native: tuple[ToolResult, ...], output: str) -> tuple[tuple[str, bool], ...]: + def checks( + cls, run: LifecycleRun, rows: tuple[Todo, ...], native: tuple[ToolResult, ...], output: str + ) -> tuple[tuple[str, bool], ...]: before, after = rows messages = run.commander.comms.bus.inbox(run.worker) unit = run.commander.comms.registry.require(run.worker) goal = unit.goal.require() - instructions = goal.order.instructions(goal.order.todos( - run.commander.comms.goals, unit.incarnation, goal.id)) - native_deadlines = tuple(result.todo.deadline for result in native if isinstance(result, TodoChanged)) - return (("native deadline retained", bool(native_deadlines) and native_deadlines[-1] == after.deadline), - ("deadline actually changed", before.deadline != after.deadline), - ("assignment unchanged", before.assignment == after.assignment), - ("unit got the actual update", bool(messages)), - ("unit messages hide clocks", all(hhmm(clock) not in message.body - for clock in (before.deadline, after.deadline) for message in messages)), - ("unit goal hides deadline", "Deadline:" not in instructions - and hhmm(after.deadline) not in instructions), - ("commander still sees deadline", f"due {hhmm(after.deadline)}" in output)) + instructions = goal.order.instructions( + goal.order.todos(run.commander.comms.goals, unit.incarnation, goal.id) + ) + native_deadlines = tuple( + result.todo.deadline for result in native if isinstance(result, TodoChanged) + ) + return ( + ( + "native deadline retained", + bool(native_deadlines) and native_deadlines[-1] == after.deadline, + ), + ("deadline actually changed", before.deadline != after.deadline), + ("assignment unchanged", before.assignment == after.assignment), + ("unit got the actual update", bool(messages)), + ( + "unit messages hide clocks", + all( + hhmm(clock) not in message.body + for clock in (before.deadline, after.deadline) + for message in messages + ), + ), + ( + "unit goal hides deadline", + "Deadline:" not in instructions and hhmm(after.deadline) not in instructions, + ), + ("commander still sees deadline", f"due {hhmm(after.deadline)}" in output), + ) @dataclass(frozen=True) @@ -320,28 +442,50 @@ class ProgressComparison(ParityComparison): @property def passed(self) -> bool: - return (self.workflow.passed and bool(self.native) and self.current == self.native - and self.first_seconds == 600 and self.later_seconds == 900 - and bool(self.merged_no_strikes) and all(self.merged_no_strikes)) + return all( + ( + self.workflow.passed, + bool(self.native), + self.current == self.native, + self.first_seconds == 600, + self.later_seconds == 900, + bool(self.merged_no_strikes), + all(self.merged_no_strikes), + ) + ) def describe(self) -> str: - return (f"{self.workflow.describe()}; live_progress={len(self.native)}; " - f"read_equal={self.current == self.native}; intervals={self.first_seconds}/{self.later_seconds}; " - f"live_merged_no_strikes={self.merged_no_strikes}") + return ( + f"{self.workflow.describe()}; live_progress={len(self.native)}; " + f"read_equal={self.current == self.native}; " + f"intervals={self.first_seconds}/{self.later_seconds}; " + f"live_merged_no_strikes={self.merged_no_strikes}" + ) def verify_order_progress() -> ParityComparison: context = MeasuredCommander.read(CommanderConfig.path()) current = {key: value.line for key, value in context.progress().items()} - results = tuple(invocation.result for invocation in context.invocations - if type(invocation.request) is CommsTodoListTool) + results = tuple( + invocation.result + for invocation in context.invocations + if isinstance(invocation.request, CommsTodoListTool) + ) if len(results) != 1 or not isinstance(results[0], TodoList): return UnverifiedParity("Current progress did not return one native TodoList measurement") result = results[0] observed = {key: value.line for key, value in result.progress.items()} - merged = tuple(not result.progress[todo.id].stalled( - todo.order.checked().at.recorded_at("An order"), time.time()) - for todo in result.todos if todo.id in result.progress - and isinstance(result.progress[todo.id], MergedProgress)) - return ProgressComparison(current, observed, NoCommitProgress.seconds, Moving.seconds, - merged, OrderProgressMutationCase.read()) + todos = {todo.id: todo for todo in result.todos} + merged = tuple( + not progress.stalled(todos[pid].order.checked().at.recorded_at("An order"), time.time()) + for pid, progress in result.progress.items() + if isinstance(progress, MergedProgress) + ) + return ProgressComparison( + current, + observed, + NoCommitProgress.seconds, + Moving.seconds, + merged, + OrderProgressMutationCase.read(), + ) diff --git a/src/agent_comms/commander_parity/rows.py b/src/agent_comms/commander_parity/rows.py index 0595f31cd0..169a19bc74 100644 --- a/src/agent_comms/commander_parity/rows.py +++ b/src/agent_comms/commander_parity/rows.py @@ -11,9 +11,16 @@ StoryPhase, ) from . import ParityComparison, ParityRow, UnverifiedParity +from .lifecycle import ( + AssignOrderCase, + BuildBoundProofCase, + CreateProblemCase, + InstalledEvidenceCase, + StrikeLimitCase, + verify_order_progress, +) from .load import LoadComparison from .measurements import RuleComparison, StorageComparison, measured_load -from .lifecycle import AssignOrderCase, BuildBoundProofCase, CreateProblemCase, InstalledEvidenceCase, StrikeLimitCase, verify_order_progress class CreateProblemParityRow(ParityRow): @@ -23,9 +30,7 @@ class CreateProblemParityRow(ParityRow): native = "`CommsTodoCreateTool`" phases = (OneOrderOwnerPhase,) - @classmethod - def verify(cls) -> ParityComparison: - return CreateProblemCase.read() + comparison = CreateProblemCase.read class AssignOrderParityRow(ParityRow): @@ -35,9 +40,7 @@ class AssignOrderParityRow(ParityRow): native = "`CommsOrderTool`, `CommsTodoTool` assignment and release" phases = (OneOrderOwnerPhase,) - @classmethod - def verify(cls) -> ParityComparison: - return AssignOrderCase.read() + comparison = AssignOrderCase.read class DeliveredCommitParityRow(ParityRow): @@ -55,9 +58,7 @@ class InstalledEvidenceParityRow(ParityRow): native = "`CommsProblemCloseTool`" phases = (OneOrderOwnerPhase,) - @classmethod - def verify(cls) -> ParityComparison: - return InstalledEvidenceCase.read() + comparison = InstalledEvidenceCase.read class BoardParityRow(ParityRow): @@ -83,9 +84,7 @@ class StrikeLimitParityRow(ParityRow): native = "`Strike` rows, author-declared `StrikeLimit`, one `comms_fire`" phases = (RecordPlanePhase,) - @classmethod - def verify(cls) -> ParityComparison: - return StrikeLimitCase.read() + comparison = StrikeLimitCase.read class VerdictParityRow(ParityRow): @@ -295,9 +294,7 @@ class BuildBoundProofParityRow(ParityRow): native = "Build run records runtime and Core/Toad revisions; close requires delivered commit" phases = (RecordPlanePhase,) - @classmethod - def verify(cls) -> ParityComparison: - return BuildBoundProofCase.read() + comparison = BuildBoundProofCase.read class DeletionTargetParityRow(ParityRow): @@ -333,9 +330,7 @@ class OrderProgressParityRow(ParityRow): ) phases = (OneOrderOwnerPhase, ControlPlanePhase) - @classmethod - def verify(cls) -> ParityComparison: - return verify_order_progress() + comparison = staticmethod(verify_order_progress) class MultipleCommandersParityRow(ParityRow): From d4e77bd8373d8d854b405908400da440fdc70a14 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:41:01 -0400 Subject: [PATCH 26/66] Select lifecycle units from the caller or canonical held orders, not presentation parents --- src/agent_comms/commander_parity/lifecycle.py | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/agent_comms/commander_parity/lifecycle.py b/src/agent_comms/commander_parity/lifecycle.py index e6309cd30a..627627c9b9 100644 --- a/src/agent_comms/commander_parity/lifecycle.py +++ b/src/agent_comms/commander_parity/lifecycle.py @@ -26,7 +26,6 @@ from ..comms import Comms from ..field_codec import FieldCodec from ..goals import CountCheckResult, OrderCheck, ProblemGoalOrder -from ..registry_document import RosterVisibility from ..todos import ( ApplicationOrderRun, MergedProgress, @@ -109,18 +108,14 @@ def open(cls) -> Iterator[LifecycleRun]: config.commander ) caller = source.registry.require(actor) - candidates = tuple( - thread - for thread in source.registry.snapshot().visible_threads(RosterVisibility()) - if thread.parent == config.commander and thread.name != config.commander - ) - if not candidates: - raise ValueError("Lifecycle comparison requires an existing registered unit") - worker = ( - caller - if actor != config.commander - else sorted(candidates, key=lambda thread: thread.name)[0] - ) + if actor == config.commander: + authority = MeasuredCommander(config=config) + held = source.goals.todos.held_for_commander(authority.incarnation) + if not held: + raise ValueError("Lifecycle comparison requires an existing native order holder") + worker = source.registry.require(held[-1].held().owner) + else: + worker = caller directory = caller.storage_roots.scratch / "commander-parity" / uuid4().hex with CheckRoot(source, (config.commander, worker.name), directory).open() as copied: root = copied.root From f7b3284c59d7f5b70ae6a826dd5ab69d3c6de454 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:44:34 -0400 Subject: [PATCH 27/66] Route native closing through the existing commander invocation owner --- src/agent_comms/commander/order.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/agent_comms/commander/order.py b/src/agent_comms/commander/order.py index e1cc9956cd..3c4d19d81e 100644 --- a/src/agent_comms/commander/order.py +++ b/src/agent_comms/commander/order.py @@ -476,8 +476,7 @@ def problem(self, order_file, unit, due, where, disk=False, def close(self, pid, works, artifact=''): """Core owns closing: the installed evidence, the artifact and the no-open-order rule live on the Todo.""" evidence = artifact or self.comms.goals.todos.get(pid).order.closing_artifact() - ThreadCaller(self.commander).invoke(CommsProblemCloseTool(problem=pid, - works=works, artifact=evidence), self.comms) + self.call(CommsProblemCloseTool(problem=pid, works=works, artifact=evidence)) self.write_board() print(f'{pid} closed') From 2c7ccc864d1a43813c5379df2d5c3f9800136b28 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:48:58 -0400 Subject: [PATCH 28/66] Let the existing lifecycle copier include real receiver threads for transfer comparisons --- src/agent_comms/commander_parity/lifecycle.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/agent_comms/commander_parity/lifecycle.py b/src/agent_comms/commander_parity/lifecycle.py index 627627c9b9..394b6a56c3 100644 --- a/src/agent_comms/commander_parity/lifecycle.py +++ b/src/agent_comms/commander_parity/lifecycle.py @@ -101,7 +101,7 @@ class LifecycleRun: @classmethod @contextlib.contextmanager - def open(cls) -> Iterator[LifecycleRun]: + def open(cls, *, extra_threads: tuple[str, ...] = ()) -> Iterator[LifecycleRun]: config = CommanderConfig.read(CommanderConfig.path()) source = Comms(config.root) actor = ToolCaller.from_actor(os.environ.get("AGENT_COMMS_THREAD")).acting_name( @@ -117,7 +117,8 @@ def open(cls) -> Iterator[LifecycleRun]: else: worker = caller directory = caller.storage_roots.scratch / "commander-parity" / uuid4().hex - with CheckRoot(source, (config.commander, worker.name), directory).open() as copied: + threads = tuple(dict.fromkeys((config.commander, worker.name, *extra_threads))) + with CheckRoot(source, threads, directory).open() as copied: root = copied.root commander = MeasuredCommander( commander=config.commander, From 58280c339cf55250f36312399e475c86f75a8cfa Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:50:14 -0400 Subject: [PATCH 29/66] Read commander authority through its existing configured context instead of an invalid wrapper constructor --- src/agent_comms/commander_parity/lifecycle.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/agent_comms/commander_parity/lifecycle.py b/src/agent_comms/commander_parity/lifecycle.py index 394b6a56c3..4143fe0d3e 100644 --- a/src/agent_comms/commander_parity/lifecycle.py +++ b/src/agent_comms/commander_parity/lifecycle.py @@ -23,7 +23,6 @@ from ..commander.config import CommanderConfig from ..commander.order import Commander, OrderFile, hhmm from ..commander.request import RequestAddVerb -from ..comms import Comms from ..field_codec import FieldCodec from ..goals import CountCheckResult, OrderCheck, ProblemGoalOrder from ..todos import ( @@ -102,15 +101,14 @@ class LifecycleRun: @classmethod @contextlib.contextmanager def open(cls, *, extra_threads: tuple[str, ...] = ()) -> Iterator[LifecycleRun]: - config = CommanderConfig.read(CommanderConfig.path()) - source = Comms(config.root) + config = MeasuredCommander.read(CommanderConfig.path()) + source = config.comms actor = ToolCaller.from_actor(os.environ.get("AGENT_COMMS_THREAD")).acting_name( config.commander ) caller = source.registry.require(actor) if actor == config.commander: - authority = MeasuredCommander(config=config) - held = source.goals.todos.held_for_commander(authority.incarnation) + held = source.goals.todos.held_for_commander(config.incarnation) if not held: raise ValueError("Lifecycle comparison requires an existing native order holder") worker = source.registry.require(held[-1].held().owner) From 8de42d10ce26b2f116ec8b7d56a57133d526ce83 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:53:16 -0400 Subject: [PATCH 30/66] Accept read and write request capabilities in lifecycle comparison declarations --- src/agent_comms/commander_parity/lifecycle.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/agent_comms/commander_parity/lifecycle.py b/src/agent_comms/commander_parity/lifecycle.py index 4143fe0d3e..c47216d63e 100644 --- a/src/agent_comms/commander_parity/lifecycle.py +++ b/src/agent_comms/commander_parity/lifecycle.py @@ -43,7 +43,6 @@ CommsTodoCreateTool, CommsTodoListTool, ProblemOrdered, - ThreadToolRequest, TodoChanged, TodoList, ToolRequest, @@ -200,8 +199,8 @@ def complete(self, todo: Todo) -> Todo: class LifecycleCase(ABC): - native_owner: ClassVar[type[ThreadToolRequest]] - additional_owners: ClassVar[tuple[type[ThreadToolRequest], ...]] = () + native_owner: ClassVar[type[ToolRequest[ThreadCaller]]] + additional_owners: ClassVar[tuple[type[ToolRequest[ThreadCaller]], ...]] = () @classmethod @abstractmethod @@ -235,7 +234,7 @@ def read(cls) -> ParityComparison: class CreateProblemCase(LifecycleCase): - native_owner: ClassVar[type[ThreadToolRequest]] = CommsTodoCreateTool + native_owner: ClassVar[type[ToolRequest[ThreadCaller]]] = CommsTodoCreateTool @classmethod def exercise(cls, run: LifecycleRun) -> tuple[Todo, ...]: From 5adf2ed873468962f2c8b577a199aab3e076b77b Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:57:47 -0400 Subject: [PATCH 31/66] Compare real current and native refusals to hand delivered checked work to another unit --- src/agent_comms/commander_parity/handoff.py | 144 ++++++++++++++++++ src/agent_comms/commander_parity/lifecycle.py | 12 ++ src/agent_comms/commander_parity/rows.py | 3 + 3 files changed, 159 insertions(+) create mode 100644 src/agent_comms/commander_parity/handoff.py diff --git a/src/agent_comms/commander_parity/handoff.py b/src/agent_comms/commander_parity/handoff.py new file mode 100644 index 0000000000..70b951ec78 --- /dev/null +++ b/src/agent_comms/commander_parity/handoff.py @@ -0,0 +1,144 @@ +"""Actual current/native refusals to transfer delivered checked work.""" + +from __future__ import annotations + +import contextlib +import io +import json +import subprocess +from dataclasses import dataclass, replace +from pathlib import Path +from uuid import uuid4 + +from ..change_report import FileLine +from ..commander.config import CommanderConfig +from ..commander.order import OrderFile, deadline +from ..field_codec import FieldCodec +from ..goal_actions import OrderGoalAction +from ..tools import CommsOrderTool +from . import ParityComparison, UnverifiedParity +from .lifecycle import LifecycleRun, MeasuredCommander + + +@dataclass(frozen=True) +class HandoffComparison(ParityComparison): + cases: tuple[tuple[str, int, int, tuple[str, ...], tuple[str, ...], bool], ...] + + @property + def passed(self) -> bool: + return len(self.cases) == 2 and all( + all((expected != 0, actual != 0, bool(reference), reference == refusal, unchanged)) + for _, expected, actual, reference, refusal, unchanged in self.cases + ) + + def describe(self) -> str: + return f"actual finished-order transfer refusals (current/native): {self.cases!r}" + + +class FinishedHandoffCase: + @staticmethod + def state(run: LifecycleRun, pid: str, receiver: str) -> str: + return json.dumps( + FieldCodec.encode( + ( + run.commander.comms.goals.todos.get(pid), + run.commander.comms.registry.require(run.worker).goal, + run.commander.comms.registry.require(receiver).goal, + ) + ), + sort_keys=True, + ) + + @staticmethod + def current( + run: LifecycleRun, receiver: str, document: OrderFile, pid: str + ) -> subprocess.CompletedProcess[str]: + return run.invoke( + "import sys; from agent_comms.commander.order import Commander, OrderFile; " + "Commander.model_validate_json(sys.argv[1]).send(sys.argv[2], sys.argv[3], " + "OrderFile.read(sys.argv[4]), reassign=sys.argv[5], traced=sys.argv[6])", + run.commander.model_dump_json(), + receiver, + run.due, + json.dumps(FieldCodec.encode(document)), + "Compare the actual refusal to hand completed checked work to another real unit.", + f"{Path(__file__).resolve()}:1", + ) + + @staticmethod + def native( + run: LifecycleRun, receiver: str, document: OrderFile, pid: str + ) -> subprocess.CompletedProcess[str]: + context = run.commander + todo = context.comms.goals.todos.get(pid) + tool = CommsOrderTool( + owner=receiver, + order=OrderGoalAction( + problem=pid, + expected_revision=todo.revision, + assigner=context.incarnation, + order=document.order, + deadline=deadline(run.due), + commander=context.commander, + request=document.order.request( + context.comms.registry.require(context.commander), (pid,) + ), + traced=FileLine.from_wire(f"{Path(__file__).resolve()}:1"), + ), + ) + return run.invoke( + "from pathlib import Path; import sys; from agent_comms.comms import Comms; " + "from agent_comms.field_codec import FieldCodec; " + "from agent_comms.tool_caller import ThreadCaller; " + "from agent_comms.tools import CommsOrderTool; " + "ThreadCaller(sys.argv[2]).invoke(FieldCodec.decode(CommsOrderTool, " + "FieldCodec.parse(sys.argv[3])), Comms(Path(sys.argv[1])))", + str(context.root), + context.commander, + json.dumps(FieldCodec.encode(tool)), + ) + + @classmethod + def read(cls) -> ParityComparison: + authority = MeasuredCommander.read(CommanderConfig.path()) + owners = tuple( + dict.fromkeys( + todo.held().owner + for todo in authority.comms.goals.todos.held_for_commander(authority.incarnation) + ) + )[-2:] + if len(owners) < 2: + return UnverifiedParity("Finished-work transfer needs two real registered unit holders") + cases = [] + for label, execute in (("current", cls.current), ("native", cls.native)): + with LifecycleRun.open(extra_threads=owners) as run: + with contextlib.redirect_stdout(io.StringIO()): + receiver = next(owner for owner in owners if owner != run.worker) + delivered = run.complete(run.create()) + before = cls.state(run, delivered.id, receiver) + document = OrderFile.read(run.document.read_text()) + document = replace( + document, + problems=(delivered.id,), + order=replace(document.order, id=uuid4().hex), + ) + run.decisions.write_text(json.dumps([receiver])) + reference = run.invoke( + "from pathlib import Path; import sys; from agent_comms.comms import Comms; " + "todo=Comms(Path(sys.argv[1])).goals.todos.get(sys.argv[2]); " + "todo.order.require_transfer(todo)", + str(run.commander.root), + delivered.id, + ) + attempt = execute(run, receiver, document, delivered.id) + cases.append( + ( + label, + reference.returncode, + attempt.returncode, + tuple(reference.stderr.splitlines()[-1:]), + tuple(attempt.stderr.splitlines()[-1:]), + before == cls.state(run, delivered.id, receiver), + ) + ) + return HandoffComparison(tuple(cases)) diff --git a/src/agent_comms/commander_parity/lifecycle.py b/src/agent_comms/commander_parity/lifecycle.py index c47216d63e..4d20dda45a 100644 --- a/src/agent_comms/commander_parity/lifecycle.py +++ b/src/agent_comms/commander_parity/lifecycle.py @@ -7,6 +7,7 @@ import json import os import shlex +import subprocess import sys import time from abc import ABC, abstractmethod @@ -177,6 +178,17 @@ def open(cls, *, extra_threads: tuple[str, ...] = ()) -> Iterator[LifecycleRun]: ) yield cls(commander, worker.name, decisions, document, root / "application.json") + @staticmethod + def invoke(program: str, *arguments: str) -> subprocess.CompletedProcess[str]: + source = str(Path(__file__).resolve().parents[2]) + return subprocess.run( + (sys.executable, "-c", program, *arguments), + capture_output=True, + text=True, + timeout=120, + env={**os.environ, "PYTHONPATH": source}, + ) + @property def due(self) -> str: return time.strftime("%H:%M", time.localtime(time.time() + 3600)) diff --git a/src/agent_comms/commander_parity/rows.py b/src/agent_comms/commander_parity/rows.py index 169a19bc74..97ab0b9321 100644 --- a/src/agent_comms/commander_parity/rows.py +++ b/src/agent_comms/commander_parity/rows.py @@ -11,6 +11,7 @@ StoryPhase, ) from . import ParityComparison, ParityRow, UnverifiedParity +from .handoff import FinishedHandoffCase from .lifecycle import ( AssignOrderCase, BuildBoundProofCase, @@ -286,6 +287,8 @@ class FinishedWorkTransferParityRow(ParityRow): native = "Transfer refuses delivered checked work; that owner merges; check is reused" phases = (RecordPlanePhase,) + comparison = FinishedHandoffCase.read + class BuildBoundProofParityRow(ParityRow): number = 30 From c474d9f9560201471e915265b3d083bc283ed1a0 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:58:30 -0400 Subject: [PATCH 32/66] Admit both read and thread-only tools in lifecycle observations --- src/agent_comms/commander_parity/lifecycle.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/agent_comms/commander_parity/lifecycle.py b/src/agent_comms/commander_parity/lifecycle.py index 4d20dda45a..0e633e1bf0 100644 --- a/src/agent_comms/commander_parity/lifecycle.py +++ b/src/agent_comms/commander_parity/lifecycle.py @@ -69,9 +69,12 @@ def describe(self) -> str: ) +type LifecycleRequest = ToolRequest[ToolCaller] | ToolRequest[ThreadCaller] + + @dataclass(frozen=True) class NativeInvocation: - request: ToolRequest[ThreadCaller] + request: LifecycleRequest result: ToolResult @@ -84,7 +87,7 @@ class MeasuredCommander(Commander): def invocations(self) -> tuple[NativeInvocation, ...]: return tuple(self._invocations) - def call(self, tool: ToolRequest[ThreadCaller]) -> ToolResult: + def call(self, tool: LifecycleRequest) -> ToolResult: result: ToolResult = super().call(tool) self._invocations.append(NativeInvocation(tool, result)) return result @@ -211,8 +214,8 @@ def complete(self, todo: Todo) -> Todo: class LifecycleCase(ABC): - native_owner: ClassVar[type[ToolRequest[ThreadCaller]]] - additional_owners: ClassVar[tuple[type[ToolRequest[ThreadCaller]], ...]] = () + native_owner: ClassVar[type[LifecycleRequest]] + additional_owners: ClassVar[tuple[type[LifecycleRequest], ...]] = () @classmethod @abstractmethod From 47ac4054206d80fffb0dc27101ebf5d277d7924e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 20 Oct 2026 00:02:41 -0400 Subject: [PATCH 33/66] Let BoundedRun own native comparison process outcomes and retirement for all argv forms --- src/agent_comms/commander_parity/handoff.py | 32 +++++++++++-------- src/agent_comms/commander_parity/lifecycle.py | 21 +++++++----- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/agent_comms/commander_parity/handoff.py b/src/agent_comms/commander_parity/handoff.py index 70b951ec78..2507f40859 100644 --- a/src/agent_comms/commander_parity/handoff.py +++ b/src/agent_comms/commander_parity/handoff.py @@ -5,12 +5,12 @@ import contextlib import io import json -import subprocess from dataclasses import dataclass, replace from pathlib import Path from uuid import uuid4 from ..change_report import FileLine +from ..child_process import ChildOutcome, ChildResult, ExitedOutcome from ..commander.config import CommanderConfig from ..commander.order import OrderFile, deadline from ..field_codec import FieldCodec @@ -22,12 +22,22 @@ @dataclass(frozen=True) class HandoffComparison(ParityComparison): - cases: tuple[tuple[str, int, int, tuple[str, ...], tuple[str, ...], bool], ...] + cases: tuple[ + tuple[str, ChildOutcome, ChildOutcome, tuple[str, ...], tuple[str, ...], bool], ... + ] @property def passed(self) -> bool: return len(self.cases) == 2 and all( - all((expected != 0, actual != 0, bool(reference), reference == refusal, unchanged)) + all( + ( + expected == ExitedOutcome(1), + actual == ExitedOutcome(1), + bool(reference), + reference == refusal, + unchanged, + ) + ) for _, expected, actual, reference, refusal, unchanged in self.cases ) @@ -50,9 +60,7 @@ def state(run: LifecycleRun, pid: str, receiver: str) -> str: ) @staticmethod - def current( - run: LifecycleRun, receiver: str, document: OrderFile, pid: str - ) -> subprocess.CompletedProcess[str]: + def current(run: LifecycleRun, receiver: str, document: OrderFile, pid: str) -> ChildResult: return run.invoke( "import sys; from agent_comms.commander.order import Commander, OrderFile; " "Commander.model_validate_json(sys.argv[1]).send(sys.argv[2], sys.argv[3], " @@ -66,9 +74,7 @@ def current( ) @staticmethod - def native( - run: LifecycleRun, receiver: str, document: OrderFile, pid: str - ) -> subprocess.CompletedProcess[str]: + def native(run: LifecycleRun, receiver: str, document: OrderFile, pid: str) -> ChildResult: context = run.commander todo = context.comms.goals.todos.get(pid) tool = CommsOrderTool( @@ -134,10 +140,10 @@ def read(cls) -> ParityComparison: cases.append( ( label, - reference.returncode, - attempt.returncode, - tuple(reference.stderr.splitlines()[-1:]), - tuple(attempt.stderr.splitlines()[-1:]), + reference.outcome, + attempt.outcome, + tuple(reference.stderr.decode(errors="replace").splitlines()[-1:]), + tuple(attempt.stderr.decode(errors="replace").splitlines()[-1:]), before == cls.state(run, delivered.id, receiver), ) ) diff --git a/src/agent_comms/commander_parity/lifecycle.py b/src/agent_comms/commander_parity/lifecycle.py index 0e633e1bf0..2331074ec3 100644 --- a/src/agent_comms/commander_parity/lifecycle.py +++ b/src/agent_comms/commander_parity/lifecycle.py @@ -2,12 +2,12 @@ from __future__ import annotations +import asyncio import contextlib import io import json import os import shlex -import subprocess import sys import time from abc import ABC, abstractmethod @@ -21,6 +21,7 @@ from ..change_report import FileLine from ..check_root import CheckRoot +from ..child_process import BoundedRun, ChildResult from ..commander.config import CommanderConfig from ..commander.order import Commander, OrderFile, hhmm from ..commander.request import RequestAddVerb @@ -181,15 +182,19 @@ def open(cls, *, extra_threads: tuple[str, ...] = ()) -> Iterator[LifecycleRun]: ) yield cls(commander, worker.name, decisions, document, root / "application.json") + @classmethod + def invoke(cls, program: str, *arguments: str) -> ChildResult: + return cls.invoke_command((sys.executable, "-c", program, *arguments)) + @staticmethod - def invoke(program: str, *arguments: str) -> subprocess.CompletedProcess[str]: + def invoke_command(command: tuple[str, ...]) -> ChildResult: source = str(Path(__file__).resolve().parents[2]) - return subprocess.run( - (sys.executable, "-c", program, *arguments), - capture_output=True, - text=True, - timeout=120, - env={**os.environ, "PYTHONPATH": source}, + return asyncio.run( + BoundedRun.run( + command, + timeout=120, + env={**os.environ, "PYTHONPATH": source}, + ) ) @property From 9cf54604542fe4c3757388276135e2bbd94f5f68 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:36:55 -0400 Subject: [PATCH 34/66] Place checked-commit worktrees under the configured checkout owner --- src/agent_comms/goals.py | 5 +++-- src/agent_comms/todos.py | 10 +++++----- tests/test_order_progress.py | 4 ++-- tests/test_standing_rules.py | 6 +++--- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/agent_comms/goals.py b/src/agent_comms/goals.py index 5cd5f236cf..be2daa6035 100644 --- a/src/agent_comms/goals.py +++ b/src/agent_comms/goals.py @@ -331,12 +331,13 @@ def recorded(self, sha: str, repository: str) -> tuple[ChildResult, ...]: return tuple(checked.result for checked in CheckedCommit.stored(repository, sha) if checked.command == self.command)[:1] - def at(self, revision: str, repository: str) -> ChildResult: + def at(self, revision: str, repository: str, checkout_base: Path) -> ChildResult: """Read the result on ``revision``, or explicitly run the check in a detached checkout.""" sha = CheckedCommit.git(repository, "rev-parse", f"{revision}^{{commit}}").strip() for recorded in self.recorded(sha, repository): return recorded - with tempfile.TemporaryDirectory(prefix="order-check-") as scratch: + checkout_base.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix="order-check-", dir=checkout_base) as scratch: tree = str(Path(scratch) / "tree") CheckedCommit.git(repository, "worktree", "add", "--detach", tree, sha) try: diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index e65df2c3dd..35885bbe0e 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -98,14 +98,14 @@ async def verified(self, todo: Todo, registry: Registration) -> OrderDelivery: check = todo.order.check for each in (check, *todo.order.deletes.checks): await self.require_source(each) - each.result.require(await self.checked(each)) + each.result.require(await self.checked(each, registry.storage_rule.checkout_base)) return replace(self, at=await self.delivery_time(check)) @abstractmethod async def require_source(self, check): ... @abstractmethod - async def checked(self, check: OrderCheck) -> ChildResult: ... + async def checked(self, check: OrderCheck, checkout_base: Path) -> ChildResult: ... @abstractmethod async def delivery_time(self, check): ... @@ -140,8 +140,8 @@ def work(self) -> str: async def require_source(self, check: OrderCheck) -> None: raise ValueError("A commit delivery names the unit's delivered commit and the checked one.") - async def checked(self, check: OrderCheck) -> ChildResult: - return await asyncio.to_thread(check.at, self.commit, check.directory) + async def checked(self, check: OrderCheck, checkout_base: Path) -> ChildResult: + return await asyncio.to_thread(check.at, self.commit, check.directory, checkout_base) async def delivery_time(self, check: OrderCheck) -> EventTime: return EventTime.capture(float( @@ -281,7 +281,7 @@ def line(self) -> str: async def require_source(self, check): pass - async def checked(self, check: OrderCheck) -> ChildResult: + async def checked(self, check: OrderCheck, checkout_base: Path) -> ChildResult: return await check.run() async def delivery_time(self, check): diff --git a/tests/test_order_progress.py b/tests/test_order_progress.py index da1e20fce6..25a51e2ab8 100644 --- a/tests/test_order_progress.py +++ b/tests/test_order_progress.py @@ -115,7 +115,7 @@ def test_uncounted_check_keeps_other_overdue_orders_visible(commander, commander replace(todo, order=broken).upsert(db) create(commander, 'P2', deadline=1) commit(unit) - broken.check.at(git(unit, 'rev-parse', 'HEAD'), str(unit)) + broken.check.at(git(unit, 'rev-parse', 'HEAD'), str(unit), unit.parent) progress = read((comms.goals.todos.get('P1'), unit)) assert progress.line.endswith("; check prints no integer: printf 'no count here'") @@ -213,7 +213,7 @@ def test_progress_reads_only_explicit_check_receipts(checkout, tmp_path): source = ProgressSource.of(todo, StorageRule(checkout_base=unit.parent), Listed()) assert not source.readings().readings assert not marker.exists() - check.at(git(unit, 'rev-parse', 'HEAD').strip(), str(unit)) + check.at(git(unit, 'rev-parse', 'HEAD').strip(), str(unit), unit.parent) assert marker.read_text() == 'x' assert len(source.readings().readings) == 1 assert source.readings().readings[0].distance == CountPrinted(todo.order.checked().untouched(())) diff --git a/tests/test_standing_rules.py b/tests/test_standing_rules.py index 4053c77e2d..9f64a92ac3 100644 --- a/tests/test_standing_rules.py +++ b/tests/test_standing_rules.py @@ -206,17 +206,17 @@ def commit(left): check = f"echo run >> {runs}; cat left" todo = create(commander, "P1", command=check, expected=0, deadline=time.time() + 3600) ordered_check = todo.order.checked().check - ordered_check.at(git("rev-parse", "HEAD").strip(), str(unit)) + ordered_check.at(git("rev-parse", "HEAD").strip(), str(unit), unit.parent) def progress(): return OrderProgress.read(comms.goals.todos.get("P1"), storage, PullRequests()) commit(2) - ordered_check.at(git("rev-parse", "HEAD").strip(), str(unit)) + ordered_check.at(git("rev-parse", "HEAD").strip(), str(unit), unit.parent) assert "; distance 2, projected done " in progress().line assert isinstance(progress().distance_readings().trend, MovingTrend) commit(2) - ordered_check.at(git("rev-parse", "HEAD").strip(), str(unit)) + ordered_check.at(git("rev-parse", "HEAD").strip(), str(unit), unit.parent) assert progress().line.endswith("; distance 2, not falling") assert runs.read_text().count("run") == 3 # once per commit: base, then each branch commit From 0509388ad56ba688284c42b6032d2a7e98c9d6ef Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:37:16 -0400 Subject: [PATCH 35/66] Exercise current and native delivered-commit outcomes on the copied bus --- src/agent_comms/commander_parity/delivery.py | 143 +++++++++++++++++++ src/agent_comms/commander_parity/rows.py | 2 + 2 files changed, 145 insertions(+) create mode 100644 src/agent_comms/commander_parity/delivery.py diff --git a/src/agent_comms/commander_parity/delivery.py b/src/agent_comms/commander_parity/delivery.py new file mode 100644 index 0000000000..b5c2a5ae49 --- /dev/null +++ b/src/agent_comms/commander_parity/delivery.py @@ -0,0 +1,143 @@ +"""Compare commit delivery through the current command and the native tool.""" + +from __future__ import annotations + +import contextlib +import io +import json +import subprocess +from dataclasses import dataclass, replace +from pathlib import Path + +from ..commander.order import OrderFile +from ..field_codec import FieldCodec +from ..progress_claims import MergedClaim +from ..todos import CommitDelivery, FileDeliveryEvidence, Todo +from ..tools import CommsOrderDoneTool +from . import ParityComparison +from .lifecycle import LifecycleRun + + +@dataclass(frozen=True) +class DeliveryOutcome: + pending: bool + order_line: str + refusal: str + + @classmethod + def saved(cls, todo: Todo, refusal: str) -> DeliveryOutcome: + return cls(todo.pending, todo.order.line, refusal) + + +@dataclass(frozen=True) +class DeliveryComparison(ParityComparison): + cases: tuple[tuple[str, str, DeliveryOutcome, DeliveryOutcome], ...] + + @property + def passed(self) -> bool: + return len(self.cases) == 3 and not self.cases[0][2].pending and all( + current.pending == native.pending + and current.order_line == native.order_line + and (not current.pending or (commit in current.refusal and commit in native.refusal + and "not in" in current.refusal and "not in" in native.refusal)) + for _, commit, current, native in self.cases) + + def describe(self) -> str: + return repr(self.cases) + + +@dataclass(frozen=True) +class DeliveryRepository: + path: Path + + def git(self, *arguments: str) -> str: + return subprocess.check_output( + ("git", "-C", str(self.path), "-c", "user.name=parity", + "-c", "user.email=parity@invalid", *arguments), + text=True, stderr=subprocess.PIPE).strip() + + @classmethod + def create(cls, root: Path) -> DeliveryRepository: + path = root / "delivery-repository" + path.mkdir() + repo = cls(path) + remote = root / "delivery-origin.git" + subprocess.run(("git", "init", "--bare", "-q", str(remote)), check=True) + repo.git("init", "-q", "-b", "main") + url = "https://github.com/parity-local/delivery.git" + repo.git("config", f"url.{remote}.insteadOf", url) + repo.git("remote", "add", "origin", url) + repo.git("commit", "-q", "--allow-empty", "-m", "copied delivery base") + repo.git("push", "-q", "origin", "main") + repo.git("symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/main") + return repo + + def cases(self) -> tuple[tuple[str, str], ...]: + base = self.git("rev-parse", "HEAD") + self.git("switch", "-q", "-c", "main-answer") + self.git("commit", "-q", "--allow-empty", "-m", "answer integrated on main") + main = self.git("rev-parse", "HEAD") + self.git("switch", "-q", "main") + self.git("merge", "-q", "--no-ff", "main-answer", "-m", "integrate main answer") + self.git("push", "-q", "origin", "main") + self.git("switch", "-q", "-c", "branch-answer", base) + self.git("commit", "-q", "--allow-empty", "-m", "answer integrated elsewhere") + branch = self.git("rev-parse", "HEAD") + self.git("switch", "-q", "-c", "integration", base) + self.git("merge", "-q", "--no-ff", "branch-answer", "-m", "integrate branch answer") + self.git("push", "-q", "origin", "integration") + self.git("switch", "-q", "-c", "unmerged-answer", base) + self.git("commit", "-q", "--allow-empty", "-m", "published but not integrated") + unmerged = self.git("rev-parse", "HEAD") + self.git("push", "-q", "origin", "unmerged-answer") + self.git("switch", "-q", "main") + return (("merged to main", main), ("merged to another branch", branch), + ("not merged", unmerged)) + + +class DeliveredCommitCase: + @classmethod + def create_order(cls, run: LifecycleRun, repository: DeliveryRepository) -> Todo: + document = OrderFile.read(run.document.read_text()) + document = replace(document, order=replace(document.order, + check=replace(document.order.check, directory=str(repository.path)))) + run.document.write_text(json.dumps(FieldCodec.encode(document))) + before = {todo.id for todo in run.commander.comms.goals.todos.for_commander( + run.commander.incarnation)} + run.commander.problem(str(run.document), run.worker, run.due, + f"{Path(__file__).resolve()}:1") + created, = (todo for todo in run.commander.comms.goals.todos.for_commander( + run.commander.incarnation) if todo.id not in before) + return created + + @classmethod + def read(cls) -> ParityComparison: + cases = [] + with LifecycleRun.open() as run, contextlib.redirect_stdout(io.StringIO()): + repository = DeliveryRepository.create(run.commander.root) + run.decisions.write_text("[]") + run.artifact.write_text("Copied-bus delivered-commit comparison; no source input executed.") + for name, commit in repository.cases(): + current_order = cls.create_order(run, repository) + refusal = "" + try: + run.commander.done(current_order.id, commit, seen=str(run.artifact)) + except (SystemExit, ValueError) as error: + refusal = str(error) + current = DeliveryOutcome.saved( + run.commander.comms.goals.todos.get(current_order.id), refusal) + native_order = cls.create_order(run, repository) + refusal = "" + try: + run.commander.call(CommsOrderDoneTool( + problem=native_order.id, order_id=native_order.order.id, + delivery=CommitDelivery(commit=repository.git("rev-parse", "HEAD"), + delivered_commit=commit, evidence=FileDeliveryEvidence(str(run.artifact))), + claims=(MergedClaim(commit),))) + except ValueError as error: + refusal = str(error) + native = DeliveryOutcome.saved( + run.commander.comms.goals.todos.get(native_order.id), refusal) + cases.append((name, commit, current, native)) + return DeliveryComparison(tuple(cases)) + diff --git a/src/agent_comms/commander_parity/rows.py b/src/agent_comms/commander_parity/rows.py index 97ab0b9321..5a3b5b61c4 100644 --- a/src/agent_comms/commander_parity/rows.py +++ b/src/agent_comms/commander_parity/rows.py @@ -11,6 +11,7 @@ StoryPhase, ) from . import ParityComparison, ParityRow, UnverifiedParity +from .delivery import DeliveredCommitCase from .handoff import FinishedHandoffCase from .lifecycle import ( AssignOrderCase, @@ -50,6 +51,7 @@ class DeliveredCommitParityRow(ParityRow): current = "`order.py done`" native = "`CommsOrderDoneTool`: merged to main, another branch, or not merged" phases = (OneOrderOwnerPhase, RecordPlanePhase) + comparison = DeliveredCommitCase.read class InstalledEvidenceParityRow(ParityRow): From 44081ccb97dd52c699295ff79b763564011740d2 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:40:40 -0400 Subject: [PATCH 36/66] Keep every copied delivery order failing until its actual run --- src/agent_comms/commander_parity/delivery.py | 2 ++ tests/test_commander_parity_delivery.py | 34 ++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 tests/test_commander_parity_delivery.py diff --git a/src/agent_comms/commander_parity/delivery.py b/src/agent_comms/commander_parity/delivery.py index b5c2a5ae49..022de0615a 100644 --- a/src/agent_comms/commander_parity/delivery.py +++ b/src/agent_comms/commander_parity/delivery.py @@ -102,12 +102,14 @@ def create_order(cls, run: LifecycleRun, repository: DeliveryRepository) -> Todo document = replace(document, order=replace(document.order, check=replace(document.order.check, directory=str(repository.path)))) run.document.write_text(json.dumps(FieldCodec.encode(document))) + run.decisions.write_text(json.dumps([run.worker])) before = {todo.id for todo in run.commander.comms.goals.todos.for_commander( run.commander.incarnation)} run.commander.problem(str(run.document), run.worker, run.due, f"{Path(__file__).resolve()}:1") created, = (todo for todo in run.commander.comms.goals.todos.for_commander( run.commander.incarnation) if todo.id not in before) + run.decisions.write_text("[]") return created @classmethod diff --git a/tests/test_commander_parity_delivery.py b/tests/test_commander_parity_delivery.py new file mode 100644 index 0000000000..09d3e736fc --- /dev/null +++ b/tests/test_commander_parity_delivery.py @@ -0,0 +1,34 @@ +from agent_comms.commander_parity.delivery import DeliveryRepository, DeliveredCommitCase +from agent_comms.commander_parity.rows import DeliveredCommitParityRow +from agent_comms.goals import CountCheckResult, OrderCheck + + +def test_delivery_graph_is_pushed_and_distinguishes_integration(tmp_path): + repository = DeliveryRepository.create(tmp_path) + main, branch, unmerged = repository.cases() + for _, commit in (main, branch, unmerged): + assert repository.git("branch", "--remotes", "--contains", commit, "--list", "origin/*") + assert repository.git("merge-base", main[1], "origin/main") == main[1] + assert repository.git("merge-base", branch[1], "origin/integration") == branch[1] + assert repository.git("merge-base", branch[1], "origin/main") != branch[1] + assert repository.git("merge-base", unmerged[1], "origin/main") != unmerged[1] + assert repository.git("merge-base", unmerged[1], "origin/integration") != unmerged[1] + + +def test_check_uses_configured_persistent_checkout_and_retains_real_result(tmp_path): + repository = DeliveryRepository.create(tmp_path) + checkout_base = tmp_path / "persistent-checkouts" + marker = tmp_path / "observed-checkouts" + check = OrderCheck(str(repository.path), f'pwd >> "{marker}"; echo 0', CountCheckResult(0)) + commit = repository.git("rev-parse", "HEAD") + first = check.at(commit, str(repository.path), checkout_base) + assert check.at(commit, str(repository.path), checkout_base) == first + actual, = marker.read_text().splitlines() + assert actual.startswith(str(checkout_base) + "/order-check-") + assert not tuple(checkout_base.iterdir()) + assert len(check.recorded(commit, str(repository.path))) == 1 + + +def test_delivery_row_declares_comparison_once(): + assert "verify" not in vars(DeliveredCommitParityRow) + assert DeliveredCommitParityRow.comparison == DeliveredCommitCase.read From fc5358e82664dc4de9cab55df7e873725cab1177 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:45:22 -0400 Subject: [PATCH 37/66] State the checked main commit and migrate the explicit check proof --- src/agent_comms/commander_parity/delivery.py | 11 +++++++---- tools/proofs/cn_progress_read_only.py | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/agent_comms/commander_parity/delivery.py b/src/agent_comms/commander_parity/delivery.py index 022de0615a..fe69c920fc 100644 --- a/src/agent_comms/commander_parity/delivery.py +++ b/src/agent_comms/commander_parity/delivery.py @@ -31,6 +31,7 @@ def saved(cls, todo: Todo, refusal: str) -> DeliveryOutcome: @dataclass(frozen=True) class DeliveryComparison(ParityComparison): + checked_commit: str cases: tuple[tuple[str, str, DeliveryOutcome, DeliveryOutcome], ...] @property @@ -38,12 +39,13 @@ def passed(self) -> bool: return len(self.cases) == 3 and not self.cases[0][2].pending and all( current.pending == native.pending and current.order_line == native.order_line - and (not current.pending or (commit in current.refusal and commit in native.refusal - and "not in" in current.refusal and "not in" in native.refusal)) + and (all(commit in outcome.refusal and "not in" in outcome.refusal + for outcome in (current, native)) + if current.pending else commit in current.order_line) for _, commit, current, native in self.cases) def describe(self) -> str: - return repr(self.cases) + return f"checked main={self.checked_commit}; outcomes={self.cases!r}" @dataclass(frozen=True) @@ -141,5 +143,6 @@ def read(cls) -> ParityComparison: native = DeliveryOutcome.saved( run.commander.comms.goals.todos.get(native_order.id), refusal) cases.append((name, commit, current, native)) - return DeliveryComparison(tuple(cases)) + checked_commit = repository.git("rev-parse", "HEAD") + return DeliveryComparison(checked_commit, tuple(cases)) diff --git a/tools/proofs/cn_progress_read_only.py b/tools/proofs/cn_progress_read_only.py index 5065895634..05d2364053 100644 --- a/tools/proofs/cn_progress_read_only.py +++ b/tools/proofs/cn_progress_read_only.py @@ -63,7 +63,7 @@ def verify() -> None: assert not MARKER.exists() sha = CheckedCommit.git(str(checkout), "rev-parse", "HEAD").strip() assert not check.recorded(sha, str(checkout)) - check.at(sha, str(checkout)) # Deliberately explicit, never a board read. + check.at(sha, str(checkout), comms.registry.storage_rule.checkout_base) assert MARKER.read_bytes() == MARKER_CONTENT assert len(board(todo).readings) == 1 assert MARKER.read_bytes() == MARKER_CONTENT From 15758fd6f5b6d8129e2ec03d74aaccc370384e22 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:48:27 -0400 Subject: [PATCH 38/66] Compare real command process exits without swallowing native refusals --- src/agent_comms/commander_parity/delivery.py | 147 ++++++++++++++----- tests/test_commander_parity_delivery.py | 2 +- 2 files changed, 108 insertions(+), 41 deletions(-) diff --git a/src/agent_comms/commander_parity/delivery.py b/src/agent_comms/commander_parity/delivery.py index fe69c920fc..be0a9ce940 100644 --- a/src/agent_comms/commander_parity/delivery.py +++ b/src/agent_comms/commander_parity/delivery.py @@ -5,7 +5,9 @@ import contextlib import io import json +import os import subprocess +import sys from dataclasses import dataclass, replace from pathlib import Path @@ -20,13 +22,14 @@ @dataclass(frozen=True) class DeliveryOutcome: + exit_code: int pending: bool order_line: str refusal: str @classmethod - def saved(cls, todo: Todo, refusal: str) -> DeliveryOutcome: - return cls(todo.pending, todo.order.line, refusal) + def saved(cls, todo: Todo, process: subprocess.CompletedProcess[str]) -> DeliveryOutcome: + return cls(process.returncode, todo.pending, todo.order.line, process.stderr) @dataclass(frozen=True) @@ -36,13 +39,23 @@ class DeliveryComparison(ParityComparison): @property def passed(self) -> bool: - return len(self.cases) == 3 and not self.cases[0][2].pending and all( - current.pending == native.pending - and current.order_line == native.order_line - and (all(commit in outcome.refusal and "not in" in outcome.refusal - for outcome in (current, native)) - if current.pending else commit in current.order_line) - for _, commit, current, native in self.cases) + return ( + len(self.cases) == 3 + and not self.cases[0][2].pending + and all( + (current.exit_code, current.pending) == (native.exit_code, native.pending) + and current.order_line == native.order_line + and ( + all( + commit in outcome.refusal and "not in" in outcome.refusal + for outcome in (current, native) + ) + if current.pending + else commit in current.order_line + ) + for _, commit, current, native in self.cases + ) + ) def describe(self) -> str: return f"checked main={self.checked_commit}; outcomes={self.cases!r}" @@ -54,9 +67,19 @@ class DeliveryRepository: def git(self, *arguments: str) -> str: return subprocess.check_output( - ("git", "-C", str(self.path), "-c", "user.name=parity", - "-c", "user.email=parity@invalid", *arguments), - text=True, stderr=subprocess.PIPE).strip() + ( + "git", + "-C", + str(self.path), + "-c", + "user.name=parity", + "-c", + "user.email=parity@invalid", + *arguments, + ), + text=True, + stderr=subprocess.PIPE, + ).strip() @classmethod def create(cls, root: Path) -> DeliveryRepository: @@ -93,24 +116,48 @@ def cases(self) -> tuple[tuple[str, str], ...]: unmerged = self.git("rev-parse", "HEAD") self.git("push", "-q", "origin", "unmerged-answer") self.git("switch", "-q", "main") - return (("merged to main", main), ("merged to another branch", branch), - ("not merged", unmerged)) + return ( + ("merged to main", main), + ("merged to another branch", branch), + ("not merged", unmerged), + ) class DeliveredCommitCase: + @staticmethod + def invoke(program: str, *arguments: str) -> subprocess.CompletedProcess[str]: + source = str(Path(__file__).resolve().parents[2]) + return subprocess.run( + (sys.executable, "-c", program, *arguments), + capture_output=True, + text=True, + timeout=120, + env={**os.environ, "PYTHONPATH": source}, + ) + @classmethod def create_order(cls, run: LifecycleRun, repository: DeliveryRepository) -> Todo: document = OrderFile.read(run.document.read_text()) - document = replace(document, order=replace(document.order, - check=replace(document.order.check, directory=str(repository.path)))) + document = replace( + document, + order=replace( + document.order, check=replace(document.order.check, directory=str(repository.path)) + ), + ) run.document.write_text(json.dumps(FieldCodec.encode(document))) run.decisions.write_text(json.dumps([run.worker])) - before = {todo.id for todo in run.commander.comms.goals.todos.for_commander( - run.commander.incarnation)} - run.commander.problem(str(run.document), run.worker, run.due, - f"{Path(__file__).resolve()}:1") - created, = (todo for todo in run.commander.comms.goals.todos.for_commander( - run.commander.incarnation) if todo.id not in before) + before = { + todo.id + for todo in run.commander.comms.goals.todos.for_commander(run.commander.incarnation) + } + run.commander.problem( + str(run.document), run.worker, run.due, f"{Path(__file__).resolve()}:1" + ) + (created,) = ( + todo + for todo in run.commander.comms.goals.todos.for_commander(run.commander.incarnation) + if todo.id not in before + ) run.decisions.write_text("[]") return created @@ -120,29 +167,49 @@ def read(cls) -> ParityComparison: with LifecycleRun.open() as run, contextlib.redirect_stdout(io.StringIO()): repository = DeliveryRepository.create(run.commander.root) run.decisions.write_text("[]") - run.artifact.write_text("Copied-bus delivered-commit comparison; no source input executed.") + run.artifact.write_text( + "Copied-bus delivered-commit comparison; no source input executed." + ) for name, commit in repository.cases(): current_order = cls.create_order(run, repository) - refusal = "" - try: - run.commander.done(current_order.id, commit, seen=str(run.artifact)) - except (SystemExit, ValueError) as error: - refusal = str(error) + process = cls.invoke( + "from agent_comms.commander.order import Commander; import sys; " + "Commander.model_validate_json(sys.argv[1]).done(" + "sys.argv[2],sys.argv[3],seen=sys.argv[4])", + run.commander.model_dump_json(), + current_order.id, + commit, + str(run.artifact), + ) current = DeliveryOutcome.saved( - run.commander.comms.goals.todos.get(current_order.id), refusal) + run.commander.comms.goals.todos.get(current_order.id), process + ) native_order = cls.create_order(run, repository) - refusal = "" - try: - run.commander.call(CommsOrderDoneTool( - problem=native_order.id, order_id=native_order.order.id, - delivery=CommitDelivery(commit=repository.git("rev-parse", "HEAD"), - delivered_commit=commit, evidence=FileDeliveryEvidence(str(run.artifact))), - claims=(MergedClaim(commit),))) - except ValueError as error: - refusal = str(error) + tool = CommsOrderDoneTool( + problem=native_order.id, + order_id=native_order.order.checked().id, + delivery=CommitDelivery( + commit=repository.git("rev-parse", "HEAD"), + delivered_commit=commit, + evidence=FileDeliveryEvidence(str(run.artifact)), + ), + claims=(MergedClaim(commit),), + ) + process = cls.invoke( + "from pathlib import Path; import sys; " + "from agent_comms.comms import Comms; " + "from agent_comms.field_codec import FieldCodec; " + "from agent_comms.tool_caller import ThreadCaller; " + "from agent_comms.tools import CommsOrderDoneTool; " + "ThreadCaller(sys.argv[2]).invoke(FieldCodec.decode(CommsOrderDoneTool, " + "FieldCodec.parse(sys.argv[3])),Comms(Path(sys.argv[1])))", + str(run.commander.root), + run.commander.commander, + json.dumps(FieldCodec.encode(tool)), + ) native = DeliveryOutcome.saved( - run.commander.comms.goals.todos.get(native_order.id), refusal) + run.commander.comms.goals.todos.get(native_order.id), process + ) cases.append((name, commit, current, native)) checked_commit = repository.git("rev-parse", "HEAD") return DeliveryComparison(checked_commit, tuple(cases)) - diff --git a/tests/test_commander_parity_delivery.py b/tests/test_commander_parity_delivery.py index 09d3e736fc..023bf6faa1 100644 --- a/tests/test_commander_parity_delivery.py +++ b/tests/test_commander_parity_delivery.py @@ -23,7 +23,7 @@ def test_check_uses_configured_persistent_checkout_and_retains_real_result(tmp_p commit = repository.git("rev-parse", "HEAD") first = check.at(commit, str(repository.path), checkout_base) assert check.at(commit, str(repository.path), checkout_base) == first - actual, = marker.read_text().splitlines() + (actual,) = marker.read_text().splitlines() assert actual.startswith(str(checkout_base) + "/order-check-") assert not tuple(checkout_base.iterdir()) assert len(check.recorded(commit, str(repository.path))) == 1 From b3100a84a65007666a58449d025cfdf4d60f55b2 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:51:59 -0400 Subject: [PATCH 39/66] Keep child timeout and retirement outcomes in the delivery comparison --- src/agent_comms/commander_parity/delivery.py | 36 +++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/agent_comms/commander_parity/delivery.py b/src/agent_comms/commander_parity/delivery.py index be0a9ce940..859109a597 100644 --- a/src/agent_comms/commander_parity/delivery.py +++ b/src/agent_comms/commander_parity/delivery.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio import contextlib import io import json @@ -11,6 +12,7 @@ from dataclasses import dataclass, replace from pathlib import Path +from ..child_process import BoundedRun, ChildOutcome, ChildResult, ExitedOutcome from ..commander.order import OrderFile from ..field_codec import FieldCodec from ..progress_claims import MergedClaim @@ -22,14 +24,20 @@ @dataclass(frozen=True) class DeliveryOutcome: - exit_code: int + outcome: ChildOutcome pending: bool order_line: str refusal: str @classmethod - def saved(cls, todo: Todo, process: subprocess.CompletedProcess[str]) -> DeliveryOutcome: - return cls(process.returncode, todo.pending, todo.order.line, process.stderr) + def saved(cls, todo: Todo, process: ChildResult) -> DeliveryOutcome: + return cls( + process.outcome, todo.pending, todo.order.line, process.stderr.decode(errors="replace") + ) + + @property + def delivered(self) -> bool: + return self.outcome.successful and not self.pending @dataclass(frozen=True) @@ -41,13 +49,15 @@ class DeliveryComparison(ParityComparison): def passed(self) -> bool: return ( len(self.cases) == 3 - and not self.cases[0][2].pending + and self.cases[0][2].delivered and all( - (current.exit_code, current.pending) == (native.exit_code, native.pending) + (current.outcome, current.pending) == (native.outcome, native.pending) and current.order_line == native.order_line and ( all( - commit in outcome.refusal and "not in" in outcome.refusal + outcome.outcome == ExitedOutcome(1) + and commit in outcome.refusal + and "not in" in outcome.refusal for outcome in (current, native) ) if current.pending @@ -125,14 +135,14 @@ def cases(self) -> tuple[tuple[str, str], ...]: class DeliveredCommitCase: @staticmethod - def invoke(program: str, *arguments: str) -> subprocess.CompletedProcess[str]: + def invoke(program: str, *arguments: str) -> ChildResult: source = str(Path(__file__).resolve().parents[2]) - return subprocess.run( - (sys.executable, "-c", program, *arguments), - capture_output=True, - text=True, - timeout=120, - env={**os.environ, "PYTHONPATH": source}, + return asyncio.run( + BoundedRun.run( + (sys.executable, "-c", program, *arguments), + timeout=120, + env={**os.environ, "PYTHONPATH": source}, + ) ) @classmethod From 36a29c25086c20319cef22f8f423c3fde5ab159e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 20 Oct 2026 00:04:22 -0400 Subject: [PATCH 40/66] Use the lifecycle-owned candidate process launcher for delivered-commit parity --- src/agent_comms/commander_parity/delivery.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/src/agent_comms/commander_parity/delivery.py b/src/agent_comms/commander_parity/delivery.py index 859109a597..0026115cd7 100644 --- a/src/agent_comms/commander_parity/delivery.py +++ b/src/agent_comms/commander_parity/delivery.py @@ -2,17 +2,14 @@ from __future__ import annotations -import asyncio import contextlib import io import json -import os import subprocess -import sys from dataclasses import dataclass, replace from pathlib import Path -from ..child_process import BoundedRun, ChildOutcome, ChildResult, ExitedOutcome +from ..child_process import ChildOutcome, ChildResult, ExitedOutcome from ..commander.order import OrderFile from ..field_codec import FieldCodec from ..progress_claims import MergedClaim @@ -134,17 +131,6 @@ def cases(self) -> tuple[tuple[str, str], ...]: class DeliveredCommitCase: - @staticmethod - def invoke(program: str, *arguments: str) -> ChildResult: - source = str(Path(__file__).resolve().parents[2]) - return asyncio.run( - BoundedRun.run( - (sys.executable, "-c", program, *arguments), - timeout=120, - env={**os.environ, "PYTHONPATH": source}, - ) - ) - @classmethod def create_order(cls, run: LifecycleRun, repository: DeliveryRepository) -> Todo: document = OrderFile.read(run.document.read_text()) @@ -182,7 +168,7 @@ def read(cls) -> ParityComparison: ) for name, commit in repository.cases(): current_order = cls.create_order(run, repository) - process = cls.invoke( + process = run.invoke( "from agent_comms.commander.order import Commander; import sys; " "Commander.model_validate_json(sys.argv[1]).done(" "sys.argv[2],sys.argv[3],seen=sys.argv[4])", @@ -205,7 +191,7 @@ def read(cls) -> ParityComparison: ), claims=(MergedClaim(commit),), ) - process = cls.invoke( + process = run.invoke( "from pathlib import Path; import sys; " "from agent_comms.comms import Comms; " "from agent_comms.field_codec import FieldCodec; " From 80c5348af5ddc502315e4bf9e18036a7ec429fa5 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 20 Oct 2026 00:04:52 -0400 Subject: [PATCH 41/66] Preserve caller-owned cwd and sanitized environments in the bounded process owner --- src/agent_comms/commander_parity/lifecycle.py | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/agent_comms/commander_parity/lifecycle.py b/src/agent_comms/commander_parity/lifecycle.py index 2331074ec3..60117649bd 100644 --- a/src/agent_comms/commander_parity/lifecycle.py +++ b/src/agent_comms/commander_parity/lifecycle.py @@ -182,20 +182,24 @@ def open(cls, *, extra_threads: tuple[str, ...] = ()) -> Iterator[LifecycleRun]: ) yield cls(commander, worker.name, decisions, document, root / "application.json") + @staticmethod + def python_environment() -> dict[str, str]: + return {**os.environ, "PYTHONPATH": str(Path(__file__).resolve().parents[2])} + @classmethod def invoke(cls, program: str, *arguments: str) -> ChildResult: - return cls.invoke_command((sys.executable, "-c", program, *arguments)) + return cls.invoke_command( + (sys.executable, "-c", program, *arguments), env=cls.python_environment() + ) @staticmethod - def invoke_command(command: tuple[str, ...]) -> ChildResult: - source = str(Path(__file__).resolve().parents[2]) - return asyncio.run( - BoundedRun.run( - command, - timeout=120, - env={**os.environ, "PYTHONPATH": source}, - ) - ) + def invoke_command( + command: tuple[str, ...], + *, + cwd: str | Path | None = None, + env: dict[str, str] | None = None, + ) -> ChildResult: + return asyncio.run(BoundedRun.run(command, timeout=120, cwd=cwd, env=env)) @property def due(self) -> str: From c140e10041ccb75c925aafbc9d31ea0deffe4aff Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:40:30 -0400 Subject: [PATCH 42/66] Compare deletion-intent refusal and acceptance through both real delivery adapters --- src/agent_comms/commander_parity/deletion.py | 185 +++++++++++++++++++ src/agent_comms/commander_parity/rows.py | 8 - 2 files changed, 185 insertions(+), 8 deletions(-) create mode 100644 src/agent_comms/commander_parity/deletion.py diff --git a/src/agent_comms/commander_parity/deletion.py b/src/agent_comms/commander_parity/deletion.py new file mode 100644 index 0000000000..060026a5c1 --- /dev/null +++ b/src/agent_comms/commander_parity/deletion.py @@ -0,0 +1,185 @@ +"""Deletion intent parity through real orders, pushed commits and native delivery checks.""" + +from __future__ import annotations + +import contextlib +import io +import json +import shlex +import subprocess +import sys +import tempfile +from abc import ABC, abstractmethod +from dataclasses import dataclass, replace +from pathlib import Path + +from ..change_report import FileLine +from ..child_process import CountPrinted +from ..commander.order import OrderFile, deadline +from ..field_codec import FieldCodec +from ..goal_actions import CreateProblemGoalAction +from ..goals import CountCheckResult, OrderCheck +from ..progress_claims import MergedClaim +from ..plan_phases import RecordPlanePhase +from ..repo import Repo +from ..todos import CommitDelivery, FamilyDeletion, MechanicalOrderRun, Todo +from ..tools import CommsOrderDoneTool, CommsTodoCreateTool, ProblemOrdered +from . import ParityComparison, ParityRow +from .lifecycle import LifecycleObservation, LifecycleRun, MeasuredCommander + + +@dataclass(frozen=True) +class DeletionComparison(ParityComparison): + current: LifecycleObservation + native: LifecycleObservation + + @property + def passed(self) -> bool: + return (self.current.passed and self.native.passed + and self.current.checks == self.native.checks) + + def describe(self) -> str: + return f"current: {self.current.describe()}; native: {self.native.describe()}" + + @classmethod + def read(cls) -> DeletionComparison: + return cls(CurrentDeletionPath.read(), NativeDeletionPath.read()) + + +class DeletionPath(ABC): + """Both adapters exercise the same scenario on independent LifecycleRun copies.""" + + @classmethod + @abstractmethod + def send(cls, run: LifecycleRun, document: OrderFile) -> Todo: ... + + @classmethod + @abstractmethod + def delivery_command(cls, run: LifecycleRun, todo: Todo, commit: str) -> tuple[str, ...]: ... + + @staticmethod + def commit(repository: Path, text: str, message: str) -> str: + (repository / 'family.py').write_text(text) + Repo.git(repository, 'add', 'family.py') + Repo.git(repository, 'commit', '-qm', message) + Repo.git(repository, 'push', '-q', 'origin', 'main') + return Repo.git(repository, 'rev-parse', 'HEAD').strip() + + @classmethod + def read(cls) -> LifecycleObservation: + with LifecycleRun.open() as original: + # Keep source checkouts out of the copied worker's storage accounting. + # Limits remain unchanged; all detached checks use this persistent owned base. + rule = original.commander.comms.registry.storage_rule + with tempfile.TemporaryDirectory(prefix='parity-deletion-', + dir=rule.checkout_base) as checkouts: + root = original.commander.root + (root / 'storage.json').write_text(json.dumps(FieldCodec.encode( + replace(rule, checkout_base=Path(checkouts))))) + (root / 'commander.json').write_text(original.commander.model_dump_json()) + run = replace(original, commander=MeasuredCommander.read_root(root)) + return cls.exercise(run) + + @classmethod + def exercise(cls, run: LifecycleRun) -> LifecycleObservation: + repository = Path(run.commander.comms.registry.require(run.worker).worktree) + remote = repository.parent / 'deletion-origin.git' + Repo.git(repository, 'init', '-q', '-b', 'main') + Repo.git(repository, 'config', 'user.name', 'Deletion parity') + Repo.git(repository, 'config', 'user.email', 'deletion-parity@localhost') + Repo.git(repository, 'init', '--bare', '-q', '--initial-branch=main', str(remote)) + Repo.git(repository, 'remote', 'add', 'origin', str(remote)) + family = 'class RetiredDecision:\n pass\n\n' + cls.commit(repository, family + 'def decisions():\n return [RetiredDecision()]\n', + 'Declare the obsolete decision family and its caller') + count = OrderCheck(str(repository), shlex.join((sys.executable, '-c', + "import ast;from pathlib import Path;" + "print(sum(isinstance(node,ast.ClassDef) for node in " + "ast.walk(ast.parse(Path('family.py').read_text()))))")), CountCheckResult(0)) + deletion = FamilyDeletion('obsolete decision family', + FileLine(str(repository / 'family.py'), 1), count) + initial = OrderFile.read(run.document.read_text()) + order = replace(initial.order, + check=OrderCheck(str(repository), shlex.join((sys.executable, '-c', + 'import family;assert not family.decisions()'))), + run=MechanicalOrderRun(), deletes=deletion) + document = replace(initial, order=order) + run.document.write_text(json.dumps(FieldCodec.encode(document))) + output = io.StringIO() + with contextlib.redirect_stdout(output): + before = cls.send(run, document) + partial = cls.commit(repository, family + 'def decisions():\n return []\n', + 'Repair the caller but leave one obsolete family site') + refused = subprocess.run(cls.delivery_command(run, before, partial), + capture_output=True, text=True, timeout=90) + after_refusal = run.commander.comms.goals.todos.get(before.id) + remaining = count.recorded(partial, str(repository)) + complete = cls.commit(repository, 'def decisions():\n return []\n', + 'Delete the last obsolete family declaration') + accepted = subprocess.run(cls.delivery_command(run, before, complete), + capture_output=True, text=True, timeout=90) + after_acceptance = run.commander.comms.goals.todos.get(before.id) + zero = count.recorded(complete, str(repository)) + primary = order.check.recorded(partial, str(repository)) + checks = ( + ('native order retained deletion intent', before.order.checked().deletes == deletion), + ('partial commit passed primary check', bool(primary) + and order.check.result.distance(primary[0]) == CountPrinted(0)), + ('partial commit retained one family site', bool(remaining) + and count.result.distance(remaining[0]) == CountPrinted(1)), + ('nonzero delivery refused', refused.returncode != 0), + ('refusal preserved pending order', before == after_refusal and after_refusal.pending), + ('zero count checked on delivered commit', bool(zero) + and count.result.distance(zero[0]) == CountPrinted(0)), + ('zero delivery accepted', accepted.returncode == 0 and not after_acceptance.pending), + ) + return LifecycleObservation( + CommsOrderDoneTool.__qualname__, len((refused, accepted)), checks, + output.getvalue() + f'partial={partial}; zero={complete}; ' + f'refused={refused.returncode}: {refused.stderr}; ' + f'accepted={accepted.returncode}: {accepted.stdout}{accepted.stderr}') + + +class CurrentDeletionPath(DeletionPath): + @classmethod + def send(cls, run: LifecycleRun, document: OrderFile) -> Todo: + return run.create() + + @classmethod + def delivery_command(cls, run: LifecycleRun, todo: Todo, commit: str) -> tuple[str, ...]: + return (sys.executable, '-c', 'from agent_comms.commander.cli import main;main()', + '--config', str(run.commander.root / 'commander.json'), + 'order', 'done', todo.id, commit) + + +class NativeDeletionPath(DeletionPath): + @classmethod + def send(cls, run: LifecycleRun, document: OrderFile) -> Todo: + result: ProblemOrdered = run.commander.call(CommsTodoCreateTool( + owner=run.worker, order=CreateProblemGoalAction( + problem='P1', repo=Repo.of(Path(document.order.check.directory)).github, + text=document.defect, order=document.order, + request=document.order.request( + run.commander.comms.registry.require(run.commander.commander), ('P1',)), + deadline=deadline(run.due), commander=run.commander.commander))) + return result.todo + + @classmethod + def delivery_command(cls, run: LifecycleRun, todo: Todo, commit: str) -> tuple[str, ...]: + request = CommsOrderDoneTool(problem=todo.id, order_id=todo.order.id, + delivery=CommitDelivery(commit=commit, delivered_commit=commit), + claims=(MergedClaim(commit),)) + arguments = FieldCodec.encode(request) + arguments.pop(request.family_discriminator) + return (sys.executable, '-m', 'agent_comms', '--root', str(run.commander.root), + 'tool', '--from', run.commander.commander, '--tool', request.declared_name, + '--arguments', json.dumps(arguments)) + + +class DeletionTargetParityRow(ParityRow): + number = 31 + capability = 'An order states its intent as a deletion target' + current = 'Current order command with a declared FamilyDeletion' + native = 'CommsTodoCreateTool and CommsOrderDoneTool check the delivered family count' + phases = (RecordPlanePhase,) + comparison = DeletionComparison.read diff --git a/src/agent_comms/commander_parity/rows.py b/src/agent_comms/commander_parity/rows.py index 97ab0b9321..39c8618a94 100644 --- a/src/agent_comms/commander_parity/rows.py +++ b/src/agent_comms/commander_parity/rows.py @@ -300,14 +300,6 @@ class BuildBoundProofParityRow(ParityRow): comparison = BuildBoundProofCase.read -class DeletionTargetParityRow(ParityRow): - number = 31 - capability = "An order states its intent as a deletion target" - current = "none; P520 renamed a decision rather than deleting it" - native = "Order owns deletion family/site/count; delivery refuses nonzero or renamed decisions" - phases = (RecordPlanePhase,) - - class UnitTimeParityRow(ParityRow): number = 32 capability = "Where units' time goes, classified" From 9aca5534c9cdeeb93decbc3e65378470716fdd61 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:36:55 -0400 Subject: [PATCH 43/66] Place checked-commit worktrees under the configured checkout owner --- src/agent_comms/goals.py | 5 +++-- src/agent_comms/todos.py | 10 +++++----- tests/test_order_progress.py | 4 ++-- tests/test_standing_rules.py | 6 +++--- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/agent_comms/goals.py b/src/agent_comms/goals.py index 5cd5f236cf..be2daa6035 100644 --- a/src/agent_comms/goals.py +++ b/src/agent_comms/goals.py @@ -331,12 +331,13 @@ def recorded(self, sha: str, repository: str) -> tuple[ChildResult, ...]: return tuple(checked.result for checked in CheckedCommit.stored(repository, sha) if checked.command == self.command)[:1] - def at(self, revision: str, repository: str) -> ChildResult: + def at(self, revision: str, repository: str, checkout_base: Path) -> ChildResult: """Read the result on ``revision``, or explicitly run the check in a detached checkout.""" sha = CheckedCommit.git(repository, "rev-parse", f"{revision}^{{commit}}").strip() for recorded in self.recorded(sha, repository): return recorded - with tempfile.TemporaryDirectory(prefix="order-check-") as scratch: + checkout_base.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix="order-check-", dir=checkout_base) as scratch: tree = str(Path(scratch) / "tree") CheckedCommit.git(repository, "worktree", "add", "--detach", tree, sha) try: diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index e65df2c3dd..35885bbe0e 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -98,14 +98,14 @@ async def verified(self, todo: Todo, registry: Registration) -> OrderDelivery: check = todo.order.check for each in (check, *todo.order.deletes.checks): await self.require_source(each) - each.result.require(await self.checked(each)) + each.result.require(await self.checked(each, registry.storage_rule.checkout_base)) return replace(self, at=await self.delivery_time(check)) @abstractmethod async def require_source(self, check): ... @abstractmethod - async def checked(self, check: OrderCheck) -> ChildResult: ... + async def checked(self, check: OrderCheck, checkout_base: Path) -> ChildResult: ... @abstractmethod async def delivery_time(self, check): ... @@ -140,8 +140,8 @@ def work(self) -> str: async def require_source(self, check: OrderCheck) -> None: raise ValueError("A commit delivery names the unit's delivered commit and the checked one.") - async def checked(self, check: OrderCheck) -> ChildResult: - return await asyncio.to_thread(check.at, self.commit, check.directory) + async def checked(self, check: OrderCheck, checkout_base: Path) -> ChildResult: + return await asyncio.to_thread(check.at, self.commit, check.directory, checkout_base) async def delivery_time(self, check: OrderCheck) -> EventTime: return EventTime.capture(float( @@ -281,7 +281,7 @@ def line(self) -> str: async def require_source(self, check): pass - async def checked(self, check: OrderCheck) -> ChildResult: + async def checked(self, check: OrderCheck, checkout_base: Path) -> ChildResult: return await check.run() async def delivery_time(self, check): diff --git a/tests/test_order_progress.py b/tests/test_order_progress.py index da1e20fce6..25a51e2ab8 100644 --- a/tests/test_order_progress.py +++ b/tests/test_order_progress.py @@ -115,7 +115,7 @@ def test_uncounted_check_keeps_other_overdue_orders_visible(commander, commander replace(todo, order=broken).upsert(db) create(commander, 'P2', deadline=1) commit(unit) - broken.check.at(git(unit, 'rev-parse', 'HEAD'), str(unit)) + broken.check.at(git(unit, 'rev-parse', 'HEAD'), str(unit), unit.parent) progress = read((comms.goals.todos.get('P1'), unit)) assert progress.line.endswith("; check prints no integer: printf 'no count here'") @@ -213,7 +213,7 @@ def test_progress_reads_only_explicit_check_receipts(checkout, tmp_path): source = ProgressSource.of(todo, StorageRule(checkout_base=unit.parent), Listed()) assert not source.readings().readings assert not marker.exists() - check.at(git(unit, 'rev-parse', 'HEAD').strip(), str(unit)) + check.at(git(unit, 'rev-parse', 'HEAD').strip(), str(unit), unit.parent) assert marker.read_text() == 'x' assert len(source.readings().readings) == 1 assert source.readings().readings[0].distance == CountPrinted(todo.order.checked().untouched(())) diff --git a/tests/test_standing_rules.py b/tests/test_standing_rules.py index 4053c77e2d..9f64a92ac3 100644 --- a/tests/test_standing_rules.py +++ b/tests/test_standing_rules.py @@ -206,17 +206,17 @@ def commit(left): check = f"echo run >> {runs}; cat left" todo = create(commander, "P1", command=check, expected=0, deadline=time.time() + 3600) ordered_check = todo.order.checked().check - ordered_check.at(git("rev-parse", "HEAD").strip(), str(unit)) + ordered_check.at(git("rev-parse", "HEAD").strip(), str(unit), unit.parent) def progress(): return OrderProgress.read(comms.goals.todos.get("P1"), storage, PullRequests()) commit(2) - ordered_check.at(git("rev-parse", "HEAD").strip(), str(unit)) + ordered_check.at(git("rev-parse", "HEAD").strip(), str(unit), unit.parent) assert "; distance 2, projected done " in progress().line assert isinstance(progress().distance_readings().trend, MovingTrend) commit(2) - ordered_check.at(git("rev-parse", "HEAD").strip(), str(unit)) + ordered_check.at(git("rev-parse", "HEAD").strip(), str(unit), unit.parent) assert progress().line.endswith("; distance 2, not falling") assert runs.read_text().count("run") == 3 # once per commit: base, then each branch commit From afbb68f235be6194651f943d41dccd45ede99142 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:47:25 -0400 Subject: [PATCH 44/66] Exercise current command and native entrypoints with exact delivered-commit observations --- src/agent_comms/commander_parity/deletion.py | 232 +++++++++++++------ 1 file changed, 159 insertions(+), 73 deletions(-) diff --git a/src/agent_comms/commander_parity/deletion.py b/src/agent_comms/commander_parity/deletion.py index 060026a5c1..d4655a9c13 100644 --- a/src/agent_comms/commander_parity/deletion.py +++ b/src/agent_comms/commander_parity/deletion.py @@ -19,10 +19,17 @@ from ..field_codec import FieldCodec from ..goal_actions import CreateProblemGoalAction from ..goals import CountCheckResult, OrderCheck -from ..progress_claims import MergedClaim from ..plan_phases import RecordPlanePhase +from ..progress_claims import MergedClaim from ..repo import Repo -from ..todos import CommitDelivery, FamilyDeletion, MechanicalOrderRun, Todo +from ..todos import ( + CheckedCommitDelivery, + CommitDelivery, + DeliveredProblemOrder, + FamilyDeletion, + MechanicalOrderRun, + Todo, +) from ..tools import CommsOrderDoneTool, CommsTodoCreateTool, ProblemOrdered from . import ParityComparison, ParityRow from .lifecycle import LifecycleObservation, LifecycleRun, MeasuredCommander @@ -35,8 +42,9 @@ class DeletionComparison(ParityComparison): @property def passed(self) -> bool: - return (self.current.passed and self.native.passed - and self.current.checks == self.native.checks) + return ( + self.current.passed and self.native.passed and self.current.checks == self.native.checks + ) def describe(self) -> str: return f"current: {self.current.describe()}; native: {self.native.describe()}" @@ -59,11 +67,11 @@ def delivery_command(cls, run: LifecycleRun, todo: Todo, commit: str) -> tuple[s @staticmethod def commit(repository: Path, text: str, message: str) -> str: - (repository / 'family.py').write_text(text) - Repo.git(repository, 'add', 'family.py') - Repo.git(repository, 'commit', '-qm', message) - Repo.git(repository, 'push', '-q', 'origin', 'main') - return Repo.git(repository, 'rev-parse', 'HEAD').strip() + (repository / "family.py").write_text(text) + Repo.git(repository, "add", "family.py") + Repo.git(repository, "commit", "-qm", message) + Repo.git(repository, "push", "-q", "origin", "main") + return Repo.git(repository, "rev-parse", "HEAD").strip() @classmethod def read(cls) -> LifecycleObservation: @@ -71,73 +79,118 @@ def read(cls) -> LifecycleObservation: # Keep source checkouts out of the copied worker's storage accounting. # Limits remain unchanged; all detached checks use this persistent owned base. rule = original.commander.comms.registry.storage_rule - with tempfile.TemporaryDirectory(prefix='parity-deletion-', - dir=rule.checkout_base) as checkouts: + with tempfile.TemporaryDirectory( + prefix="parity-deletion-", dir=rule.checkout_base + ) as checkouts: root = original.commander.root - (root / 'storage.json').write_text(json.dumps(FieldCodec.encode( - replace(rule, checkout_base=Path(checkouts))))) - (root / 'commander.json').write_text(original.commander.model_dump_json()) + (root / "storage.json").write_text( + json.dumps(FieldCodec.encode(replace(rule, checkout_base=Path(checkouts)))) + ) + (root / "commander.json").write_text(original.commander.model_dump_json()) run = replace(original, commander=MeasuredCommander.read_root(root)) return cls.exercise(run) @classmethod def exercise(cls, run: LifecycleRun) -> LifecycleObservation: repository = Path(run.commander.comms.registry.require(run.worker).worktree) - remote = repository.parent / 'deletion-origin.git' - Repo.git(repository, 'init', '-q', '-b', 'main') - Repo.git(repository, 'config', 'user.name', 'Deletion parity') - Repo.git(repository, 'config', 'user.email', 'deletion-parity@localhost') - Repo.git(repository, 'init', '--bare', '-q', '--initial-branch=main', str(remote)) - Repo.git(repository, 'remote', 'add', 'origin', str(remote)) - family = 'class RetiredDecision:\n pass\n\n' - cls.commit(repository, family + 'def decisions():\n return [RetiredDecision()]\n', - 'Declare the obsolete decision family and its caller') - count = OrderCheck(str(repository), shlex.join((sys.executable, '-c', - "import ast;from pathlib import Path;" - "print(sum(isinstance(node,ast.ClassDef) for node in " - "ast.walk(ast.parse(Path('family.py').read_text()))))")), CountCheckResult(0)) - deletion = FamilyDeletion('obsolete decision family', - FileLine(str(repository / 'family.py'), 1), count) + remote = repository.parent / "deletion-origin.git" + Repo.git(repository, "init", "-q", "-b", "main") + Repo.git(repository, "config", "user.name", "Deletion parity") + Repo.git(repository, "config", "user.email", "deletion-parity@localhost") + Repo.git(repository, "init", "--bare", "-q", "--initial-branch=main", str(remote)) + Repo.git(repository, "remote", "add", "origin", str(remote)) + family = "class RetiredDecision:\n pass\n\n" + cls.commit( + repository, + family + "def decisions():\n return [RetiredDecision()]\n", + "Declare the obsolete decision family and its caller", + ) + count = OrderCheck( + str(repository), + shlex.join( + ( + sys.executable, + "-c", + "import ast;from pathlib import Path;" + "print(sum(isinstance(node,ast.ClassDef) for node in " + "ast.walk(ast.parse(Path('family.py').read_text()))))", + ) + ), + CountCheckResult(0), + ) + deletion = FamilyDeletion( + "obsolete decision family", FileLine(str(repository / "family.py"), 1), count + ) initial = OrderFile.read(run.document.read_text()) - order = replace(initial.order, - check=OrderCheck(str(repository), shlex.join((sys.executable, '-c', - 'import family;assert not family.decisions()'))), - run=MechanicalOrderRun(), deletes=deletion) + order = replace( + initial.order, + check=OrderCheck( + str(repository), + shlex.join((sys.executable, "-c", "import family;assert not family.decisions()")), + ), + run=MechanicalOrderRun(), + deletes=deletion, + ) document = replace(initial, order=order) run.document.write_text(json.dumps(FieldCodec.encode(document))) output = io.StringIO() with contextlib.redirect_stdout(output): before = cls.send(run, document) - partial = cls.commit(repository, family + 'def decisions():\n return []\n', - 'Repair the caller but leave one obsolete family site') - refused = subprocess.run(cls.delivery_command(run, before, partial), - capture_output=True, text=True, timeout=90) + partial = cls.commit( + repository, + family + "def decisions():\n return []\n", + "Repair the caller but leave one obsolete family site", + ) + refused = subprocess.run( + cls.delivery_command(run, before, partial), capture_output=True, text=True, timeout=90 + ) after_refusal = run.commander.comms.goals.todos.get(before.id) remaining = count.recorded(partial, str(repository)) - complete = cls.commit(repository, 'def decisions():\n return []\n', - 'Delete the last obsolete family declaration') - accepted = subprocess.run(cls.delivery_command(run, before, complete), - capture_output=True, text=True, timeout=90) + complete = cls.commit( + repository, + "def decisions():\n return []\n", + "Delete the last obsolete family declaration", + ) + accepted = subprocess.run( + cls.delivery_command(run, before, complete), capture_output=True, text=True, timeout=90 + ) after_acceptance = run.commander.comms.goals.todos.get(before.id) zero = count.recorded(complete, str(repository)) primary = order.check.recorded(partial, str(repository)) + delivered = after_acceptance.order checks = ( - ('native order retained deletion intent', before.order.checked().deletes == deletion), - ('partial commit passed primary check', bool(primary) - and order.check.result.distance(primary[0]) == CountPrinted(0)), - ('partial commit retained one family site', bool(remaining) - and count.result.distance(remaining[0]) == CountPrinted(1)), - ('nonzero delivery refused', refused.returncode != 0), - ('refusal preserved pending order', before == after_refusal and after_refusal.pending), - ('zero count checked on delivered commit', bool(zero) - and count.result.distance(zero[0]) == CountPrinted(0)), - ('zero delivery accepted', accepted.returncode == 0 and not after_acceptance.pending), + ("native order retained deletion intent", before.order.checked().deletes == deletion), + ( + "partial commit passed primary check", + bool(primary) and order.check.result.distance(primary[0]) == CountPrinted(0), + ), + ( + "partial commit retained one family site", + bool(remaining) and count.result.distance(remaining[0]) == CountPrinted(1), + ), + ("nonzero delivery refused", refused.returncode != 0), + ("refusal preserved pending order", before == after_refusal and after_refusal.pending), + ( + "zero count checked on delivered commit", + bool(zero) and count.result.distance(zero[0]) == CountPrinted(0), + ), + ("zero delivery accepted", accepted.returncode == 0 and not after_acceptance.pending), + ( + "accepted delivery names the zero-site commit", + isinstance(delivered, DeliveredProblemOrder) + and isinstance(delivered.delivery, CheckedCommitDelivery) + and delivered.delivery.delivered_commit == complete, + ), + ("accepted delivery retained deletion intent", delivered.checked().deletes == deletion), ) return LifecycleObservation( - CommsOrderDoneTool.__qualname__, len((refused, accepted)), checks, - output.getvalue() + f'partial={partial}; zero={complete}; ' - f'refused={refused.returncode}: {refused.stderr}; ' - f'accepted={accepted.returncode}: {accepted.stdout}{accepted.stderr}') + CommsOrderDoneTool.__qualname__, + len((refused, accepted)), + checks, + output.getvalue() + f"partial={partial}; zero={complete}; " + f"refused={refused.returncode}: {refused.stdout}{refused.stderr}; " + f"accepted={accepted.returncode}: {accepted.stdout}{accepted.stderr}", + ) class CurrentDeletionPath(DeletionPath): @@ -147,39 +200,72 @@ def send(cls, run: LifecycleRun, document: OrderFile) -> Todo: @classmethod def delivery_command(cls, run: LifecycleRun, todo: Todo, commit: str) -> tuple[str, ...]: - return (sys.executable, '-c', 'from agent_comms.commander.cli import main;main()', - '--config', str(run.commander.root / 'commander.json'), - 'order', 'done', todo.id, commit) + return ( + sys.executable, + "-c", + "import sys;from agent_comms.commander.order import Commander,DoneVerb;" + "DoneVerb(unit=sys.argv[2],message=sys.argv[3],sha=sys.argv[4]).run(" + "Commander.read_root(sys.argv[1]))", + str(run.commander.root), + run.worker, + todo.order.checked().id, + commit, + ) class NativeDeletionPath(DeletionPath): @classmethod def send(cls, run: LifecycleRun, document: OrderFile) -> Todo: - result: ProblemOrdered = run.commander.call(CommsTodoCreateTool( - owner=run.worker, order=CreateProblemGoalAction( - problem='P1', repo=Repo.of(Path(document.order.check.directory)).github, - text=document.defect, order=document.order, - request=document.order.request( - run.commander.comms.registry.require(run.commander.commander), ('P1',)), - deadline=deadline(run.due), commander=run.commander.commander))) + result = run.commander.call( + CommsTodoCreateTool( + owner=run.worker, + order=CreateProblemGoalAction( + problem="P1", + repo=Repo.of(Path(document.order.check.directory)).github, + text=document.defect, + order=document.order, + request=document.order.request( + run.commander.comms.registry.require(run.commander.commander), ("P1",) + ), + deadline=deadline(run.due), + commander=run.commander.commander, + ), + ) + ) + if not isinstance(result, ProblemOrdered): + raise TypeError("Native creation returned no ordered problem") return result.todo @classmethod def delivery_command(cls, run: LifecycleRun, todo: Todo, commit: str) -> tuple[str, ...]: - request = CommsOrderDoneTool(problem=todo.id, order_id=todo.order.id, + request = CommsOrderDoneTool( + problem=todo.id, + order_id=todo.order.checked().id, delivery=CommitDelivery(commit=commit, delivered_commit=commit), - claims=(MergedClaim(commit),)) + claims=(MergedClaim(commit),), + ) arguments = FieldCodec.encode(request) arguments.pop(request.family_discriminator) - return (sys.executable, '-m', 'agent_comms', '--root', str(run.commander.root), - 'tool', '--from', run.commander.commander, '--tool', request.declared_name, - '--arguments', json.dumps(arguments)) + return ( + sys.executable, + "-m", + "agent_comms.cli", + "--root", + str(run.commander.root), + "tool", + "--from", + run.commander.commander, + "--tool", + request.declared_name, + "--arguments", + json.dumps(arguments), + ) class DeletionTargetParityRow(ParityRow): number = 31 - capability = 'An order states its intent as a deletion target' - current = 'Current order command with a declared FamilyDeletion' - native = 'CommsTodoCreateTool and CommsOrderDoneTool check the delivered family count' + capability = "An order states its intent as a deletion target" + current = "Current order command with a declared FamilyDeletion" + native = "CommsTodoCreateTool and CommsOrderDoneTool check the delivered family count" phases = (RecordPlanePhase,) comparison = DeletionComparison.read From e9f6345b09e510898e1f815360ca2ce103ece4f6 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:55:35 -0400 Subject: [PATCH 45/66] Let declared requests compose argument projection and let the CLI own tool argv --- src/agent_comms/tools.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/agent_comms/tools.py b/src/agent_comms/tools.py index cf39fb398f..2ef7eec76c 100644 --- a/src/agent_comms/tools.py +++ b/src/agent_comms/tools.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import sys import threading from abc import abstractmethod from collections.abc import Mapping @@ -68,7 +69,7 @@ TodoOperation, VerdictAudience) from .change_report import ChangeReport from .messages import InfoKind, MessageKind, NoticeRole, SendableMessageKind -from .native_tools import NativeToolDeclaration, NativeRequestTools +from .native_tools import ArgumentTool, NativeToolDeclaration, NativeRequestTools from .tool_caller import ToolCaller, ThreadCaller, NoThreadCaller, ThreadListing from .thread_identity import ThreadIncarnation from .cli_result import ToolCatalogResult @@ -292,7 +293,7 @@ def tool_field( @dataclass(frozen=True, kw_only=True) class ToolRequest[Caller: ToolCaller = ToolCaller]( - ThreadCommandAvailability, NativeRequestTools, NativeToolDeclaration + ArgumentTool, ThreadCommandAvailability, NativeRequestTools, NativeToolDeclaration ): description: ClassVar[str] bus_effect: ClassVar[str] = ("Changes the bus. Tristan has authorized claude-ai " @@ -328,6 +329,10 @@ def schema(cls) -> dict[str, object]: ) return result + def cli_command(self, caller: str | None = None) -> InvokeCliCommand: + """Project this declaration into Core's existing external tool command.""" + return InvokeCliCommand(caller=caller, tool=self.declared_name, arguments=self.arguments) + @classmethod def invoke( cls, comms: Comms, arguments: Mapping[str, object], *, caller: ToolCaller @@ -1362,6 +1367,15 @@ class InvokeCliCommand(CliCommand, declared_name="tool"): "--arguments", help="Tool arguments as a JSON object", default_factory=dict, parser_default="{}") + def argv(self, root: Path) -> tuple[str, ...]: + """The CLI boundary owns its module, grammar and argument serialization.""" + from . import cli + + caller = ('--from', self.caller) if self.caller is not None else () + return (sys.executable, '-m', cli.__name__, '--root', str(root), + self.declared_name, *caller, '--tool', self.tool, + '--arguments', json.dumps(self.arguments)) + def apply(self, ctx: Comms) -> dict[str, JsonValue]: return invoke_tool(ctx, self.tool, self.arguments, caller=ToolCaller.from_actor(self.caller)) From eca51be1d990062a26565ccf9e99bf059e925c41 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:56:33 -0400 Subject: [PATCH 46/66] Use caller-owned identity and declared native CLI projection in deletion parity --- plans/commander-native-plan.md | 2 +- src/agent_comms/commander_parity/deletion.py | 18 ++---------------- src/agent_comms/tools.py | 9 +++++---- 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/plans/commander-native-plan.md b/plans/commander-native-plan.md index 05042371b2..4fbae94e3a 100644 --- a/plans/commander-native-plan.md +++ b/plans/commander-native-plan.md @@ -61,7 +61,7 @@ | 28 | Measured load and self-report | `capacity.py` unit count | `commander.capacity.Load.of` reports orders, overdue, edits and forks; self-report comparison unverified | `control_plane` | | 29 | No handoff of finished, checked work | none; scratch transfer moved P507 after its fix was pushed and checked | Transfer refuses delivered checked work; that owner merges; check is reused | `record_plane` | | 30 | A proof is bound to the build it ran on | none; screenshots and logs are bare paths | Build run records runtime and Core/Toad revisions; close requires delivered commit | `record_plane` | -| 31 | An order states its intent as a deletion target | none; P520 renamed a decision rather than deleting it | Order owns deletion family/site/count; delivery refuses nonzero or renamed decisions | `record_plane` | +| 31 | An order states its intent as a deletion target | Current order command with a declared FamilyDeletion | CommsTodoCreateTool and CommsOrderDoneTool check the delivered family count | `record_plane` | | 32 | Where units' time goes, classified | none; activity.log lacks durations and turn boundaries | `UnitTime`: model, build/check/test/shell, dependency/commander waits and idle; classify large waits | `commander_page`, `control_plane` | | 33 | Order progress beside its deadline; the unit never sees a due time | `order.py board`, `overdue`, `due`, `send`; `rotate.py` | Derived `OrderProgress`; merged work draws no strike; first commit 10 min, later progress 15 min STALL; units see no due time | `one_order_owner`, `control_plane` | | 34 | Several commanders on one machine and bus, one per project | none; a second commander.json install overwrites the first | Registry commander/project ownership; caller from --from; scoped orders, strikes, load, board, alarms and forks; raw-send guard; duplicate project commander refused | `one_order_owner`, `cutover` | diff --git a/src/agent_comms/commander_parity/deletion.py b/src/agent_comms/commander_parity/deletion.py index d4655a9c13..e66014991a 100644 --- a/src/agent_comms/commander_parity/deletion.py +++ b/src/agent_comms/commander_parity/deletion.py @@ -30,6 +30,7 @@ MechanicalOrderRun, Todo, ) +from ..tool_caller import ThreadCaller from ..tools import CommsOrderDoneTool, CommsTodoCreateTool, ProblemOrdered from . import ParityComparison, ParityRow from .lifecycle import LifecycleObservation, LifecycleRun, MeasuredCommander @@ -244,22 +245,7 @@ def delivery_command(cls, run: LifecycleRun, todo: Todo, commit: str) -> tuple[s delivery=CommitDelivery(commit=commit, delivered_commit=commit), claims=(MergedClaim(commit),), ) - arguments = FieldCodec.encode(request) - arguments.pop(request.family_discriminator) - return ( - sys.executable, - "-m", - "agent_comms.cli", - "--root", - str(run.commander.root), - "tool", - "--from", - run.commander.commander, - "--tool", - request.declared_name, - "--arguments", - json.dumps(arguments), - ) + return request.cli_command(ThreadCaller(run.commander.commander)).argv(run.commander.root) class DeletionTargetParityRow(ParityRow): diff --git a/src/agent_comms/tools.py b/src/agent_comms/tools.py index 2ef7eec76c..b4742c291d 100644 --- a/src/agent_comms/tools.py +++ b/src/agent_comms/tools.py @@ -329,9 +329,10 @@ def schema(cls) -> dict[str, object]: ) return result - def cli_command(self, caller: str | None = None) -> InvokeCliCommand: + def cli_command(self, caller: ToolCaller) -> InvokeCliCommand: """Project this declaration into Core's existing external tool command.""" - return InvokeCliCommand(caller=caller, tool=self.declared_name, arguments=self.arguments) + return InvokeCliCommand(caller=caller.acting_name(''), + tool=self.declared_name, arguments=self.arguments) @classmethod def invoke( @@ -1371,9 +1372,9 @@ def argv(self, root: Path) -> tuple[str, ...]: """The CLI boundary owns its module, grammar and argument serialization.""" from . import cli - caller = ('--from', self.caller) if self.caller is not None else () + caller = ToolCaller.from_actor(self.caller) return (sys.executable, '-m', cli.__name__, '--root', str(root), - self.declared_name, *caller, '--tool', self.tool, + self.declared_name, '--from', caller.acting_name(''), '--tool', self.tool, '--arguments', json.dumps(self.arguments)) def apply(self, ctx: Comms) -> dict[str, JsonValue]: From 928cfa5f57a21824712be8bd27b1168a7fe8f324 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:58:03 -0400 Subject: [PATCH 47/66] Bind the native CLI module at its ordinary module import boundary --- src/agent_comms/tools.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/agent_comms/tools.py b/src/agent_comms/tools.py index b4742c291d..673bbf334a 100644 --- a/src/agent_comms/tools.py +++ b/src/agent_comms/tools.py @@ -13,6 +13,7 @@ from pydantic import JsonValue +from . import cli as comms_cli from .commander.capacity import Load from .progress_claims import ProgressClaim from .pi_vocabulary import ServiceTier, ThinkingLevel, ThinkingLevelField @@ -1370,10 +1371,8 @@ class InvokeCliCommand(CliCommand, declared_name="tool"): def argv(self, root: Path) -> tuple[str, ...]: """The CLI boundary owns its module, grammar and argument serialization.""" - from . import cli - caller = ToolCaller.from_actor(self.caller) - return (sys.executable, '-m', cli.__name__, '--root', str(root), + return (sys.executable, '-m', comms_cli.__name__, '--root', str(root), self.declared_name, '--from', caller.acting_name(''), '--tool', self.tool, '--arguments', json.dumps(self.arguments)) From 4ca8aabca559cc65d167b9c380039ff9adf2e21d Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:59:15 -0400 Subject: [PATCH 48/66] Delete the other hand-built native tool argv and envelope projection --- tools/proofs/cn_phase3_record.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/tools/proofs/cn_phase3_record.py b/tools/proofs/cn_phase3_record.py index d925660ffd..54cda2f8d8 100644 --- a/tools/proofs/cn_phase3_record.py +++ b/tools/proofs/cn_phase3_record.py @@ -11,12 +11,10 @@ import os import shutil import subprocess -import sys import time from dataclasses import replace from pathlib import Path -from agent_comms import cli as comms_cli from agent_comms.change_report import FileLine from agent_comms.comms import Comms from agent_comms.field_codec import FieldCodec @@ -50,22 +48,7 @@ def cli[Caller: ToolCaller](caller: str, tool: ToolRequest[Caller]) -> str: - arguments = FieldCodec.encode(tool) - del arguments[tool.family_discriminator] - command = [ - sys.executable, - "-m", - comms_cli.__name__, - "--root", - str(ROOT), - "tool", - "--from", - caller, - "--tool", - tool.declared_name, - "--arguments", - json.dumps(arguments), - ] + command = tool.cli_command(ToolCaller.from_actor(caller)).argv(ROOT) result = subprocess.run( command, capture_output=True, From dde7370a4ee3c1a2d43e0c5e8128f862d7bbc79e Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 20 Oct 2026 00:04:10 -0400 Subject: [PATCH 49/66] Use the shared bounded lifecycle producer and typed delivery outcomes --- src/agent_comms/commander_parity/deletion.py | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/agent_comms/commander_parity/deletion.py b/src/agent_comms/commander_parity/deletion.py index e66014991a..f199ea9733 100644 --- a/src/agent_comms/commander_parity/deletion.py +++ b/src/agent_comms/commander_parity/deletion.py @@ -6,7 +6,6 @@ import io import json import shlex -import subprocess import sys import tempfile from abc import ABC, abstractmethod @@ -14,7 +13,7 @@ from pathlib import Path from ..change_report import FileLine -from ..child_process import CountPrinted +from ..child_process import CountPrinted, ExitedOutcome from ..commander.order import OrderFile, deadline from ..field_codec import FieldCodec from ..goal_actions import CreateProblemGoalAction @@ -142,9 +141,7 @@ def exercise(cls, run: LifecycleRun) -> LifecycleObservation: family + "def decisions():\n return []\n", "Repair the caller but leave one obsolete family site", ) - refused = subprocess.run( - cls.delivery_command(run, before, partial), capture_output=True, text=True, timeout=90 - ) + refused = run.invoke_command(cls.delivery_command(run, before, partial)) after_refusal = run.commander.comms.goals.todos.get(before.id) remaining = count.recorded(partial, str(repository)) complete = cls.commit( @@ -152,9 +149,7 @@ def exercise(cls, run: LifecycleRun) -> LifecycleObservation: "def decisions():\n return []\n", "Delete the last obsolete family declaration", ) - accepted = subprocess.run( - cls.delivery_command(run, before, complete), capture_output=True, text=True, timeout=90 - ) + accepted = run.invoke_command(cls.delivery_command(run, before, complete)) after_acceptance = run.commander.comms.goals.todos.get(before.id) zero = count.recorded(complete, str(repository)) primary = order.check.recorded(partial, str(repository)) @@ -169,13 +164,16 @@ def exercise(cls, run: LifecycleRun) -> LifecycleObservation: "partial commit retained one family site", bool(remaining) and count.result.distance(remaining[0]) == CountPrinted(1), ), - ("nonzero delivery refused", refused.returncode != 0), + ("nonzero delivery refused", refused.outcome == ExitedOutcome(1)), ("refusal preserved pending order", before == after_refusal and after_refusal.pending), ( "zero count checked on delivered commit", bool(zero) and count.result.distance(zero[0]) == CountPrinted(0), ), - ("zero delivery accepted", accepted.returncode == 0 and not after_acceptance.pending), + ( + "zero delivery accepted", + accepted.outcome == ExitedOutcome(0) and not after_acceptance.pending, + ), ( "accepted delivery names the zero-site commit", isinstance(delivered, DeliveredProblemOrder) @@ -189,8 +187,10 @@ def exercise(cls, run: LifecycleRun) -> LifecycleObservation: len((refused, accepted)), checks, output.getvalue() + f"partial={partial}; zero={complete}; " - f"refused={refused.returncode}: {refused.stdout}{refused.stderr}; " - f"accepted={accepted.returncode}: {accepted.stdout}{accepted.stderr}", + f"refused={refused.outcome.summary}: " + f"{refused.stdout.decode(errors='replace')}{refused.stderr.decode(errors='replace')}; " + f"accepted={accepted.outcome.summary}: " + f"{accepted.stdout.decode(errors='replace')}{accepted.stderr.decode(errors='replace')}", ) From 472b1f2463c1a4e0ad5dbc5ae535f750036ff39f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 20 Oct 2026 00:04:45 -0400 Subject: [PATCH 50/66] Remove codec imports made obsolete by native declaration ownership --- tools/proofs/cn_phase3_record.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tools/proofs/cn_phase3_record.py b/tools/proofs/cn_phase3_record.py index 54cda2f8d8..bbadb40bd5 100644 --- a/tools/proofs/cn_phase3_record.py +++ b/tools/proofs/cn_phase3_record.py @@ -7,7 +7,6 @@ from __future__ import annotations import argparse -import json import os import shutil import subprocess @@ -17,7 +16,6 @@ from agent_comms.change_report import FileLine from agent_comms.comms import Comms -from agent_comms.field_codec import FieldCodec from agent_comms.goal_actions import CreateProblemGoalAction, SetGoalAction from agent_comms.goals import CountCheckResult, OrderCheck, RequestGoalOrder from agent_comms.progress_claims import MergedClaim From f6679cfd2053296e25c57fd033bd517053a37407 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 20 Oct 2026 00:07:04 -0400 Subject: [PATCH 51/66] Pass candidate Python environment through the shared process owner explicitly --- src/agent_comms/commander_parity/deletion.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/agent_comms/commander_parity/deletion.py b/src/agent_comms/commander_parity/deletion.py index f199ea9733..78834661c6 100644 --- a/src/agent_comms/commander_parity/deletion.py +++ b/src/agent_comms/commander_parity/deletion.py @@ -141,7 +141,9 @@ def exercise(cls, run: LifecycleRun) -> LifecycleObservation: family + "def decisions():\n return []\n", "Repair the caller but leave one obsolete family site", ) - refused = run.invoke_command(cls.delivery_command(run, before, partial)) + refused = run.invoke_command( + cls.delivery_command(run, before, partial), env=run.python_environment() + ) after_refusal = run.commander.comms.goals.todos.get(before.id) remaining = count.recorded(partial, str(repository)) complete = cls.commit( @@ -149,7 +151,9 @@ def exercise(cls, run: LifecycleRun) -> LifecycleObservation: "def decisions():\n return []\n", "Delete the last obsolete family declaration", ) - accepted = run.invoke_command(cls.delivery_command(run, before, complete)) + accepted = run.invoke_command( + cls.delivery_command(run, before, complete), env=run.python_environment() + ) after_acceptance = run.commander.comms.goals.todos.get(before.id) zero = count.recorded(complete, str(repository)) primary = order.check.recorded(partial, str(repository)) From fe8006bfffc7d157cded4c624a4df10f9c295dba Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:53:06 -0400 Subject: [PATCH 52/66] P311 bind delivered proof to the installed Core and Toad pair --- src/agent_comms/commander_parity/lifecycle.py | 54 ++++++++++++++++--- src/agent_comms/launch_runtime.py | 50 ++++++++++++++++- src/agent_comms/todos.py | 15 +++++- 3 files changed, 111 insertions(+), 8 deletions(-) diff --git a/src/agent_comms/commander_parity/lifecycle.py b/src/agent_comms/commander_parity/lifecycle.py index 2c0e441cb0..9010969012 100644 --- a/src/agent_comms/commander_parity/lifecycle.py +++ b/src/agent_comms/commander_parity/lifecycle.py @@ -7,6 +7,7 @@ import json import os import shlex +import subprocess import sys import time from abc import ABC, abstractmethod @@ -25,12 +26,16 @@ from ..commander.request import RequestAddVerb from ..field_codec import FieldCodec from ..goals import CountCheckResult, OrderCheck, ProblemGoalOrder +from ..launch_runtime import StackBuild +from ..progress_claims import WorkingClaim from ..todos import ( ApplicationOrderRun, MergedProgress, Moving, NoCommitProgress, PendingProblemOrder, + OperationDelivery, + FileDeliveryEvidence, Todo, ) from ..tool_caller import ThreadCaller, ToolCaller @@ -39,6 +44,7 @@ CommsChangeProblemTool, CommsFireTool, CommsOrderTool, + CommsOrderDoneTool, CommsProblemCloseTool, CommsTodoCreateTool, CommsTodoListTool, @@ -325,12 +331,48 @@ def checks( class BuildBoundProofCase(InstalledEvidenceCase): @classmethod - def read(cls) -> ParityComparison: - closing = super().read() - return UnverifiedParity( - "Missing native owner: a build-bound application-run record linking the " - "runtime and Core/Toad revisions to closing evidence. The actual close " - "owner currently accepts a bare artifact path. " + closing.describe() + def exercise(cls, run: LifecycleRun) -> tuple[Todo, ...]: + delivered, closed = super().exercise(run) + stale_run = replace(run, artifact=run.artifact.with_name("older-build-proof.json")) + created = stale_run.create() + installed = StackBuild.installed() + older = subprocess.check_output( + ("git", "rev-parse", f"{installed.core}^"), + cwd=created.order.check.directory, text=True, + ).strip() + stale_run.artifact.write_text(json.dumps(FieldCodec.encode( + stale_run.commander.comms.registry.require(stale_run.worker).goal))) + stale_run.decisions.write_text("[]") + stale_run.commander.call(CommsOrderDoneTool( + problem=created.id, order_id=created.order.id, + delivery=OperationDelivery( + evidence=FileDeliveryEvidence(str(stale_run.artifact)), + proof_build=replace(installed, core=older)), + claims=(WorkingClaim(str(stale_run.artifact)),))) + stale = stale_run.commander.comms.goals.todos.get(created.id) + try: + stale_run.commander.close( + stale.id, created.order.run.text, str(stale_run.artifact)) + except ValueError as error: + print(error) + return delivered, closed, stale, stale_run.commander.comms.goals.todos.get(stale.id) + + @classmethod + def checks( + cls, run: LifecycleRun, rows: tuple[Todo, ...], native: tuple[ToolResult, ...], output: str + ) -> tuple[tuple[str, bool], ...]: + delivered, closed, stale, after = rows + current_build = delivered.order.delivery.proof_build + older_build = stale.order.delivery.proof_build + return ( + ("installed proof closes", closed.order.accepted and not closed.state.open), + ("build recorded durably", bool(current_build and older_build) + and FieldCodec.decode(Todo, FieldCodec.encode(stale)) == stale), + ("older proof refused without mutation", stale == after and after.state.open), + ("both revisions named", bool(current_build and older_build) + and all(revision in output for revision in ( + current_build.core, current_build.toad, older_build.core, older_build.toad))), + ("native close returned", bool(native)), ) diff --git a/src/agent_comms/launch_runtime.py b/src/agent_comms/launch_runtime.py index d69b9676c7..d2eaa9fa10 100644 --- a/src/agent_comms/launch_runtime.py +++ b/src/agent_comms/launch_runtime.py @@ -3,6 +3,8 @@ from __future__ import annotations import os +import json +import subprocess import sys from collections.abc import Callable, Mapping, MutableMapping from dataclasses import dataclass, field, fields, replace @@ -11,7 +13,7 @@ from .declared_family import DeclaredFamily from .errors import RelationViolationError -from .field_codec import FieldCodec, JsonShapeFamily, JsonShapeMember, PathText, TextRepresentation +from .field_codec import FieldCodec, GitCommitText, JsonShapeFamily, JsonShapeMember, PathText, TextRepresentation class CallingThreadUnavailable(RelationViolationError): @@ -49,6 +51,52 @@ def require_available(self) -> None: raise ValueError("Target interpreter is unavailable") +@dataclass(frozen=True) +class StackBuild: + """The installed pair, observed from its interpreter rather than this caller's imports.""" + + core: Annotated[str, GitCommitText] + toad: Annotated[str, GitCommitText] + + @classmethod + def installed(cls) -> StackBuild: + from .owner_cutover import StoppedOwnerInstallation + + runtime = LaunchRuntime(StoppedOwnerInstallation.current_pointer() / "bin/python") + runtime.require_available() + output = subprocess.check_output( + (str(runtime.interpreter), "-c", + "import json; from importlib.metadata import distribution; " + "from agent_comms.package_stamp import STAMP; " + "print(json.dumps([STAMP.commit(), " + "distribution('batrachian-toad').read_text('direct_url.json')]))"), + env=runtime.environment(os.environ), text=True, timeout=30, + ) + core, metadata = FieldCodec.decode(tuple[str, str], json.loads(output)) + source = FieldCodec.decode(DistributionSource, json.loads(metadata)) + return cls(core, source.vcs_info.commit_id) + + def require_same(self, installed: StackBuild) -> None: + if self != installed: + raise ValueError( + f"Proof build Core {self.core}, Toad {self.toad} differs from installed " + f"Core {installed.core}, Toad {installed.toad}; run the proof again." + ) + + +@dataclass(frozen=True) +class VcsSource: + commit_id: Annotated[str, GitCommitText] + vcs: str + requested_revision: str + + +@dataclass(frozen=True) +class DistributionSource: + url: str + vcs_info: VcsSource + + class RestoredRuntime(LaunchRuntime): """Original-process recovery preserves its original import environment too.""" diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index a1122786a5..399d6bc896 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -34,6 +34,7 @@ from .external_transcript import ExternalTranscript from .messages import InfoKind, Message from .command import Command +from .launch_runtime import StackBuild from .declared_family import DeclaredFamily from .errors import ToolRefusedError from .field_codec import ( @@ -84,6 +85,7 @@ class OrderDelivery(DeclaredFamily, StoryEvent, affix="Delivery"): at: EventTime = field(default_factory=UnrecordedEventTime) evidence: DeliveryEvidence = field(default_factory=NoDeliveryEvidence, metadata={"wire_omit_default": True}) + proof_build: StackBuild | None = field(default=None, metadata={"wire_omit_default": True}) @property def work(self) -> str: @@ -109,7 +111,17 @@ async def verified(self, todo: Todo, registry: Registration) -> OrderDelivery: 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)) + return replace(self, at=await self.delivery_time(check), + proof_build=self.proof_build or await asyncio.to_thread(StackBuild.installed)) + + def require_current_build(self) -> None: + installed = StackBuild.installed() + if self.proof_build is None: + raise ValueError( + f"Proof build is unrecorded; installed Core {installed.core}, " + f"Toad {installed.toad}; run and record a new proof." + ) + self.proof_build.require_same(installed) @abstractmethod async def require_source(self, check): ... @@ -1687,6 +1699,7 @@ def corrected(self, correction: CorrectCheckTodo) -> ProblemOrder: def installed(self, works, artifact): self.run.require_closing(works) + self.delivery.require_current_build() return self.copy_as(InstalledProblemOrder, works=works, artifact=artifact) def require_checked(self, problem: str) -> None: From d3d2c91deca0a77e03ae359b8f57e5d1b15439ac Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:57:49 -0400 Subject: [PATCH 53/66] Model missing proof builds explicitly and exercise copied-bus close --- src/agent_comms/commander_parity/lifecycle.py | 76 ++++++++---- src/agent_comms/launch_runtime.py | 108 +++++++++++++---- src/agent_comms/owner_cutover.py | 6 +- src/agent_comms/todos.py | 38 +++--- tests/conftest.py | 4 + tests/runtime_publication_journey.py | 4 +- tests/test_proof_build.py | 109 ++++++++++++++++++ tests/test_runtime_publication.py | 8 +- tools/publish_runtime.py | 10 +- 9 files changed, 281 insertions(+), 82 deletions(-) create mode 100644 tests/test_proof_build.py diff --git a/src/agent_comms/commander_parity/lifecycle.py b/src/agent_comms/commander_parity/lifecycle.py index 9010969012..db6cd8f395 100644 --- a/src/agent_comms/commander_parity/lifecycle.py +++ b/src/agent_comms/commander_parity/lifecycle.py @@ -26,16 +26,17 @@ from ..commander.request import RequestAddVerb from ..field_codec import FieldCodec from ..goals import CountCheckResult, OrderCheck, ProblemGoalOrder -from ..launch_runtime import StackBuild +from ..launch_runtime import ProofBuildMismatchError, RecordedBuildIdentity from ..progress_claims import WorkingClaim from ..todos import ( ApplicationOrderRun, + DeliveredProblemOrder, + FileDeliveryEvidence, MergedProgress, Moving, NoCommitProgress, - PendingProblemOrder, OperationDelivery, - FileDeliveryEvidence, + PendingProblemOrder, Todo, ) from ..tool_caller import ThreadCaller, ToolCaller @@ -43,8 +44,8 @@ from ..tools import ( CommsChangeProblemTool, CommsFireTool, - CommsOrderTool, CommsOrderDoneTool, + CommsOrderTool, CommsProblemCloseTool, CommsTodoCreateTool, CommsTodoListTool, @@ -334,26 +335,40 @@ class BuildBoundProofCase(InstalledEvidenceCase): def exercise(cls, run: LifecycleRun) -> tuple[Todo, ...]: delivered, closed = super().exercise(run) stale_run = replace(run, artifact=run.artifact.with_name("older-build-proof.json")) + stale_run.decisions.write_text(json.dumps([stale_run.worker])) created = stale_run.create() - installed = StackBuild.installed() + installed = RecordedBuildIdentity.installed() older = subprocess.check_output( ("git", "rev-parse", f"{installed.core}^"), - cwd=created.order.check.directory, text=True, + cwd=created.order.checked().check.directory, + text=True, ).strip() - stale_run.artifact.write_text(json.dumps(FieldCodec.encode( - stale_run.commander.comms.registry.require(stale_run.worker).goal))) + stale_run.artifact.write_text( + json.dumps( + FieldCodec.encode(stale_run.commander.comms.registry.require(stale_run.worker).goal) + ) + ) stale_run.decisions.write_text("[]") - stale_run.commander.call(CommsOrderDoneTool( - problem=created.id, order_id=created.order.id, - delivery=OperationDelivery( - evidence=FileDeliveryEvidence(str(stale_run.artifact)), - proof_build=replace(installed, core=older)), - claims=(WorkingClaim(str(stale_run.artifact)),))) + stale_run.commander.call( + CommsOrderDoneTool( + problem=created.id, + order_id=created.order.checked().id, + delivery=OperationDelivery( + evidence=FileDeliveryEvidence(str(stale_run.artifact)), + proof_build=replace(installed, core=older), + ), + claims=(WorkingClaim(str(stale_run.artifact)),), + ) + ) stale = stale_run.commander.comms.goals.todos.get(created.id) try: stale_run.commander.close( - stale.id, created.order.run.text, str(stale_run.artifact)) - except ValueError as error: + stale.id, + "Read the copied native goal and confirm its recorded source location " + "and assigned worker.", + str(stale_run.artifact), + ) + except ProofBuildMismatchError as error: print(error) return delivered, closed, stale, stale_run.commander.comms.goals.todos.get(stale.id) @@ -362,16 +377,31 @@ def checks( cls, run: LifecycleRun, rows: tuple[Todo, ...], native: tuple[ToolResult, ...], output: str ) -> tuple[tuple[str, bool], ...]: delivered, closed, stale, after = rows - current_build = delivered.order.delivery.proof_build - older_build = stale.order.delivery.proof_build + assert isinstance(delivered.order, DeliveredProblemOrder) + assert isinstance(stale.order, DeliveredProblemOrder) + current_build = delivered.order.delivery.proof_build.recorded() + older_build = stale.order.delivery.proof_build.recorded() return ( ("installed proof closes", closed.order.accepted and not closed.state.open), - ("build recorded durably", bool(current_build and older_build) - and FieldCodec.decode(Todo, FieldCodec.encode(stale)) == stale), + ( + "build recorded durably", + bool(current_build and older_build) + and FieldCodec.decode(Todo, FieldCodec.encode(stale)) == stale, + ), ("older proof refused without mutation", stale == after and after.state.open), - ("both revisions named", bool(current_build and older_build) - and all(revision in output for revision in ( - current_build.core, current_build.toad, older_build.core, older_build.toad))), + ( + "both revisions named", + bool(current_build and older_build) + and all( + revision in output + for revision in ( + current_build.core, + current_build.toad, + older_build.core, + older_build.toad, + ) + ), + ), ("native close returned", bool(native)), ) diff --git a/src/agent_comms/launch_runtime.py b/src/agent_comms/launch_runtime.py index d2eaa9fa10..4f421ffe04 100644 --- a/src/agent_comms/launch_runtime.py +++ b/src/agent_comms/launch_runtime.py @@ -2,10 +2,11 @@ from __future__ import annotations -import os import json +import os import subprocess import sys +from abc import abstractmethod from collections.abc import Callable, Mapping, MutableMapping from dataclasses import dataclass, field, fields, replace from pathlib import Path @@ -13,7 +14,14 @@ from .declared_family import DeclaredFamily from .errors import RelationViolationError -from .field_codec import FieldCodec, GitCommitText, JsonShapeFamily, JsonShapeMember, PathText, TextRepresentation +from .field_codec import ( + FieldCodec, + GitCommitText, + JsonShapeFamily, + JsonShapeMember, + PathText, + TextRepresentation, +) class CallingThreadUnavailable(RelationViolationError): @@ -31,6 +39,14 @@ def running(cls) -> LaunchRuntime: """Keep ordinary launches on the calling process's installation.""" return cls(Path(sys.executable)) + @staticmethod + def current_pointer() -> Path: + return Path.home() / ".local/share/agent-comms/current" + + @classmethod + def installed(cls) -> LaunchRuntime: + return cls(cls.current_pointer() / "bin/python") + @property def prefix(self) -> Path: return self.interpreter.parent.parent.resolve() @@ -41,7 +57,9 @@ def native_entrypoint(self) -> str: def environment(self, source: Mapping[str, str]) -> dict[str, str]: configuration = replace( - RestartEnvironment.inherit(source).for_interpreter(self.interpreter).without_pythonpath(), + RestartEnvironment.inherit(source) + .for_interpreter(self.interpreter) + .without_pythonpath(), virtual_env=str(self.prefix), ) return configuration.apply_runtime(source) @@ -51,34 +69,66 @@ def require_available(self) -> None: raise ValueError("Target interpreter is unavailable") +class ProofBuildMismatchError(ValueError): + """A retained proof does not identify the currently installed pair.""" + + +class BuildIdentity(DeclaredFamily, affix="BuildIdentity"): + @abstractmethod + def recorded(self) -> RecordedBuildIdentity: ... + + @abstractmethod + def require_installed(self) -> None: ... + + @dataclass(frozen=True) -class StackBuild: +class UnrecordedBuildIdentity(BuildIdentity): + def recorded(self) -> RecordedBuildIdentity: + return RecordedBuildIdentity.installed() + + def require_installed(self) -> None: + installed = RecordedBuildIdentity.installed() + raise ProofBuildMismatchError( + f"Proof build is unrecorded; installed Core {installed.core}, " + f"Toad {installed.toad}; run and record a new proof." + ) + + +@dataclass(frozen=True) +class RecordedBuildIdentity(BuildIdentity): """The installed pair, observed from its interpreter rather than this caller's imports.""" core: Annotated[str, GitCommitText] toad: Annotated[str, GitCommitText] - @classmethod - def installed(cls) -> StackBuild: - from .owner_cutover import StoppedOwnerInstallation + def recorded(self) -> RecordedBuildIdentity: + return self - runtime = LaunchRuntime(StoppedOwnerInstallation.current_pointer() / "bin/python") + @classmethod + def installed(cls) -> RecordedBuildIdentity: + runtime = LaunchRuntime.installed() runtime.require_available() output = subprocess.check_output( - (str(runtime.interpreter), "-c", - "import json; from importlib.metadata import distribution; " - "from agent_comms.package_stamp import STAMP; " - "print(json.dumps([STAMP.commit(), " - "distribution('batrachian-toad').read_text('direct_url.json')]))"), - env=runtime.environment(os.environ), text=True, timeout=30, + ( + str(runtime.interpreter), + "-c", + "import json; from importlib.metadata import distribution; " + "from agent_comms.package_stamp import STAMP; " + "print(json.dumps([STAMP.commit(), " + "distribution('batrachian-toad').read_text('direct_url.json')]))", + ), + env=runtime.environment(os.environ), + text=True, + timeout=30, ) core, metadata = FieldCodec.decode(tuple[str, str], json.loads(output)) source = FieldCodec.decode(DistributionSource, json.loads(metadata)) return cls(core, source.vcs_info.commit_id) - def require_same(self, installed: StackBuild) -> None: + def require_installed(self) -> None: + installed = self.installed() if self != installed: - raise ValueError( + raise ProofBuildMismatchError( f"Proof build Core {self.core}, Toad {self.toad} differs from installed " f"Core {installed.core}, Toad {installed.toad}; run the proof again." ) @@ -184,6 +234,7 @@ class RestartEnvironment(EnvironmentRecord): default=default_agent_directory, metadata={"wire_name": "PI_CODING_AGENT_DIR", "native": True}, ) + def __post_init__(self) -> None: object.__setattr__(self, "native_config", self.absolute(self.native_config)) object.__setattr__(self, "agent_directory", self.absolute(self.agent_directory)) @@ -197,7 +248,9 @@ def absolute(self, directory: Path) -> Path: def inherit(cls, environment: Mapping[str, str]): values = cls.inherited(environment) native_name, agent_name = cls.variable("native_config"), cls.variable("agent_directory") - values[native_name] = values.get(native_name) or values.get(agent_name) or str(cls.default_agent_directory) + values[native_name] = ( + values.get(native_name) or values.get(agent_name) or str(cls.default_agent_directory) + ) return FieldCodec.decode(cls, values) def for_agent(self, directory: Path) -> RestartEnvironment: @@ -211,14 +264,18 @@ def for_interpreter(self, interpreter: Path) -> RestartEnvironment: """Put this process's runtime first, exactly once; preserve other search paths.""" directory = str(interpreter.parent) inherited = os.defpath if self.path is None else self.path - path = os.pathsep.join((directory, *(entry for entry in inherited.split(os.pathsep) - if entry != directory))) + path = os.pathsep.join( + (directory, *(entry for entry in inherited.split(os.pathsep) if entry != directory)) + ) return replace(self, path=path) def encode_native(self) -> dict[str, str]: encoded = self.encode() - native = (FieldCodec.wire_key(declared) for declared in fields(self) - if declared.metadata.get("native")) + native = ( + FieldCodec.wire_key(declared) + for declared in fields(self) + if declared.metadata.get("native") + ) return {key: encoded[key] for key in native} def auth_revision(self) -> tuple[int, int]: @@ -300,8 +357,13 @@ def current(cls, environment: Mapping[str, str] = os.environ) -> ThreadLaunch: return FieldCodec.decode(cls, values) @classmethod - def called_by(cls, name: str, canonicalize: Callable[[str], str], *, - environment: Mapping[str, str] = os.environ) -> bool: + def called_by( + cls, + name: str, + canonicalize: Callable[[str], str], + *, + environment: Mapping[str, str] = os.environ, + ) -> bool: try: caller = cls.current(environment).name except CallingThreadUnavailable: diff --git a/src/agent_comms/owner_cutover.py b/src/agent_comms/owner_cutover.py index d2e699d575..d7a03299cf 100644 --- a/src/agent_comms/owner_cutover.py +++ b/src/agent_comms/owner_cutover.py @@ -116,13 +116,9 @@ def complete(self, stopped: StoppedOwnerBatch) -> tuple[OwnerRestartResult, ...] self.after_stopped(stopped.lifecycle) return stopped.launch(self.launch_runtime(stopped)) - @staticmethod - def current_pointer() -> Path: - return Path.home() / ".local/share/agent-comms/current" - def launch_runtime(self, stopped: StoppedOwnerBatch) -> LaunchRuntime: """Only installation selects the runtime after publishing its pointer.""" - selected = LaunchRuntime(self.current_pointer().resolve(strict=True) / "bin/python") + selected = LaunchRuntime.installed() selected.require_available() return selected diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 399d6bc896..033bc7ce56 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -7,14 +7,14 @@ from __future__ import annotations +import asyncio import calendar import json import re -import subprocess import sqlite3 +import subprocess import sys import time -import asyncio from abc import abstractmethod from collections.abc import Iterator, Sequence from contextlib import contextmanager @@ -33,23 +33,29 @@ from .bus_publication import AgentOrigin from .external_transcript import ExternalTranscript from .messages import InfoKind, Message +from .channel_targets import CommandTarget from .command import Command -from .launch_runtime import StackBuild from .declared_family import DeclaredFamily from .errors import ToolRefusedError from .field_codec import ( - FieldCodec, GitCommitText, MalformedRecord, NullMember, RecordMember, SubclassUnion, WireValue, + FieldCodec, + GitCommitText, + MalformedRecord, + NullMember, + RecordMember, + SubclassUnion, + WireValue, ) -from .channel_targets import CommandTarget from .goals import CheckedCommit, CountCheckResult, OrderCheck, ProblemGoalOrder +from .launch_runtime import BuildIdentity, UnrecordedBuildIdentity from .order_story import ProblemStory, StoryEvent, StoryLine, StorySource +from .plan_phases import OrderPhase, UnplannedPhase from .recorded_time import EventTime, RecordedEventTime, UnrecordedEventTime +from .standing_rules import UndatedStandingRule +from .storage_rules import StorageRule 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 -from .storage_rules import StorageRule -from .plan_phases import OrderPhase, UnplannedPhase from .work_branches import WorkBranchError, WorkBranches if TYPE_CHECKING: @@ -85,7 +91,8 @@ class OrderDelivery(DeclaredFamily, StoryEvent, affix="Delivery"): at: EventTime = field(default_factory=UnrecordedEventTime) evidence: DeliveryEvidence = field(default_factory=NoDeliveryEvidence, metadata={"wire_omit_default": True}) - proof_build: StackBuild | None = field(default=None, metadata={"wire_omit_default": True}) + proof_build: BuildIdentity = field(default_factory=UnrecordedBuildIdentity, + metadata={"wire_omit_default": True}) @property def work(self) -> str: @@ -112,16 +119,7 @@ async def verified(self, todo: Todo, registry: Registration) -> OrderDelivery: await self.require_source(each) each.result.require(await self.checked(each)) return replace(self, at=await self.delivery_time(check), - proof_build=self.proof_build or await asyncio.to_thread(StackBuild.installed)) - - def require_current_build(self) -> None: - installed = StackBuild.installed() - if self.proof_build is None: - raise ValueError( - f"Proof build is unrecorded; installed Core {installed.core}, " - f"Toad {installed.toad}; run and record a new proof." - ) - self.proof_build.require_same(installed) + proof_build=await asyncio.to_thread(self.proof_build.recorded)) @abstractmethod async def require_source(self, check): ... @@ -1699,7 +1697,7 @@ def corrected(self, correction: CorrectCheckTodo) -> ProblemOrder: def installed(self, works, artifact): self.run.require_closing(works) - self.delivery.require_current_build() + self.delivery.proof_build.require_installed() return self.copy_as(InstalledProblemOrder, works=works, artifact=artifact) def require_checked(self, problem: str) -> None: diff --git a/tests/conftest.py b/tests/conftest.py index 4e45d3708a..0e456bffaf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -300,7 +300,11 @@ def commander_script(commander, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) from agent_comms.commander.config import CommanderConfig comms, _ = commander + installed = LaunchRuntime.current_pointer().resolve(strict=True) monkeypatch.setenv("HOME", str(tmp_path)) + pointer = LaunchRuntime.current_pointer() + pointer.parent.mkdir(parents=True) + pointer.symlink_to(installed) repo = tmp_path / "repo" repo.mkdir() for command in (("init", "-q"), ("remote", "add", "origin", "https://github.com/OpenHCSDev/agent-comms.git")): diff --git a/tests/runtime_publication_journey.py b/tests/runtime_publication_journey.py index 972607958b..6d2f1da322 100644 --- a/tests/runtime_publication_journey.py +++ b/tests/runtime_publication_journey.py @@ -17,7 +17,7 @@ source = Path(__file__).resolve().parents[1] from agent_comms.active_route import ActiveRoute, publish_active_route from agent_comms.comms import Comms -from agent_comms.owner_cutover import StoppedOwnerInstallation +from agent_comms.launch_runtime import LaunchRuntime from agent_comms.maintenance_barrier import ReadyPhase from agent_comms.runtime import socket_path from tools.publish_runtime import RuntimePublication @@ -41,7 +41,7 @@ def main(): for key in tuple(os.environ): if key.startswith(('AGENT_COMMS_', 'PI_')) and key not in environment: del os.environ[key] - current = StoppedOwnerInstallation.current_pointer() + current = LaunchRuntime.current_pointer() current.parent.mkdir(mode=0o700, parents=True) current.symlink_to(old) starter = ''' diff --git a/tests/test_proof_build.py b/tests/test_proof_build.py new file mode 100644 index 0000000000..5ad7b1af3b --- /dev/null +++ b/tests/test_proof_build.py @@ -0,0 +1,109 @@ +"""Build-bound close uses real native tools and a retained-history bus copy.""" + +import subprocess +from dataclasses import replace +from pathlib import Path + +import pytest + +from agent_comms.change_report import FileLine +from agent_comms.check_root import CheckRoot +from agent_comms.comms import Comms +from agent_comms.field_codec import FieldCodec +from agent_comms.goal_actions import CreateProblemGoalAction, SetGoalAction +from agent_comms.goals import CountCheckResult, OrderCheck, RequestGoalOrder +from agent_comms.launch_runtime import RecordedBuildIdentity +from agent_comms.progress_claims import WorkingClaim +from agent_comms.recorded_time import RecordedEventTime +from agent_comms.tool_caller import ThreadCaller +from agent_comms.tools import CommsOrderDoneTool, CommsProblemCloseTool, CommsTodoCreateTool +from agent_comms.todos import ( + ApplicationOrderRun, + FileDeliveryEvidence, + OperationDelivery, + PendingProblemOrder, + Todo, +) + + +def test_installed_proof_closes_and_older_build_is_refused_on_copied_bus(tmp_path): + source = Comms(tmp_path / "source") + source.messaging.initialize_private_initial_protocol() + for number, name in enumerate(("lead", "worker"), 1): + source.registry.declare( + source.registry.new_thread( + name, frozenset({"comms"}), str(tmp_path), created_at=float(number) + ) + ) + installed = RecordedBuildIdentity.installed() + older_core = subprocess.check_output( + ("git", "rev-parse", f"{installed.core}^"), + cwd=Path(__file__).resolve().parents[1], + text=True, + ).strip() + older = replace(installed, core=older_core) + caller = ThreadCaller("lead") + works = "Open the installed application and confirm the copied goal keeps its source location." + artifact = tmp_path / "proof.txt" + artifact.write_text(works) + with CheckRoot(source, ("lead", "worker"), tmp_path / "copied").open() as copied: + copied.goals.update_goal( + "lead", + SetGoalAction( + text="Record the copied application proof", + goal_id="request", + order=RequestGoalOrder(), + ), + ) + for problem, build in (("P1", installed), ("P2", older)): + order = PendingProblemOrder( + owner=FileLine(__file__, 1), + check=OrderCheck(str(tmp_path), "printf 0", CountCheckResult(0)), + edits=("Record the copied application proof on its installed build.",), + banned=("Do not change the source bus.",), + report="Report the actual close result.", + run=ApplicationOrderRun(works), + ) + caller.invoke( + CommsTodoCreateTool( + owner="worker", + order=CreateProblemGoalAction( + problem=problem, + order=order, + request="request", + deadline=RecordedEventTime(4102444800), + commander="lead", + repo="OpenHCSDev/agent-comms", + text="Bind proof to its build", + ), + ), + copied, + ) + caller.invoke( + CommsOrderDoneTool( + problem=problem, + order_id=order.id, + delivery=OperationDelivery( + evidence=FileDeliveryEvidence(str(artifact)), proof_build=build + ), + claims=(WorkingClaim(str(artifact)),), + ), + copied, + ) + delivered = copied.goals.todos.get(problem) + assert delivered.order.delivery.proof_build == build + assert FieldCodec.decode(Todo, FieldCodec.encode(delivered)) == delivered + close = CommsProblemCloseTool(problem=problem, works=works, artifact=str(artifact)) + if build == installed: + caller.invoke(close, copied) + saved = copied.goals.todos.get(problem) + assert saved.order.accepted and not saved.state.open + else: + with pytest.raises(ValueError) as refusal: + caller.invoke(close, copied) + assert all( + revision in str(refusal.value) + for revision in (installed.core, installed.toad, older.core, older.toad) + ) + assert copied.goals.todos.get(problem) == delivered + assert not source.goals.todos.for_commander(source.registry.require("lead").incarnation) diff --git a/tests/test_runtime_publication.py b/tests/test_runtime_publication.py index 0d95d50910..547452bd45 100644 --- a/tests/test_runtime_publication.py +++ b/tests/test_runtime_publication.py @@ -8,7 +8,7 @@ import pytest from agent_comms.active_route import LocalRoute -from agent_comms.owner_cutover import StoppedOwnerInstallation +from agent_comms.launch_runtime import LaunchRuntime from tools.publish_runtime import RuntimePublication @@ -23,7 +23,7 @@ def test_cli_failure_leaves_links_untouched(tmp_path, monkeypatch, command): link = tmp_path / "command" link.symlink_to("previous-command") publication = RuntimePublication(runtime, {link: binary}) - current = StoppedOwnerInstallation.current_pointer() + current = LaunchRuntime.current_pointer() current.parent.mkdir(parents=True) current.symlink_to("previous-runtime") before = (link.lstat(), current.lstat()) @@ -39,7 +39,7 @@ def test_cli_failure_leaves_links_untouched(tmp_path, monkeypatch, command): def test_pending_carries_refuse_before_cli_or_maintenance(tmp_path, monkeypatch): monkeypatch.setenv('HOME', str(tmp_path)) - current = StoppedOwnerInstallation.current_pointer() + current = LaunchRuntime.current_pointer() current.parent.mkdir(parents=True) current.symlink_to(Path(sys.executable).parent.resolve().parent) root = tmp_path / "private-bus" @@ -57,7 +57,7 @@ def test_pointer_publication_does_not_kill_unselected_runtime_processes( for runtime in runtimes.values(): (runtime / "bin").mkdir(parents=True) (runtime / "bin/python").symlink_to(sys.executable) - current = StoppedOwnerInstallation.current_pointer() + current = LaunchRuntime.current_pointer() current.parent.mkdir(parents=True) current.symlink_to(runtimes["previous-runtime"]) retired, kept = (subprocess.Popen([runtime / "bin/python", "-c", "import time; time.sleep(60)"]) diff --git a/tools/publish_runtime.py b/tools/publish_runtime.py index d5d15cf99f..ad3d2b1e78 100644 --- a/tools/publish_runtime.py +++ b/tools/publish_runtime.py @@ -27,7 +27,7 @@ from agent_comms.harness_install import Harness, replace_link from agent_comms.launch_runtime import LaunchRuntime from agent_comms.maintenance_barrier import MaintenanceOperator, MaintenanceState -from agent_comms.owner_cutover import StoppedOwnerInstallation, MaintainedOwnerCutover +from agent_comms.owner_cutover import MaintainedOwnerCutover from agent_comms.owner_lifecycle import OwnerRestartSelection, OwnerRestartResult from agent_comms.owner_restart import OwnerRestartHandoff, OwnerRestartRequest, StoppedOwnerBatch from agent_comms.store_files import _atomic_write_text, _store_lock @@ -122,7 +122,7 @@ def publish(self, root: Path, *, checkout: Path, timeout: float = 1200, input is resubmitted. Unrelated maintenance and changed incarnations are refused rather than guessed. """ - installed = StoppedOwnerInstallation.current_pointer().resolve(strict=True) + installed = LaunchRuntime.current_pointer().resolve(strict=True) if Path(sys.executable).parent.resolve().parent != installed: raise RuntimeError('Drain and stop must run with the installed source interpreter') carries = PreparedCarries.at(checkout, root, self.backup(root), self.published()) @@ -137,7 +137,7 @@ def publish(self, root: Path, *, checkout: Path, timeout: float = 1200, os.fsencode(self.runtime.resolve())).hexdigest()) intent_path = root / 'runtime-publication.json' with _store_lock(root / 'runtime-publication'): - previous = StoppedOwnerInstallation.current_pointer().resolve(strict=True) + previous = LaunchRuntime.current_pointer().resolve(strict=True) intent = PublicationIntent.read(intent_path) intent.require_target(self.runtime, previous) current = barrier.read() @@ -175,7 +175,7 @@ def backup(self, root: Path) -> StoreBackup: return StoreBackup(root, active_route_path(), path) def published(self) -> bool: - return StoppedOwnerInstallation.current_pointer().resolve(strict=True) == self.runtime.resolve() + return LaunchRuntime.current_pointer().resolve(strict=True) == self.runtime.resolve() def release_if_safe(self, carries: PreparedCarries, barrier, hold: MaintenanceState) -> None: if barrier.read() == hold and carries.cleanup(self.published()): @@ -197,7 +197,7 @@ def selected_route(self, root: Path): def publish_links(self) -> None: """Move declarations and the pointer, without killing unrelated processes.""" - current = StoppedOwnerInstallation.current_pointer() + current = LaunchRuntime.current_pointer() if self.published(): raise ValueError(f'Publication would retire the runtime it publishes: {self.runtime}') for link, target in self.links.items(): From e3a325e331816ac6b45581f885acaa77133f7c90 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:59:35 -0400 Subject: [PATCH 54/66] Retain proof identity through checked commit delivery --- src/agent_comms/todos.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 033bc7ce56..1840f9b5a6 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -210,7 +210,8 @@ async def verified(self, todo: Todo, registry: Registration) -> OrderDelivery: verified = await super().verified(todo, registry) merge = await self.merge_state(todo, registry) return CheckedCommitDelivery(commit=self.commit, delivered_commit=self.delivered, - at=verified.at, evidence=self.evidence, merge=merge) + at=verified.at, evidence=self.evidence, merge=merge, + proof_build=verified.proof_build) async def merge_state(self, todo: Todo, registry: Registration) -> DeliveryMerge: """Observe integration, not merely publication, at the time of completion. From 10a51638ad4aa94a64cd64b0295ffe27b0d6a915 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 20 Oct 2026 00:02:48 -0400 Subject: [PATCH 55/66] Freeze installation selection and inspect retained proof identities without capture --- src/agent_comms/commander_parity/lifecycle.py | 12 ++++++------ src/agent_comms/launch_runtime.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/agent_comms/commander_parity/lifecycle.py b/src/agent_comms/commander_parity/lifecycle.py index db6cd8f395..2a3419bf22 100644 --- a/src/agent_comms/commander_parity/lifecycle.py +++ b/src/agent_comms/commander_parity/lifecycle.py @@ -379,20 +379,20 @@ def checks( delivered, closed, stale, after = rows assert isinstance(delivered.order, DeliveredProblemOrder) assert isinstance(stale.order, DeliveredProblemOrder) - current_build = delivered.order.delivery.proof_build.recorded() - older_build = stale.order.delivery.proof_build.recorded() + current_build = delivered.order.delivery.proof_build + older_build = stale.order.delivery.proof_build + assert isinstance(current_build, RecordedBuildIdentity) + assert isinstance(older_build, RecordedBuildIdentity) return ( ("installed proof closes", closed.order.accepted and not closed.state.open), ( "build recorded durably", - bool(current_build and older_build) - and FieldCodec.decode(Todo, FieldCodec.encode(stale)) == stale, + FieldCodec.decode(Todo, FieldCodec.encode(stale)) == stale, ), ("older proof refused without mutation", stale == after and after.state.open), ( "both revisions named", - bool(current_build and older_build) - and all( + all( revision in output for revision in ( current_build.core, diff --git a/src/agent_comms/launch_runtime.py b/src/agent_comms/launch_runtime.py index 4f421ffe04..58927cfcfa 100644 --- a/src/agent_comms/launch_runtime.py +++ b/src/agent_comms/launch_runtime.py @@ -45,7 +45,7 @@ def current_pointer() -> Path: @classmethod def installed(cls) -> LaunchRuntime: - return cls(cls.current_pointer() / "bin/python") + return cls(cls.current_pointer().resolve(strict=True) / "bin/python") @property def prefix(self) -> Path: From 70aea2ad4ff1c213073cd49dd5ebfe54fb166767 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 20 Oct 2026 00:05:27 -0400 Subject: [PATCH 56/66] Observe the installed build before entering the Todo write transaction --- src/agent_comms/todos.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 1840f9b5a6..230462a057 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -1401,6 +1401,9 @@ def require_transfer(self, todo: Todo) -> None: def require_close(self): raise ValueError("Closing requires recorded installed application evidence.") + def require_installation(self) -> None: + raise ValueError("Check the delivered order before recording installation.") + def require_withdrawal(self) -> None: raise ValueError("Only an undelivered order can be withdrawn.") @@ -1696,9 +1699,11 @@ def closing_artifact(self) -> str: def corrected(self, correction: CorrectCheckTodo) -> ProblemOrder: raise ValueError("A delivered order keeps the check its delivery passed.") + def require_installation(self) -> None: + self.delivery.proof_build.require_installed() + def installed(self, works, artifact): self.run.require_closing(works) - self.delivery.proof_build.require_installed() return self.copy_as(InstalledProblemOrder, works=works, artifact=artifact) def require_checked(self, problem: str) -> None: @@ -2521,6 +2526,12 @@ class InstallTodo(CloseOrderTodo): works: str artifact: str + def prepare(self, goals, current): + current.require_revision(self.expected_revision) + current.require_commander(self.actor) + current.order.require_installation() + return self + def closed(self, current: Todo) -> ProblemOrder: if not Path(self.artifact).is_file(): raise ValueError("Retain the installed application artifact before closing.") From a55d56f59462b809cd54d7d0f4b9db05171720aa Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 20 Oct 2026 00:09:10 -0400 Subject: [PATCH 57/66] Type the installed-proof preparation boundary --- src/agent_comms/todos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 230462a057..6bb65350bd 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -2526,7 +2526,7 @@ class InstallTodo(CloseOrderTodo): works: str artifact: str - def prepare(self, goals, current): + def prepare(self, goals: Goals, current: Todo) -> InstallTodo: current.require_revision(self.expected_revision) current.require_commander(self.actor) current.order.require_installation() From 25709d58cb96e38f2e71405f87a1a759cc4fe617 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 20 Oct 2026 00:17:04 -0400 Subject: [PATCH 58/66] Use the canonical event-time clock after rebasing onto main --- src/agent_comms/commander_parity/lifecycle.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/agent_comms/commander_parity/lifecycle.py b/src/agent_comms/commander_parity/lifecycle.py index 2a3419bf22..634f71cfef 100644 --- a/src/agent_comms/commander_parity/lifecycle.py +++ b/src/agent_comms/commander_parity/lifecycle.py @@ -22,7 +22,7 @@ from ..change_report import FileLine from ..check_root import CheckRoot from ..commander.config import CommanderConfig -from ..commander.order import Commander, OrderFile, hhmm +from ..commander.order import Commander, OrderFile from ..commander.request import RequestAddVerb from ..field_codec import FieldCodec from ..goals import CountCheckResult, OrderCheck, ProblemGoalOrder @@ -486,16 +486,16 @@ def checks( ( "unit messages hide clocks", all( - hhmm(clock) not in message.body + clock.clock() not in message.body for clock in (before.deadline, after.deadline) for message in messages ), ), ( "unit goal hides deadline", - "Deadline:" not in instructions and hhmm(after.deadline) not in instructions, + "Deadline:" not in instructions and after.deadline.clock() not in instructions, ), - ("commander still sees deadline", f"due {hhmm(after.deadline)}" in output), + ("commander still sees deadline", f"due {after.deadline.clock()}" in output), ) From 8c087098b5e24ef387b47477461979b1e7d530b8 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 20 Oct 2026 00:27:20 -0400 Subject: [PATCH 59/66] Remove the deleted commander CLI field from lifecycle construction --- src/agent_comms/commander_parity/lifecycle.py | 1 - src/agent_comms/todos.py | 15 ++++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/agent_comms/commander_parity/lifecycle.py b/src/agent_comms/commander_parity/lifecycle.py index 634f71cfef..a6f10beeeb 100644 --- a/src/agent_comms/commander_parity/lifecycle.py +++ b/src/agent_comms/commander_parity/lifecycle.py @@ -130,7 +130,6 @@ def open(cls, *, extra_threads: tuple[str, ...] = ()) -> Iterator[LifecycleRun]: commander = MeasuredCommander( commander=config.commander, root=root, - cli=config.cli, state=root / "commander-state", board=root / "board.md", narrative=root / "narrative.md", diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 6bb65350bd..bf79def417 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -26,17 +26,21 @@ from uuid import uuid4 from . import skills +from .bus_publication import AgentOrigin from .change_report import FileLine +from .channel_targets import CommandTarget from .child_process import ( - BoundedRun, CheckPrinted, ChildResult, ExitedOutcome, NoIntegerPrinted, OrderCheckFailedError, + BoundedRun, + CheckPrinted, + ChildResult, + ExitedOutcome, + NoIntegerPrinted, + OrderCheckFailedError, ) -from .bus_publication import AgentOrigin -from .external_transcript import ExternalTranscript -from .messages import InfoKind, Message -from .channel_targets import CommandTarget from .command import Command from .declared_family import DeclaredFamily from .errors import ToolRefusedError +from .external_transcript import ExternalTranscript from .field_codec import ( FieldCodec, GitCommitText, @@ -48,6 +52,7 @@ ) from .goals import CheckedCommit, CountCheckResult, OrderCheck, ProblemGoalOrder from .launch_runtime import BuildIdentity, UnrecordedBuildIdentity +from .messages import InfoKind, Message from .order_story import ProblemStory, StoryEvent, StoryLine, StorySource from .plan_phases import OrderPhase, UnplannedPhase from .recorded_time import EventTime, RecordedEventTime, UnrecordedEventTime From 3db34a56a2199f059e9f759498947af61f0e0d49 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Mon, 19 Oct 2026 23:55:22 -0400 Subject: [PATCH 60/66] P302: bound load comparison and observe actual CLI returns without frame locals --- src/agent_comms/commander/capacity.py | 9 +- src/agent_comms/commander_parity/load.py | 70 ++++------ .../commander_parity/observations.py | 123 ++++++++++++++++++ src/agent_comms/turn_digests.py | 21 ++- tests/test_commander_parity_load.py | 3 +- 5 files changed, 173 insertions(+), 53 deletions(-) create mode 100644 src/agent_comms/commander_parity/observations.py diff --git a/src/agent_comms/commander/capacity.py b/src/agent_comms/commander/capacity.py index b3dda4f026..d3a7852f9c 100644 --- a/src/agent_comms/commander/capacity.py +++ b/src/agent_comms/commander/capacity.py @@ -22,14 +22,17 @@ class Load(ToolResult): def of(cls, comms: Comms, commander: str) -> Load: snapshot, todos, now = comms.registry.snapshot(), comms.goals.todos, time.time() owner = snapshot.require(commander) - owed = tuple(t for t in todos.held_for_commander(owner.incarnation) if t.pending) + held = todos.held_for_commander(owner.incarnation) + owed = tuple(t for t in held if t.pending) forks = tuple(sorted( thread.name for thread in snapshot.threads.values() if thread.parent == owner.name and snapshot.statuses[thread.name].receiving and todos.held_for_commander(thread.incarnation) )) - return cls(len(owed), sum(t.overdue(now) for t in owed), - OrderEditCountRead.count(comms, owner.name), forks) + unjudged = sum( + read.read() for read in OrderEditCountRead.for_orders(comms.root, snapshot, held) + ) + return cls(len(owed), sum(t.overdue(now) for t in owed), unjudged, forks) @projected(view="wire") def total(self) -> float: diff --git a/src/agent_comms/commander_parity/load.py b/src/agent_comms/commander_parity/load.py index 66ec536cef..f6a050dd14 100644 --- a/src/agent_comms/commander_parity/load.py +++ b/src/agent_comms/commander_parity/load.py @@ -5,39 +5,13 @@ import subprocess from dataclasses import dataclass from pathlib import Path +from typing import ClassVar from ..commander.config import CommanderConfig from ..field_codec import FieldCodec from ..private_path import FileRevision from . import ParityComparison, UnverifiedParity - -# Profile the real return, never replace the measurement, command, bus or reply. -# Comparing independent reads of a running fleet confuses new edits with drift. -OBSERVE_CAPACITY = """ -import contextlib, io, json, sys -import agent_comms.commander.capacity as capacity -from agent_comms.commander.cli import main -measured = [] -contexts = [] -def observe(frame, event, value): - if event == 'return' and frame.f_code is capacity.Load.of.__func__.__code__: - measured.append(value) - if event == 'call' and frame.f_code is capacity.main.__code__: - contexts.append(frame.f_locals['ctx']) -output = io.StringIO() -sys.setprofile(observe) -try: - with contextlib.redirect_stdout(output): - main() -finally: - sys.setprofile(None) -if len(measured) != 1 or len(contexts) != 1: - raise ValueError('Capacity must use exactly one native measurement and selected context') -config = contexts[0] -print(json.dumps(dict(current=output.getvalue(), native=str(measured[0]), - source=capacity.__file__, root=str(config.root), - commander=config.commander))) -""" +from .observations import CapacityObservation, MeasuredOutput def state_files(directory: Path) -> tuple[tuple[str, FileRevision], ...]: @@ -49,16 +23,9 @@ def state_files(directory: Path) -> tuple[tuple[str, FileRevision], ...]: @dataclass(frozen=True) -class MeasuredOutput: - current: str - native: str - source: str - root: str - commander: str - - -@dataclass(frozen=True) -class LoadComparison(ParityComparison): +class CommandComparison(ParityComparison): + arguments: ClassVar[tuple[str, ...]] + timeout: ClassVar[int] = 30 observation: MeasuredOutput state_before: tuple[tuple[str, FileRevision], ...] state_after: tuple[tuple[str, FileRevision], ...] @@ -80,16 +47,25 @@ def describe(self) -> str: @classmethod def read(cls) -> ParityComparison: - config = CommanderConfig.read(CommanderConfig.path()) + return cls.invoke(CommanderConfig.read(CommanderConfig.path()), cls.arguments) + + @classmethod + def invoke(cls, config: CommanderConfig, arguments: tuple[str, ...]) -> ParityComparison: + """Bind one native invocation and all its observations to the selected configuration.""" before = state_files(config.state) # -I uses installed imports even when this candidate runs with PYTHONPATH=src. # HOME and the environment stay unchanged; the actual CLI main runs. - current = subprocess.run( - (str(config.cli.parent / "python"), "-I", "-c", OBSERVE_CAPACITY, "capacity"), - capture_output=True, - text=True, - timeout=30, - ) + try: + current = subprocess.run( + (str(config.cli.parent / "python"), "-I", *arguments), + capture_output=True, + text=True, + timeout=cls.timeout, + ) + except subprocess.TimeoutExpired as error: + return UnverifiedParity( + f"{cls.__name__}: configured native owner timed out after {error.timeout} seconds" + ) if current.returncode: return UnverifiedParity(current.stderr) observed = FieldCodec.decode(MeasuredOutput, FieldCodec.parse(current.stdout)) @@ -103,3 +79,7 @@ def read(cls) -> ParityComparison: "The installed command used a different configuration or source" ) return cls(observed, before, state_files(config.state)) + + +class LoadComparison(CommandComparison): + arguments = CapacityObservation.arguments() diff --git a/src/agent_comms/commander_parity/observations.py b/src/agent_comms/commander_parity/observations.py new file mode 100644 index 0000000000..f92ef2ef88 --- /dev/null +++ b/src/agent_comms/commander_parity/observations.py @@ -0,0 +1,123 @@ +"""Observe real native command returns and their actual output.""" + +from __future__ import annotations + +import argparse +import io +import json +import sys +from abc import abstractmethod +from collections.abc import Callable, Iterator +from contextlib import contextmanager, redirect_stdout +from dataclasses import dataclass +from pathlib import Path +from types import CodeType, MethodType + +from agent_comms.commander import capacity +from agent_comms.commander.cli import main as commander_main +from agent_comms.commander.cli import tool_name +from agent_comms.commander.config import CommanderConfig +from agent_comms.commander.order import Commander +from agent_comms.declared_family import DeclaredFamily +from agent_comms.field_codec import FieldCodec + + +@dataclass(frozen=True) +class MeasuredOutput: + current: str + native: str + source: str + root: str + commander: str + + +class NativeObservation(DeclaredFamily, affix="Observation"): + @classmethod + def arguments(cls) -> tuple[str, ...]: + return (str(Path(__file__).resolve()), cls.declared_name) + + @abstractmethod + def read(self, root: Path) -> MeasuredOutput: ... + + +def method_code(method: object) -> CodeType: + if not isinstance(method, MethodType): + raise TypeError("An observation requires a bound Python method") + return method.__func__.__code__ + + +@contextmanager +def returned_values(callbacks: dict[CodeType, Callable[[object], None]]) -> Iterator[None]: + """Observe only declared native returns, not every Python call in the command.""" + monitor = sys.monitoring + tool = monitor.PROFILER_ID + + def returned(code: CodeType, offset: int, value: object) -> None: + callbacks[code](value) + + monitor.use_tool_id(tool, "commander-parity-observation") + try: + monitor.register_callback(tool, monitor.events.PY_RETURN, returned) + for code in callbacks: + monitor.set_local_events(tool, code, monitor.events.PY_RETURN) + yield + finally: + for code in callbacks: + monitor.set_local_events(tool, code, 0) + monitor.register_callback(tool, monitor.events.PY_RETURN, None) + monitor.free_tool_id(tool) + + +class CapacityObservation(NativeObservation): + def read(self, root: Path) -> MeasuredOutput: + measured: list[capacity.Load] = [] + contexts: list[Commander] = [] + + def measurement(value: object) -> None: + if not isinstance(value, capacity.Load): + raise TypeError("Load.of returned outside its declared contract") + measured.append(value) + + def context(value: object) -> None: + if not isinstance(value, Commander): + raise TypeError("The commander CLI returned an undeclared context") + contexts.append(value) + + output = io.StringIO() + # This process owns its command line. The actual CLI selects its root and command. + sys.argv[1:] = ["--root", str(root), tool_name(capacity.__name__)] + with ( + returned_values( + { + method_code(capacity.Load.of): measurement, + method_code(Commander.read): context, + } + ), + redirect_stdout(output), + ): + commander_main() + if len(measured) != 1 or len(contexts) != 1: + raise ValueError("Capacity must use one native measurement and selected context") + selected = contexts[0] + return MeasuredOutput( + output.getvalue(), + str(measured[0]), + str(capacity.__file__), + str(selected.comms.root), + selected.commander, + ) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("observation", type=NativeObservation.member_named) + parser.add_argument( + "--root", type=Path, default=CommanderConfig.read(CommanderConfig.path()).root + ) + arguments = parser.parse_args() + observed = arguments.observation().read(arguments.root) + print(json.dumps(FieldCodec.encode(observed))) + + +if __name__ == "__main__": + main() diff --git a/src/agent_comms/turn_digests.py b/src/agent_comms/turn_digests.py index 335f184d2a..414657faf8 100644 --- a/src/agent_comms/turn_digests.py +++ b/src/agent_comms/turn_digests.py @@ -18,6 +18,7 @@ if TYPE_CHECKING: from .comms import Comms + from .registry_document import RegistrySnapshot from .threads import Thread from .todos import Todo @@ -122,13 +123,25 @@ class OrderRead[Result](TranscriptRead[Result]): @classmethod def for_commander(cls, comms: Comms, commander: str) -> Iterator[Self]: - actor = comms.registry.require(commander).incarnation - for todo in comms.goals.todos.held_for_commander(actor): + snapshot = comms.registry.snapshot() + actor = snapshot.require(commander).incarnation + yield from cls.for_orders( + comms.root, snapshot, comms.goals.todos.held_for_commander(actor) + ) + + @classmethod + def for_orders( + cls, root: Path, snapshot: RegistrySnapshot, todos: Iterable[Todo] + ) -> Iterator[Self]: + """Read these orders against their captured roster, without reopening the registry.""" + for todo in todos: assignment = todo.held() - holder = comms.registry.require(assignment.owner) + holder = snapshot.require(assignment.owner) if holder.incarnation != assignment.owner_identity: raise ToolRefusedError(f"Order {todo.id} holder incarnation no longer exists") - yield cls(comms.root, holder, TurnsSince(todo.order.checked().at.recorded_at("An order")), todo) + yield cls( + root, holder, TurnsSince(todo.order.checked().at.recorded_at("An order")), todo + ) def edits_from_turns(self, turns: tuple[TurnDigest, ...]) -> tuple[EditCall, ...]: judged = {verdict.tool_call_id for verdict in self.todo.verdicts} diff --git a/tests/test_commander_parity_load.py b/tests/test_commander_parity_load.py index 6e26d798fa..98ed6adfa8 100644 --- a/tests/test_commander_parity_load.py +++ b/tests/test_commander_parity_load.py @@ -1,7 +1,8 @@ from dataclasses import replace from agent_comms.commander.capacity import Load -from agent_comms.commander_parity.load import LoadComparison, MeasuredOutput, state_files +from agent_comms.commander_parity.load import LoadComparison, state_files +from agent_comms.commander_parity.observations import MeasuredOutput def test_load_comparison_requires_every_reported_field_and_no_private_write(tmp_path): From 7152883d53f6c9f2fd3baffc968f14a1b307e2a6 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 20 Oct 2026 00:13:51 -0400 Subject: [PATCH 61/66] P302: let BoundedRun own native comparison collection and retirement --- src/agent_comms/commander_parity/load.py | 17 +++++------ tests/test_commander_parity_load.py | 39 +++++++++++++++++++++++- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/agent_comms/commander_parity/load.py b/src/agent_comms/commander_parity/load.py index f6a050dd14..e839acc0f7 100644 --- a/src/agent_comms/commander_parity/load.py +++ b/src/agent_comms/commander_parity/load.py @@ -2,11 +2,12 @@ from __future__ import annotations -import subprocess +import asyncio from dataclasses import dataclass from pathlib import Path from typing import ClassVar +from ..child_process import BoundedRun from ..commander.config import CommanderConfig from ..field_codec import FieldCodec from ..private_path import FileRevision @@ -55,19 +56,17 @@ def invoke(cls, config: CommanderConfig, arguments: tuple[str, ...]) -> ParityCo before = state_files(config.state) # -I uses installed imports even when this candidate runs with PYTHONPATH=src. # HOME and the environment stay unchanged; the actual CLI main runs. - try: - current = subprocess.run( + current = asyncio.run( + BoundedRun.run( (str(config.cli.parent / "python"), "-I", *arguments), - capture_output=True, - text=True, timeout=cls.timeout, ) - except subprocess.TimeoutExpired as error: + ) + if not current.outcome.successful: return UnverifiedParity( - f"{cls.__name__}: configured native owner timed out after {error.timeout} seconds" + f"{cls.__name__}: configured native owner {current.outcome.summary}; " + f"bound={cls.timeout}s; {current.stderr.decode(errors='replace')}" ) - if current.returncode: - return UnverifiedParity(current.stderr) observed = FieldCodec.decode(MeasuredOutput, FieldCodec.parse(current.stdout)) source = Path(observed.source) if ( diff --git a/tests/test_commander_parity_load.py b/tests/test_commander_parity_load.py index 98ed6adfa8..786797c6cf 100644 --- a/tests/test_commander_parity_load.py +++ b/tests/test_commander_parity_load.py @@ -1,7 +1,12 @@ +import sys from dataclasses import replace +from pathlib import Path + +import pytest from agent_comms.commander.capacity import Load -from agent_comms.commander_parity.load import LoadComparison, state_files +from agent_comms.commander.config import CommanderConfig +from agent_comms.commander_parity.load import CommandComparison, LoadComparison, state_files from agent_comms.commander_parity.observations import MeasuredOutput @@ -23,3 +28,35 @@ def test_load_comparison_requires_every_reported_field_and_no_private_write(tmp_ path.write_text("after") after = state_files(tmp_path) assert not LoadComparison(observed, before, after).passed + + +@pytest.mark.parametrize( + ("program", "expected"), + ( + ("raise SystemExit(7)", "exited with code 7"), + ("import time; time.sleep(10)", "timed out"), + ), +) +def test_failed_real_children_remain_named_unverified(tmp_path, program, expected): + """Negative process checks produce no fabricated native measurement or parity pass.""" + + class BriefComparison(CommandComparison): + timeout = 3 + + config = CommanderConfig( + commander="reader", + root=tmp_path / "bus", + cli=Path(sys.executable), + state=tmp_path / "state", + board=tmp_path / "board.md", + narrative=tmp_path / "narrative.md", + repos=(), + worktrees=tmp_path / "projects", + scratch=tmp_path / "scratch", + disk=tmp_path, + ) + comparison = BriefComparison.invoke(config, ("-c", program)) + assert not comparison.passed + assert expected in comparison.describe() + assert "bound=3s" in comparison.describe() + assert LoadComparison.timeout == 30 From bab521f2b20b296d4d3140228fe73937313b0d16 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 20 Oct 2026 00:30:50 -0400 Subject: [PATCH 62/66] P302: derive comparison interpreter from the selected runtime --- src/agent_comms/commander_parity/load.py | 5 ++++- tests/test_commander_parity_load.py | 3 --- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/agent_comms/commander_parity/load.py b/src/agent_comms/commander_parity/load.py index e839acc0f7..7f131371ae 100644 --- a/src/agent_comms/commander_parity/load.py +++ b/src/agent_comms/commander_parity/load.py @@ -10,6 +10,7 @@ from ..child_process import BoundedRun from ..commander.config import CommanderConfig from ..field_codec import FieldCodec +from ..launch_runtime import LaunchRuntime from ..private_path import FileRevision from . import ParityComparison, UnverifiedParity from .observations import CapacityObservation, MeasuredOutput @@ -56,10 +57,12 @@ def invoke(cls, config: CommanderConfig, arguments: tuple[str, ...]) -> ParityCo before = state_files(config.state) # -I uses installed imports even when this candidate runs with PYTHONPATH=src. # HOME and the environment stay unchanged; the actual CLI main runs. + runtime = LaunchRuntime.running() current = asyncio.run( BoundedRun.run( - (str(config.cli.parent / "python"), "-I", *arguments), + (str(runtime.interpreter), "-I", *arguments), timeout=cls.timeout, + runtime=runtime, ) ) if not current.outcome.successful: diff --git a/tests/test_commander_parity_load.py b/tests/test_commander_parity_load.py index 786797c6cf..2e19bcaaf1 100644 --- a/tests/test_commander_parity_load.py +++ b/tests/test_commander_parity_load.py @@ -1,6 +1,4 @@ -import sys from dataclasses import replace -from pathlib import Path import pytest @@ -46,7 +44,6 @@ class BriefComparison(CommandComparison): config = CommanderConfig( commander="reader", root=tmp_path / "bus", - cli=Path(sys.executable), state=tmp_path / "state", board=tmp_path / "board.md", narrative=tmp_path / "narrative.md", From a0c7a616d39429e1b10961bfa77bf234fa7b5b9c Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 20 Oct 2026 00:58:18 -0400 Subject: [PATCH 63/66] P302: observe the CLI-selected root context return --- .../commander_parity/observations.py | 2 +- tests/test_commander_parity_load.py | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/agent_comms/commander_parity/observations.py b/src/agent_comms/commander_parity/observations.py index f92ef2ef88..11067be95c 100644 --- a/src/agent_comms/commander_parity/observations.py +++ b/src/agent_comms/commander_parity/observations.py @@ -90,7 +90,7 @@ def context(value: object) -> None: returned_values( { method_code(capacity.Load.of): measurement, - method_code(Commander.read): context, + method_code(Commander.read_root): context, } ), redirect_stdout(output), diff --git a/tests/test_commander_parity_load.py b/tests/test_commander_parity_load.py index 2e19bcaaf1..129c5fd33c 100644 --- a/tests/test_commander_parity_load.py +++ b/tests/test_commander_parity_load.py @@ -1,3 +1,5 @@ +import shutil +import sys from dataclasses import replace import pytest @@ -5,7 +7,7 @@ from agent_comms.commander.capacity import Load from agent_comms.commander.config import CommanderConfig from agent_comms.commander_parity.load import CommandComparison, LoadComparison, state_files -from agent_comms.commander_parity.observations import MeasuredOutput +from agent_comms.commander_parity.observations import CapacityObservation, MeasuredOutput def test_load_comparison_requires_every_reported_field_and_no_private_write(tmp_path): @@ -28,6 +30,22 @@ def test_load_comparison_requires_every_reported_field_and_no_private_write(tmp_ assert not LoadComparison(observed, before, after).passed +def test_capacity_observes_actual_cli_selected_root(commander_script, tmp_path, monkeypatch): + original, _ = commander_script + selected = tmp_path / "selected" + shutil.copytree(original.root, selected) + before = state_files(original.root) + monkeypatch.setattr(sys, "argv", ["capacity-observation"]) + + observed = CapacityObservation().read(selected) + + assert observed.root == str(selected.resolve()) + assert observed.commander == original.commander + assert observed.current.strip() == observed.native + assert state_files(original.root) == before + assert sys.monitoring.get_tool(sys.monitoring.PROFILER_ID) is None + + @pytest.mark.parametrize( ("program", "expected"), ( From 14772e307e001a1f5af5a7709f1abed60a71a33f Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 20 Oct 2026 02:04:57 -0400 Subject: [PATCH 64/66] Keep completion times on finished items; delete EventTime.stamp The #2025 resolution stamped every completed event with the item start time, so a tool end recorded at 2.0 read as 1.0. Completions already carry their own decoded times; stamp had no other caller. Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/recorded_time.py | 18 +----------------- src/agent_comms/runtime_updates.py | 5 ++--- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/src/agent_comms/recorded_time.py b/src/agent_comms/recorded_time.py index 91823e9021..1708046988 100644 --- a/src/agent_comms/recorded_time.py +++ b/src/agent_comms/recorded_time.py @@ -4,19 +4,12 @@ import time from abc import abstractmethod -from dataclasses import dataclass, replace +from dataclasses import dataclass from math import isfinite -from typing import TYPE_CHECKING, TypeVar from .declared_family import DeclaredFamily from .field_codec import FieldCodec, FieldRepresentation -if TYPE_CHECKING: - from .transcript_events import TranscriptEvent - -Event = TypeVar("Event", bound="TranscriptEvent") - - class UnrecordedTimeError(ValueError): """An operation requires a source-recorded time, but none was supplied.""" @@ -93,18 +86,12 @@ def require_deadline(self) -> None: def require_delivery(self) -> None: self.require_recorded("A delivery") - @abstractmethod - def stamp(self, event: Event) -> Event: ... - @abstractmethod def continued_from(self, original_at: float): ... @dataclass(frozen=True) class UnrecordedEventTime(EventTime): - def stamp(self, event: Event) -> Event: - return event - def continued_from(self, original_at): return RecordedEventTime(original_at) @@ -139,8 +126,5 @@ def at_clock(self, clock: str) -> RecordedEventTime: def recorded_at(self, label: str) -> float: return self.at - def stamp(self, event: Event) -> Event: - return replace(event, timestamp=self.at) - def continued_from(self, original_at): return self diff --git a/src/agent_comms/runtime_updates.py b/src/agent_comms/runtime_updates.py index 37b267b165..6ec4da313c 100644 --- a/src/agent_comms/runtime_updates.py +++ b/src/agent_comms/runtime_updates.py @@ -50,9 +50,8 @@ def append(self, text: str, section: int) -> None: raise ValueError("Received a text delta for a non-text item") def finish(self, update: CompletedItem) -> tuple[ClosedDraft, FinishedItem]: - """Keep the observed source prefix's clock; completions are already dated.""" - events = tuple(self.started_at.stamp(event) for event in update.events) - return ClosedDraft(self.started_at), FinishedItem(update.turn_id, update.item_id, events) + """Close with the completed record's own already decoded event times.""" + return ClosedDraft(self.started_at), FinishedItem(update.turn_id, update.item_id, update.events) class NoPrefixDraft(DraftAccumulator): From d711e5a6bcee8817611f49b457374fe152e175b3 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 20 Oct 2026 02:21:40 -0400 Subject: [PATCH 65/66] Assert #2025's malformed-command case at the parse owner #2025 tested send_guard.simple_commands, a second shell-word parse that P294 (#2149) deleted from main. ShellInvocation.parse owns the parse and refuses malformed words, which the bash guard denies; the test now asserts that refusal. Co-Authored-By: Claude Opus 5.5 --- tests/test_transcript_event_time.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/test_transcript_event_time.py b/tests/test_transcript_event_time.py index fcf03b4db8..aa7356e456 100644 --- a/tests/test_transcript_event_time.py +++ b/tests/test_transcript_event_time.py @@ -186,10 +186,8 @@ def test_codex_rollout_projects_the_enclosing_record_clock(): assert update.events[0].timestamp == datetime.fromisoformat('2026-10-19T18:00:00Z').timestamp() -def test_malformed_shell_words_remain_visible_to_the_install_rule(): - from agent_comms.commander.send_guard import InstallCommand, simple_commands +def test_malformed_shell_words_refuse_instead_of_reaching_any_rule(tmp_path): + from agent_comms.storage_rules import ShellInvocation - command = 'uv pip install package "unterminated' - words, = simple_commands(command) - assert words == tuple(command.split()) - assert InstallCommand.runs(words) + with pytest.raises(ValueError): + tuple(ShellInvocation.parse('uv pip install package "unterminated', tmp_path)) From b562ad9de384aa07a7d777f363321d94efd0eb45 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 20 Oct 2026 02:32:57 -0400 Subject: [PATCH 66/66] Forward lifecycle process options to BoundedRun instead of restating them #2118's invoke_command declared cwd and env with None defaults, which restates BoundedRun.run's own options and their absent defaults (debt ratchet: NoneAbsence at lifecycle.py:205-206). BoundedRun.run owns them; invoke_command now forwards them. Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/commander_parity/lifecycle.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/agent_comms/commander_parity/lifecycle.py b/src/agent_comms/commander_parity/lifecycle.py index 8b6a67dc63..271de82cf3 100644 --- a/src/agent_comms/commander_parity/lifecycle.py +++ b/src/agent_comms/commander_parity/lifecycle.py @@ -199,13 +199,9 @@ def invoke(cls, program: str, *arguments: str) -> ChildResult: ) @staticmethod - def invoke_command( - command: tuple[str, ...], - *, - cwd: str | Path | None = None, - env: dict[str, str] | None = None, - ) -> ChildResult: - return asyncio.run(BoundedRun.run(command, timeout=120, cwd=cwd, env=env)) + def invoke_command(command: tuple[str, ...], **options) -> ChildResult: + """BoundedRun.run owns the process options and their absent defaults.""" + return asyncio.run(BoundedRun.run(command, timeout=120, **options)) @property def due(self) -> str: