"""Recoverable input epochs; resident model ownership is a separate lifetime. Used only by the explicitly enabled resumable adapter. The controller supplies the source-clock mapping and the profile adapter supplies verified decoder and sensor evidence. No socket, GPU, vehicle command or unbounded history lives here. """ from __future__ import annotations from dataclasses import dataclass, replace from hashlib import sha256 from .realtime_contract import StreamStart, _integer class StreamSuspended(RuntimeError): """Discard obsolete work, keep the resident model processes and lease.""" @dataclass(frozen=True, slots=True) class ResumeEvidence: camera_ns: int pose_ns: int oldest_points_ns: int newest_points_ns: int decoded_keyframe: bool def validate(self, *, cutoff_ns: int, source_now_ns: int) -> None: for name in ("camera_ns", "pose_ns", "oldest_points_ns", "newest_points_ns"): _integer(getattr(self, name), name) _integer(source_now_ns, "source clock") if ( self.decoded_keyframe is not True or not cutoff_ns <= self.pose_ns <= self.camera_ns <= source_now_ns or not cutoff_ns <= self.oldest_points_ns <= self.newest_points_ns <= self.camera_ns or source_now_ns - self.camera_ns > 250_000_000 or self.camera_ns - self.pose_ns > 100_000_000 or self.camera_ns - self.newest_points_ns > 100_000_000 or self.camera_ns - self.oldest_points_ns > 250_000_000 or max( abs(self.pose_ns - self.oldest_points_ns), abs(self.pose_ns - self.newest_points_ns) ) > 100_000_000 ): raise StreamSuspended("fresh keyframe and causal camera/pose/points required") class InputContinuity: """Access is serialized by StreamingLifecycle's lock.""" def __init__(self, activation: StreamStart) -> None: self.activation = self.epoch = activation self.phase = "active" self.generation = self.pauses = 0 self.reason: str | None = None self.cutoff_ns = 0 def check(self, epoch: StreamStart, *, synchronizing: bool = False) -> None: if epoch != self.epoch: raise StreamSuspended("obsolete input epoch") if self.phase != "active" and not (synchronizing and self.phase == "synchronizing"): raise StreamSuspended("input is waiting for resynchronization") def pause(self, reason: str) -> None: if reason not in ( "input-disconnected", "input-timeout", "source-gap", "worker-telemetry", "source-clock", ): raise ValueError("unknown recoverable pause reason") if self.phase != "waiting": self.pauses += 1 self.phase, self.reason = "waiting", reason def begin(self, source_now_ns: int) -> StreamStart: _integer(source_now_ns, "source clock") if self.phase != "waiting" or source_now_ns < self.cutoff_ns: raise StreamSuspended("new input needs a pause and nonregressing source clock") self.generation += 1 identity = sha256(f"{self.activation.epoch_id}:{self.generation}".encode()).hexdigest() # Wire v2 already binds every packet to the complete StreamStart. Change # the input epoch, NOT the resident lease generation/model activation. self.epoch = replace(self.activation, epoch_id="input-" + identity) self.cutoff_ns, self.phase = source_now_ns, "synchronizing" return self.epoch def snapshot(self) -> dict[str, object]: return { "phase": self.phase, "reason": self.reason, "input_generation": self.generation, "pauses": self.pauses, "input_start": self.epoch.to_dict(), "minimum_source_ns": str(self.cutoff_ns), "actuation_allowed": False, }