"""Observable lease state for long-running executions. The monitor is deliberately independent of an adapter. A heartbeat worker can mark a lease lost, while the executor observes that fact and decides how to cancel or refuse acceptance at its own boundary. """ from __future__ import annotations import threading from dataclasses import dataclass from datetime import datetime, timezone from typing import Callable @dataclass(frozen=True) class LeaseLoss: """Bounded evidence that the worker no longer owns a lease.""" error_type: str observed_at: str class LeaseMonitor: """Thread-safe active/lost/stopped state with a one-shot loss callback.""" def __init__(self, on_lost: Callable[[LeaseLoss], None] | None = None) -> None: self._lock = threading.Lock() self._lost = threading.Event() self._stopped = False self._evidence: LeaseLoss | None = None self._on_lost = on_lost @property def lost(self) -> bool: return self._lost.is_set() @property def stopped(self) -> bool: with self._lock: return self._stopped @property def evidence(self) -> LeaseLoss | None: with self._lock: return self._evidence def wait_lost(self, timeout: float | None = None) -> bool: """Wait until lease loss is observed; return whether it occurred.""" return self._lost.wait(timeout) def mark_lost(self, error_type: str) -> LeaseLoss: """Record the first loss and invoke the callback once. Error text is intentionally not retained: provider responses can carry credentials, prompts, or other unbounded data. """ callback: Callable[[LeaseLoss], None] | None = None with self._lock: if self._evidence is None: evidence = LeaseLoss( error_type=error_type[:80] or "OpsRunError", observed_at=datetime.now(timezone.utc).isoformat(), ) self._evidence = evidence self._lost.set() callback = self._on_lost else: evidence = self._evidence if callback is not None: callback(evidence) return evidence def stop(self) -> None: with self._lock: self._stopped = True