from __future__ import annotations from dataclasses import dataclass from pathlib import Path from typing import Any, Literal SessionStatus = Literal["ready", "interrupted", "failed"] SessionModality = Literal["point-cloud", "trajectory", "video"] LAB_REPLAY_CAPABILITY_SCHEMA = "missioncore.observation-lab-replay-capability/v1" class SessionStoreError(RuntimeError): """Base error for the host-owned observation session store.""" class SessionNotFoundError(SessionStoreError): """The requested opaque session identifier is not registered.""" class SessionNotReplayableError(SessionStoreError): """The session has no reviewed replay source.""" class SessionIntegrityError(SessionStoreError): """A stored artifact no longer satisfies its confinement/integrity boundary.""" class LayoutConflictError(SessionStoreError): """A workspace layout revision changed since the caller loaded it.""" @dataclass(frozen=True, slots=True) class LabReplayCapability: """Explicit, observation-only admission for a derived recorded replay.""" schema_version: Literal["missioncore.observation-lab-replay-capability/v1"] kind: Literal["canonical-recorded-rerun"] viewer_profile: Literal["recorded-session"] timeline: Literal["session_time"] activation: Literal["explicit"] commands_enabled: Literal[False] def __post_init__(self) -> None: expected_text = ( LAB_REPLAY_CAPABILITY_SCHEMA, "canonical-recorded-rerun", "recorded-session", "session_time", "explicit", ) if ( self.schema_version, self.kind, self.viewer_profile, self.timeline, self.activation, ) != expected_text or self.commands_enabled is not False: raise ValueError("LAB replay capability is invalid") def as_dict(self) -> dict[str, object]: return { "schema_version": self.schema_version, "kind": self.kind, "viewer_profile": self.viewer_profile, "timeline": self.timeline, "activation": self.activation, "commands_enabled": self.commands_enabled, } @dataclass(frozen=True, slots=True) class LabSessionBinding: """Immutable provenance for one derived laboratory replay.""" session_id: str source_session_id: str lab_id: str result_kind: str result_id: str source_result_id: str | None config_sha256: str | None run_created_at_utc: str published_at_utc: str replay_capability: LabReplayCapability | None provenance: dict[str, Any] def as_dict( self, *, include_replay_capability: bool = False, ) -> dict[str, Any]: document: dict[str, Any] = { "lab_id": self.lab_id, "source_session_id": self.source_session_id, "result_kind": self.result_kind, "result_id": self.result_id, "source_result_id": self.source_result_id, "config_sha256": self.config_sha256, "run_created_at_utc": self.run_created_at_utc, "published_at_utc": self.published_at_utc, "provenance": self.provenance, } if include_replay_capability: document["replay_capability"] = ( None if self.replay_capability is None else self.replay_capability.as_dict() ) return document @dataclass(frozen=True, slots=True) class SessionSource: source_id: str semantic_channel_id: str modality: SessionModality status: str seekable: bool artifact_id: str def as_dict(self) -> dict[str, Any]: return { "source_id": self.source_id, "semantic_channel_id": self.semantic_channel_id, "modality": self.modality, "status": self.status, "seekable": self.seekable, "artifact_id": self.artifact_id, } @dataclass(frozen=True, slots=True) class SessionArtifact: artifact_id: str kind: str media_type: str byte_length: int sha256: str | None integrity_status: str def as_dict(self) -> dict[str, Any]: return { "artifact_id": self.artifact_id, "kind": self.kind, "media_type": self.media_type, "byte_length": self.byte_length, "sha256": self.sha256, "integrity_status": self.integrity_status, } @dataclass(frozen=True, slots=True) class SessionSummary: session_id: str display_name: str status: SessionStatus started_at_utc: str | None completed_at_utc: str | None duration_seconds: float | None modalities: tuple[SessionModality, ...] source_count: int total_bytes: int replayable: bool origin: str lab: LabSessionBinding | None = None def as_dict(self) -> dict[str, Any]: document: dict[str, Any] = { "schema_version": "missioncore.observation-session-summary/v1", "session_id": self.session_id, "display_name": self.display_name, "status": self.status, "started_at_utc": self.started_at_utc, "completed_at_utc": self.completed_at_utc, "duration_seconds": self.duration_seconds, "modalities": list(self.modalities), "source_count": self.source_count, "total_bytes": self.total_bytes, "replayable": self.replayable, "origin": self.origin, } if self.lab is not None: document["lab"] = self.lab.as_dict() return document @dataclass(frozen=True, slots=True) class SessionDetail: summary: SessionSummary sources: tuple[SessionSource, ...] artifacts: tuple[SessionArtifact, ...] plugin_id: str archive_id: str def as_dict(self) -> dict[str, Any]: duration = self.summary.duration_seconds return { "schema_version": "missioncore.observation-session/v1", **{ key: value for key, value in self.summary.as_dict().items() if key != "schema_version" }, "timeline": { "mode": "recorded" if self.summary.replayable else "unavailable", "seekable": self.summary.replayable, "start_seconds": 0.0 if self.summary.replayable else None, "end_seconds": duration if self.summary.replayable else None, "synchronization": "host-arrival-best-effort", }, "sources": [source.as_dict() for source in self.sources], "artifacts": [artifact.as_dict() for artifact in self.artifacts], } @dataclass(frozen=True, slots=True) class SessionPage: items: tuple[SessionSummary, ...] next_cursor: str | None def as_dict(self) -> dict[str, Any]: return { "schema_version": "missioncore.observation-session-list/v1", "items": [item.as_dict() for item in self.items], "next_cursor": self.next_cursor, } @dataclass(frozen=True, slots=True) class WorkspaceLayout: workspace_id: str schema_version: int revision: int name: str layout: dict[str, Any] updated_at_utc: str def as_dict(self) -> dict[str, Any]: return { "schema_version": "missioncore.workspace-layout/v1", "workspace_id": self.workspace_id, "layout_schema_version": self.schema_version, "revision": self.revision, "name": self.name, "layout": self.layout, "updated_at_utc": self.updated_at_utc, } @dataclass(frozen=True, slots=True) class ReplayArtifact: """One confined input artifact selected for plugin-owned preparation.""" artifact_id: str path: Path media_type: str file_byte_length: int replay_byte_length: int expected_sha256: str | None @dataclass(frozen=True, slots=True) class ReplayCommand: """Internal replay request containing no vendor format or channel names.""" session_id: str plugin_id: str allowed_root: Path session_root: Path primary_artifact_id: str artifacts: tuple[ReplayArtifact, ...] timeline_origin_epoch_ns: int timeline_origin_monotonic_ns: int speed: float loop: bool @property def primary_artifact(self) -> ReplayArtifact: matches = tuple( artifact for artifact in self.artifacts if artifact.artifact_id == self.primary_artifact_id ) if len(matches) != 1: raise SessionIntegrityError("replay command has no unique primary artifact") return matches[0] @dataclass(frozen=True, slots=True) class RecordedMediaArtifact: """Internal handle for one confined archived camera source. The physical source id and filesystem locator never enter the public API. ``public_source_id`` and ``artifact_id`` are opaque catalog identifiers. """ session_id: str public_source_id: str artifact_id: str source_path: Path byte_length: int @dataclass(frozen=True, slots=True) class ObservationArtifactCandidate: artifact_id: str kind: str media_type: str locator: Path byte_length: int replay_byte_length: int sha256: str | None integrity_status: str @dataclass(frozen=True, slots=True) class ObservationSessionCandidate: session_id: str display_name: str status: SessionStatus started_at_utc: str | None completed_at_utc: str | None duration_seconds: float | None modalities: tuple[SessionModality, ...] replayable: bool total_bytes: int allowed_root: Path session_root: Path primary_replay_artifact_id: str | None timeline_origin_epoch_ns: int | None timeline_origin_monotonic_ns: int | None sources: tuple[SessionSource, ...] artifacts: tuple[ObservationArtifactCandidate, ...] @dataclass(frozen=True, slots=True) class LegacyMediaSourceCandidate: source_id: str artifact_id: str locator: Path byte_length: int epoch_count: int