From c484e6e5bf6dddbe0d1d73be695cf867840d5b54 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 17:52:57 +0000 Subject: [PATCH] Install releases carry writer locks before services restart live() entered services_stopped inside borrowing(), so services restarted and the connector's ServingProbe ran while the installer held the carried stores' writer locks. With #3861's connector_access_carry pending, the probe's MCP round trip could not read the credentials store ("store lock busy, held by" the installer itself): install 6e8a33d5 failed, and its restore failed the same way. The writer locks now span carry, publish and input retirement only; services restart after they are released, on success and on restore alike. Retirement moves before the restart, still under the locks and still before the rollback point's end. No carry or credential behavior changes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016gZLdttfQjtizEYZGek9Gh --- src/agent_comms/installer/upgrade.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/agent_comms/installer/upgrade.py b/src/agent_comms/installer/upgrade.py index 36245925c0..c48f128f7c 100644 --- a/src/agent_comms/installer/upgrade.py +++ b/src/agent_comms/installer/upgrade.py @@ -1145,10 +1145,13 @@ def live(self, rehearsal: RehearsedFiles, report: InstallReport) -> CarriedRun: sum(path.stat().st_size for path in selected if path.exists()), ), self.root.parent) restore.require() - with self.staged.borrowing( - Carry.writer_declarations(pending), files, - ) as staged, staged.services_stopped(self.installed) as stopped: + with self.staged.services_stopped(self.installed) as stopped, ExitStack() as borrowed: # The install's start holds the root and stopped its owners (owners_stopped). + # The carried stores' writer locks span carry, publish and input retirement only: + # services restart after they are released, on success and on restore alike, + # because a restarted connector's serving probe reads those stores. + staged = borrowed.enter_context(self.staged.borrowing( + Carry.writer_declarations(pending), files)) maintenance = MaintenanceBarrier(RegistryStore.at(self.root).path) holder = ExitStack() try: @@ -1188,6 +1191,11 @@ def live(self, rehearsal: RehearsedFiles, report: InstallReport) -> CarriedRun: passed = migration.carry(pending, staged, rehearsal.reconcile_arguments()) passed.files.require_rehearsal(rehearsal.files) self.publish(backup, selected_route) + retirement = tuple(replace( + step, spill_dir=backup.spill_directory(self.spill_dir), + ) for step in self.pending) + passed = passed.after_acceptance(retirement, staged) + borrowed.close() with ClearMaintenancePhase().running(self.staged.commands.clock): report.owners.release() restarted_services = stopped.start_on(self.staged) @@ -1229,13 +1237,7 @@ def rehold(hold: MaintenanceState) -> None: runtime=self.installed.runtime) from error # Owner startup is reported per owner and never rolls the install back. report.writer = RuntimeWriter(self.root, self.staged) - try: - retirement = tuple(replace( - step, spill_dir=backup.spill_directory(self.spill_dir), - ) for step in self.pending) - passed = passed.after_acceptance(retirement, staged) - finally: - started = report.owners.published_on(self.staged, self.root) + started = report.owners.published_on(self.staged, self.root) with BackupRetirePhase().running(self.staged.commands.clock): retired_backups = backup.retain(self.spill_dir) return passed.extended(