diff --git a/src/agent_comms/maintenance_barrier.py b/src/agent_comms/maintenance_barrier.py index 3901015c0b..735965d87d 100644 --- a/src/agent_comms/maintenance_barrier.py +++ b/src/agent_comms/maintenance_barrier.py @@ -141,6 +141,10 @@ def hold_restore(cls, barrier: MaintenanceBarrier, operator: MaintenanceOperator drained() +# The holding operation hands its nonce to its own child processes through this variable. +HOLDER_NONCE = "AGENT_COMMS_MAINTENANCE_HOLDER" + + class MaintenanceStatus: """Absent witness is READY; stored phases derive from their record.""" @@ -154,6 +158,10 @@ def turn_pending(self) -> bool: def require_open(self) -> None: self.phase.new_sends.require_open() + def require_open_to(self, holder: str) -> None: + """Open, or held by ``holder``: the operation holding maintenance still runs its own reads.""" + self.require_open() + @dataclass(frozen=True) class MaintenanceOperator: @@ -216,6 +224,10 @@ def successor(self, phase: type[MaintenancePhase], nonce: str) -> MaintenanceSta generation=self.generation + 1, operator=self.operator, nonce=nonce, phase=phase) + def require_open_to(self, holder: str) -> None: + if holder != self.nonce: + self.require_open() + def require_draining(self) -> None: if self.phase is not DrainingPhase: raise ValueError('Only an acquired draining phase can be cleared') @@ -300,7 +312,7 @@ def current_unlocked(self) -> MaintenanceStatus: def assert_open_unlocked(self) -> MaintenanceStatus: hold = self.current_unlocked() - hold.require_open() + hold.require_open_to(os.environ.get(HOLDER_NONCE, "")) return hold def read(self) -> MaintenanceStatus: diff --git a/src/agent_comms/todos.py b/src/agent_comms/todos.py index 6a9d18f50a..281e53ab5d 100644 --- a/src/agent_comms/todos.py +++ b/src/agent_comms/todos.py @@ -12,6 +12,7 @@ import re import subprocess import sqlite3 +import sys import time import asyncio from abc import abstractmethod @@ -758,7 +759,8 @@ class OrderWords(WireValue): # for long ordinary words with no slash or filename suffix. CITED: ClassVar[re.Pattern] = re.compile( r'(?m)^ {4}.*$|`[^`\n]*`|"[^"\n]*"|“[^”\n]*”|' - r'(?:(? bool: + """Flags, never blocks: a wording slip costs a note, not a resend.""" + if found: + print(f"Flag: jargon {', '.join(found)} hides what happened; say it plainly.", file=sys.stderr) + return False + class StrikeReason(PlainText): """What a unit did, plainly, in enough words to stand on the public record. A unit's diff --git a/tools/upgrade_bus.py b/tools/upgrade_bus.py index ab7e53b27c..62369f88d1 100644 --- a/tools/upgrade_bus.py +++ b/tools/upgrade_bus.py @@ -53,7 +53,7 @@ from agent_comms.field_codec import FieldCodec from agent_comms.history_result import EverythingHistoryPageResult from agent_comms.host_services import HostService -from agent_comms.maintenance_barrier import MaintenanceBarrier, MaintenanceOperator +from agent_comms.maintenance_barrier import HOLDER_NONCE, MaintenanceBarrier, MaintenanceOperator from agent_comms.registry_store import RegistryStore from agent_comms.store_files import _store_lock from agent_comms.todos import PlannedInstallDeadline @@ -265,6 +265,9 @@ def rehearse(root: Path, pending: tuple[Step, ...], stack: Stack, revisions: Rev copy_bus(root, copy) stack = replace(stack, home=rehearsal_home(Path(scratch), root, copy)) 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 with transcripts_held(copy): before = bus_files(copy) stopping = before_stop(copy, pending, stack, revisions) @@ -374,6 +377,7 @@ def live(checkout: Path, root: Path, current: str, target: str, pending: tuple[S before_stop(root, pending, staged, revisions) maintenance = MaintenanceBarrier(RegistryStore.at(root).path) held = maintenance.begin(MaintenanceOperator("upgrade-bus")) + 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)