"""Freezer — snapshot a running bottle to a resumable artifact. Follows the same pattern as BottleBackend: a shared base class with common post-freeze steps (write committed-image path, mark preserved, print resume hint) and backend-specific subclasses in their respective backend directories. Entry points: Freezer.commit(agent) — freeze by ActiveAgent Freezer.commit_slug(slug) — convenience wrapper for cmd_commit get_freezer(backend_name) — factory """ from __future__ import annotations from abc import ABC, abstractmethod from . import ActiveAgent from ..bottle_state import mark_preserved, write_committed_image from ..log import die, info class CommitCancelled(Exception): """Raised by Freezer._freeze when the user declines a confirmation prompt.""" class Freezer(ABC): """Freezes a running bottle to a resumable artifact. The base class owns the shared post-commit steps: - write_committed_image — records the artifact path in per-bottle state - mark_preserved — prevents teardown from removing the state dir - resume hint — printed to stderr after the snapshot Subclasses implement _freeze with the backend-specific snapshot operation and optionally override _export_hint for migration hints. """ backend_name: str def commit(self, agent: ActiveAgent) -> None: """Freeze the bottle for `agent` to a resumable artifact. Calls _freeze for the backend-specific snapshot, then writes the committed image reference to per-bottle state and marks the bottle preserved so the next `./cli.py resume` boots from the snapshot. Raises CommitCancelled if the user declines an interactive confirmation prompt (e.g. the macos-container stop prompt). """ image_ref = self._freeze(agent) write_committed_image(agent.slug, image_ref) mark_preserved(agent.slug) info(f"to resume from this snapshot: ./cli.py resume {agent.slug}") self._export_hint(agent.slug, image_ref) @abstractmethod def _freeze(self, agent: ActiveAgent) -> str: """Backend-specific snapshot. Returns the image tag or artifact path stored by write_committed_image. Raises CommitCancelled if the user declines a stop-confirmation prompt.""" def _export_hint(self, slug: str, image_ref: str) -> None: """Optionally print an export-for-migration hint after committing. Overridden by backends that provide a meaningful export command.""" def commit_slug(self, slug: str) -> None: """Convenience entry for cmd_commit when only a slug is available.""" from ..bottle_state import read_metadata metadata = read_metadata(slug) agent = ActiveAgent( backend_name=self.backend_name, slug=slug, agent_name=metadata.agent_name if metadata else "", started_at=metadata.started_at if metadata else "", services=(), ) self.commit(agent) def get_freezer(backend_name: str) -> Freezer: """Return the Freezer for the named backend. backend_name "" is treated as "docker" for backward compatibility with state dirs written before the backend field was added.""" resolved = backend_name or "docker" if resolved == "docker": from .docker.freezer import DockerFreezer return DockerFreezer() if resolved == "macos-container": from .macos_container.freezer import MacosContainerFreezer return MacosContainerFreezer() if resolved == "smolmachines": from .smolmachines.freezer import SmolmachinesFreezer return SmolmachinesFreezer() die( f"commit is only supported for docker, macos-container, and " f"smolmachines; backend {backend_name!r} has no freezer" ) raise AssertionError("unreachable")