from __future__ import annotations import threading from dataclasses import dataclass from datetime import UTC, datetime from typing import TYPE_CHECKING if TYPE_CHECKING: from collections.abc import Callable _MAX_SUMMARY_LENGTH = 500 @dataclass(frozen=True) class TaskProgressSnapshot: step_index: int step_status: str summary: str updated_at: datetime class TaskProgressHolder: """Thread-safe latest-step-progress holder for one in-flight assignment. Written by the execution thread (via ``update``, wired as the ``TaskRunner.on_step_progress`` callback) and read by the asyncio lease-renewal loop (via ``snapshot``) just before each renewal call. """ def __init__(self, *, now: Callable[[], datetime] | None = None) -> None: self._now = now or (lambda: datetime.now(UTC)) self._lock = threading.Lock() self._snapshot: TaskProgressSnapshot | None = None def update(self, step_index: int, step_status: str, summary: str) -> None: if len(summary) > _MAX_SUMMARY_LENGTH: summary = summary[:_MAX_SUMMARY_LENGTH] with self._lock: self._snapshot = TaskProgressSnapshot( step_index=step_index, step_status=step_status, summary=summary, updated_at=self._now(), ) def clear(self) -> None: with self._lock: self._snapshot = None def snapshot(self) -> TaskProgressSnapshot | None: with self._lock: return self._snapshot