diff --git a/rein_aharness/claim_loop.py b/rein_aharness/claim_loop.py index eefeeed..0d04921 100644 --- a/rein_aharness/claim_loop.py +++ b/rein_aharness/claim_loop.py @@ -179,6 +179,23 @@ def process_one( finally: hb.stop() + if hb.monitor.lost: + loss = hb.monitor.evidence + assert loss is not None + return ProcessResult( + claimed=True, + run_id=run.id, + approach=approach, + ok=False, + reason=f"lease lost ({loss.error_type})", + detail={ + "lease_loss": { + "error_type": loss.error_type, + "observed_at": loss.observed_at, + } + }, + ) + payload = { "approach": ar.approach, "ok": ar.ok, @@ -277,6 +294,23 @@ def _process_profiled_run( finally: hb.stop() + if hb.monitor.lost: + loss = hb.monitor.evidence + assert loss is not None + return ProcessResult( + claimed=True, + run_id=run.id, + approach=GLAS_APPROACH, + ok=False, + reason=f"lease lost ({loss.error_type})", + detail={ + "lease_loss": { + "error_type": loss.error_type, + "observed_at": loss.observed_at, + } + }, + ) + evidence = gateway_result["evidence"] ok = gateway_result["ok"] reason = str(evidence.get("error") or evidence.get("outcome") or "Glas execution failed") diff --git a/tests/test_claim_loop.py b/tests/test_claim_loop.py index 91fb880..0e1b689 100644 --- a/tests/test_claim_loop.py +++ b/tests/test_claim_loop.py @@ -3,6 +3,7 @@ from __future__ import annotations from unittest.mock import MagicMock, patch +import time from rein_aharness.approaches import ApproachResult, APPROACH_FI_RESEARCH_BRIEF from rein_aharness.claim_loop import process_one, poll_peek @@ -105,6 +106,35 @@ def test_process_one_failure_reopens() -> None: assert client.fail.call_args.kwargs["reopen"] is True +def test_process_one_refuses_close_after_lease_loss() -> None: + client = MagicMock(spec=ActivityCoreOpsClient) + client.config = OpsRunConfig(worker_id="w", lease_seconds=90) + client.claim.return_value = [_claimed_run()] + client.heartbeat.side_effect = OpsRunError("lease rejected") + ar = ApproachResult( + ok=True, + approach=APPROACH_FI_RESEARCH_BRIEF, + result={"path": "briefs/x.md"}, + reason="ok", + ) + + def slow_execute(*_args, **_kwargs): + time.sleep(0.05) + return ar + + with ( + patch("rein_aharness.claim_loop._heartbeat_interval", return_value=0.01), + patch("rein_aharness.claim_loop.execute_approach", side_effect=slow_execute), + ): + result = process_one(client) + + assert result.ok is False + assert result.reason.startswith("lease lost") + assert "lease_loss" in result.detail + client.complete.assert_not_called() + client.fail.assert_not_called() + + def test_poll_peek() -> None: client = MagicMock(spec=ActivityCoreOpsClient) client.list_open.return_value = [_claimed_run()] diff --git a/workplans/HARNESS-WP-0003-governed-runtime-integrity.md b/workplans/HARNESS-WP-0003-governed-runtime-integrity.md index 389049f..fc9ed11 100644 --- a/workplans/HARNESS-WP-0003-governed-runtime-integrity.md +++ b/workplans/HARNESS-WP-0003-governed-runtime-integrity.md @@ -201,7 +201,10 @@ evidence instead of raw provider exception text. This establishes the signal needed by execution and acceptance boundaries without pretending that adapters can already cancel in-flight work. Remaining work is to classify Activity Core responses, connect cancellation at each adapter boundary, and refuse result -acceptance/close after loss. +acceptance/close after loss. The claim loop now implements that last refusal for +normal and successful profiled executions: a lost lease returns bounded loss +evidence and skips Activity Core completion/failure calls, leaving reconciliation +to the owner of the expired lease. ## Verify accepted commits and reconcile metrics/reporting