From 3db903c336e28a94aa603336ada66aa9c5e5f385 Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:39:13 +0100 Subject: [PATCH] fix(runtime): retain publications when recovery state is unreadable --- src/agent_runtime/process_resources.py | 12 +++++++++--- tests/test_wave3_launch_cost_lifecycle.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/agent_runtime/process_resources.py b/src/agent_runtime/process_resources.py index 68856ea66..fd2cd05d8 100644 --- a/src/agent_runtime/process_resources.py +++ b/src/agent_runtime/process_resources.py @@ -452,13 +452,19 @@ def prune_foreground_publications(): A dead/replaced manager cannot resume attachment. A missing receipt also makes attachment impossible; publication cannot reconstruct that receipt. - Its process tree still - belongs to containment recovery; deleting a publication never signals or + Its process tree still belongs to containment recovery; deleting a publication never signals or asserts tree death. Live/unverifiable managers and background history stay. """ from src import containment from src import process_ownership - receipts = containment._load_records() + try: + receipts = json.loads(containment._store_path().read_text()) + except FileNotFoundError: + receipts = {} + except (OSError, ValueError): + return 0 # Unreadable state is not evidence that consumers are gone. + if not isinstance(receipts, dict) or any(not isinstance(r, dict) for r in receipts.values()): + return 0 retired = 0 for path in _LAUNCH_DIR.glob("*.json"): try: diff --git a/tests/test_wave3_launch_cost_lifecycle.py b/tests/test_wave3_launch_cost_lifecycle.py index ef4329139..d7cb70b4e 100644 --- a/tests/test_wave3_launch_cost_lifecycle.py +++ b/tests/test_wave3_launch_cost_lifecycle.py @@ -168,3 +168,13 @@ def test_missing_receipt_publication_cannot_recover_authority(workspace): assert resources.prune_foreground_publications() == 1 assert not resources.launch_path(launch.generation).exists() assert not containment._load_records() + + +@pytest.mark.parametrize('receipt_data', ['{corrupt', '[]', '{"receipt":null}']) +def test_unreadable_receipts_cannot_retire_live_consumers(workspace, receipt_data): + admitted = authority(workspace) + launch = resources.resolve_process_operation(admitted, ExactOperation.normalize('bash', 'printf pending'), NativeBackendResource('bash')).launch + resources.publish_launch(launch, admitted, 'receipt') + containment._store_path().write_text(receipt_data) + assert resources.prune_foreground_publications() == 0 + assert resources.launch_path(launch.generation).is_file()