diff --git a/src/agent_comms/tools.py b/src/agent_comms/tools.py index f07fe9a1eb..9400763226 100644 --- a/src/agent_comms/tools.py +++ b/src/agent_comms/tools.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import threading from abc import abstractmethod from collections.abc import Mapping from pathlib import Path @@ -1083,7 +1084,9 @@ def apply(self, comms: Comms, caller: ToolCaller) -> MessagePublished: body = VerdictAudience(self.audience).addressed(self.target, self.body) result = comms.messaging.send(self.sender, self.target, body, self.type, report=self.report) except ToolRefusedError as error: - SendRefusal.record(comms.root, self.sender, self.target, error) + # The caller hears the refusal now; the record waits for the wire lock on its own. + threading.Thread(target=SendRefusal.record, + args=(comms.root, self.sender, self.target, error)).start() raise caller.sent(comms, result.message) return MessagePublished.from_send(result) diff --git a/tools/upgrade_bus.py b/tools/upgrade_bus.py index 8bb34727e6..ab7e53b27c 100644 --- a/tools/upgrade_bus.py +++ b/tools/upgrade_bus.py @@ -230,15 +230,19 @@ def carry_all( def copy_bus(root: Path, copy: Path) -> None: - """Copy under the bus's wire lock so no writer tears the copy; sockets are skipped.""" - def sockets(directory: str, names: list[str]) -> set[str]: + """Copy under the bus's wire lock so no writer tears the copy; sockets and maintenance files are skipped.""" + # The maintenance files describe the live root; a copy is a new root and starts open. + held = set(MaintenanceBarrier(root / "registry.json").witness_paths) + + def skipped(directory: str, names: list[str]) -> set[str]: return {name for name in names - if stat.S_ISSOCK(os.lstat(os.path.join(directory, name)).st_mode)} + if Path(directory, name) in held + or stat.S_ISSOCK(os.lstat(os.path.join(directory, name)).st_mode)} from agent_comms.store_files import _store_lock with _store_lock(root / "wire"): - shutil.copytree(root, copy, symlinks=True, ignore=sockets) + shutil.copytree(root, copy, symlinks=True, ignore=skipped) def rehearsal_home(scratch: Path, root: Path, copy: Path) -> Path: