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"] 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 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 def as_dict(self) -> dict[str, Any]: return { "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, } @dataclass(frozen=True, slots=True) class SessionDetail: summary: SessionSummary sources: tuple[SessionSource, ...] artifacts: tuple[SessionArtifact, ...] 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 ReplayCommand: """Internal-only replay command. ``source_path`` never enters an API DTO.""" session_id: str source_path: Path allowed_root: Path session_root: Path replay_byte_length: int metadata_byte_length: int expected_source_sha256: str | None speed: float loop: bool @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 LegacySessionCandidate: 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 raw_path: Path raw_byte_length: int replay_raw_byte_length: int replay_metadata_byte_length: int raw_sha256: str | None raw_integrity_status: str media_sources: tuple[LegacyMediaSourceCandidate, ...] @dataclass(frozen=True, slots=True) class LegacyMediaSourceCandidate: source_id: str artifact_id: str locator: Path byte_length: int epoch_count: int