File created successfully at: /Users/agent/strukto/mirage/mirage/workspace/workspace.py

from __future__ import annotations

import time

from mirage.backend.base import BaseBackend
from mirage.backend.filetype import guess_type
from mirage.types import (ConsistencyPolicy, FileStat, Inode, MountMode,
                          SyncPolicy, SyncResult, TransferStats)
from mirage.workspace.cache import LRUCache
from mirage.workspace.cache.policy import is_fresh
from mirage.workspace.mount import MountRegistry
from mirage.workspace.sync import DirtyTracker

class _PathLock:

    def __init__(self, ws: Workspace, paths: list[str]) -> None:
        self._ws = ws
        self._paths = paths

    def __enter__(self) -> _PathLock:
        return self

    def __exit__(self, *_: object) -> None:
        self._ws.unlock_paths(self._paths)

class Workspace:
    """
    Unified virtual filesystem over heterogeneous backends.

    Mounts backends at virtual path prefixes and provides a command-level API
    (cat, ls, write, cp, mv, rm, ...) with transparent caching
    and write staging.

    Example::

        ws = Workspace(
            {"/data/": S3Backend(S3Config(bucket="my-bucket")),
             "/tmp/":  MemoryBackend()},
            cache_limit="256MB",
            sync_policy=SyncPolicy.STAGED,
        )
        content = ws.cat("/data/reports/q1.json")
        ws.write("/tmp/summary.txt", b"hello")
        ws.sync()
    """

    def __init__(
        self,
        backends: dict[str, BaseBackend | tuple],
        cache_limit: str | int = "512MB",
        sync_policy: SyncPolicy = SyncPolicy.STAGED,
        mode: MountMode = MountMode.READ,
        consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED,
        ttl_seconds: int = 300,
    ) -> None:
        self._registry = MountRegistry()
        self._cache = LRUCache(cache_limit)
        self._tracker = DirtyTracker()
        self._sync_policy = sync_policy
        self._ttl_seconds = ttl_seconds
        self._locked_paths: set[str] = set()
        self._transfer_stats: dict[str, TransferStats] = {}
        self._created_at: float = time.monotonic()

        for prefix, value in backends.items():
            if isinstance(value, tuple) and len(value) == 3:
                backend, mount_mode, mount_consistency = value
            elif isinstance(value, tuple):
                backend, mount_mode = value
                mount_consistency = consistency
            else:
                backend = value
                mount_mode = mode
                mount_consistency = consistency
            self._registry.mount(prefix, backend, mount_mode,
                                 mount_consistency)

    # ── mount management ────────────────────────────────────────────────────

    def mount(self,
              prefix: str,
              backend: BaseBackend,
              mode: MountMode = MountMode.READ,
              consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED) -> None:
        self._registry.mount(prefix, backend, mode, consistency)

    def backend(self, path: str) -> BaseBackend:
        backend, _, _ = self._registry.resolve(path)
        return backend

    # ── read ops ────────────────────────────────────────────────────────────

    def cat(self, path: str) -> bytes:
        inode = self._tracker.get(path)
        if inode and inode.dirty:
            entry = self._cache.get(path)
            if entry is not None:
                self._record_transfer(path,
                                      "read",
                                      len(entry.data),
                                      0,
                                      cache_hit=True)
                return entry.data

        entry = self._cache.get(path)
        remote_stat = None
        if entry is not None:
            consistency = self._registry.mount_for(path).consistency
            backend, bp, _ = self._registry.resolve(path)
            fresh, remote_stat = is_fresh(entry, consistency,
                                          self._ttl_seconds, backend, bp)
            if fresh:
                self._record_transfer(path,
                                      "read",
                                      len(entry.data),
                                      0,
                                      cache_hit=True)
                return entry.data
        else:
            backend, bp, _ = self._registry.resolve(path)

        t0 = time.monotonic()
        data = backend.cat(bp)
        duration_ms = int((time.monotonic() - t0) * 1000)
        self._record_transfer(path,
                              "read",
                              len(data),
                              duration_ms,
                              cache_hit=False)
        if remote_stat is None:
            remote_stat = backend.stat(bp)
        self._cache.put(path,
                        data,
                        etag=remote_stat.extra.get("etag"),
                        modified=remote_stat.modified,
                        size=remote_stat.size)
        self._tracker.on_read(path, data, self._mount_prefix(path))
        return data

    def ls(self, path: str) -> list[str]:
        backend, bp, _ = self._registry.resolve(path)
        prefix = self._mount_prefix(path).rstrip("/")
        backend_ok = False
        try:
            backend_entries = backend.ls(bp)
            backend_ok = True
        except FileNotFoundError:
            backend_entries = []
        result: set[str] = {
            prefix + "/" + e.lstrip("/")
            for e in backend_entries
        }
        norm_path = "/" + path.strip("/")
        dir_prefix = norm_path.rstrip("/") + "/"
        has_staged_children = False
        for inode in self._tracker.dirty():
            vp = inode.virtual_path
            if not vp.startswith(dir_prefix):
                continue
            if inode.checksum is None:
                result.discard(vp)
                continue
            has_staged_children = True
            remainder = vp[len(dir_prefix):]
            first = remainder.split("/")[0]
            if not first:
                continue
            if "/" in remainder:
                result.add(dir_prefix + first + "/")
            else:
                result.add(dir_prefix + first)
        if not backend_ok and not has_staged_children:
            raise FileNotFoundError(path)
        return sorted(result)

    def ls_l(self, path: str) -> list[FileStat]:
        backend, bp, _ = self._registry.resolve(path)
        return backend.ls_l(bp)

    def stat(self, path: str) -> FileStat:
        inode = self._tracker.get(path)
        if inode and inode.dirty and inode.checksum is not None:
            entry = self._cache.get(path)
            size = len(entry.data) if entry is not None else inode.size
            return FileStat(
                name=path.rstrip("/").rsplit("/", 1)[-1],
                size=size,
                type=guess_type(path),
            )
        backend, bp, _ = self._registry.resolve(path)
        return backend.stat(bp)

    def exists(self, path: str) -> bool:
        try:
            self.stat(path)
            return True
        except (FileNotFoundError, ValueError):
            return False

    def head(self, path: str, lines: int = 10) -> bytes:
        backend, bp, _ = self._registry.resolve(path)
        return backend.head(bp, lines)

    def tail(self, path: str, lines: int = 10) -> bytes:
        backend, bp, _ = self._registry.resolve(path)
        return backend.tail(bp, lines)

    def read(self, path: str, offset: int, size: int) -> bytes:
        inode = self._tracker.get(path)
        if inode and inode.dirty:
            entry = self._cache.get(path)
            if entry is not None:
                return entry.data[offset:offset + size]
        backend, bp, _ = self._registry.resolve(path)
        return backend.read(bp, offset, size)

    def grep(self, pattern: str, path: str) -> list[str]:
        backend, bp, _ = self._registry.resolve(path)
        return backend.grep(pattern, bp)

    # ── write ops ───────────────────────────────────────────────────────────

    def write(self, path: str, data: bytes) -> None:
        self._assert_not_locked(path)
        self._assert_writable(path)
        backend, bp, _ = self._registry.resolve(path)
        self._cache.put(path, data)
        self._tracker.on_write(path, data, self._mount_prefix(path))
        self._record_transfer(path, "write", len(data), 0)
        if self._sync_policy == SyncPolicy.IMMEDIATE:
            backend.put(bp, data)
            self._tracker.on_read(path, data, self._mount_prefix(path))

    def mkdir(self, path: str) -> None:
        self._assert_writable(path)
        backend, bp, _ = self._registry.resolve(path)
        backend.mkdir(bp)

    def rm(self, path: str) -> None:
        self._assert_writable(path)
        backend, bp, _ = self._registry.resolve(path)
        inode = self._tracker.get(path)
        staged_only = inode and inode.dirty and inode.remote_checksum is None
        if not staged_only:
            backend.unlink(bp)
        self._cache.invalidate(path)
        self._tracker.on_delete(path)

    def mv(self, src: str, dst: str) -> None:
        self._assert_writable(src)
        sb, sbp, _ = self._registry.resolve(src)
        db, dbp, _ = self._registry.resolve(dst)
        src_inode = self._tracker.get(src)
        staged_only = (src_inode and src_inode.dirty
                       and src_inode.remote_checksum is None)
        if staged_only:
            entry = self._cache.get(src)
            data = entry.data if entry else b""
            self._cache.put(dst, data)
            self._tracker.on_write(dst, data, self._mount_prefix(dst))
            self._cache.invalidate(src)
            self._tracker.on_delete(src)
        elif sb is db:
            sb.rename(sbp, dbp)
            self._cache.invalidate(src)
        else:
            data = sb.cat(sbp)
            db.put(dbp, data)
            sb.unlink(sbp)
            self._cache.invalidate(src)

    def cp(self, src: str, dst: str) -> None:
        self._assert_writable(dst)
        data = self.cat(src)
        db, dbp, _ = self._registry.resolve(dst)
        db.put(dbp, data)

    # ── sync lifecycle ──────────────────────────────────────────────────────

    def sync(self, path: str | None = None) -> list[SyncResult]:
        results: list[SyncResult] = []
        targets = [
            i for i in self._tracker.dirty()
            if path is None or i.virtual_path == path
        ]
        for inode in targets:
            backend, bp, _ = self._registry.resolve(inode.virtual_path)
            entry = self._cache.get(inode.virtual_path)
            data = entry.data if entry else None
            result = self._tracker.on_sync(inode.virtual_path, bp, data,
                                           backend)
            results.append(result)
        return results

    def status(self) -> list[Inode]:
        return self._tracker.status()

    def reset(self) -> None:
        self._tracker.reset()
        self._cache.clear()

    # ── path locking ─────────────────────────────────────────────────────────

    def _lock_path(self, path: str) -> None:
        self._locked_paths.add(path)

    def unlock_paths(self, paths: list[str]) -> None:
        for p in paths:
            self._locked_paths.discard(p)

    def lock_paths(self, paths: list[str]) -> _PathLock:
        for p in paths:
            self._lock_path(p)
        return _PathLock(self, paths)

    def _assert_not_locked(self, path: str) -> None:
        if path in self._locked_paths:
            raise PermissionError(
                f"path {path!r} is locked by remote execution")

    # ── helpers ─────────────────────────────────────────────────────────────

    def _mount_prefix(self, path: str) -> str:
        norm = "/" + path.strip("/")
        for m in self._registry.mounts():
            if norm == m.prefix.rstrip("/") or norm.startswith(m.prefix):
                return m.prefix
        return "/"

    def _assert_writable(self, path: str) -> None:
        _, _, mode = self._registry.resolve(path)
        if mode == MountMode.READ:
            raise PermissionError(f"mount at {path!r} is read-only")

    def _record_transfer(self,
                         path: str,
                         op: str,
                         nbytes: int,
                         duration_ms: int,
                         cache_hit: bool | None = None) -> None:
        prefix = self._mount_prefix(path)
        if prefix not in self._transfer_stats:
            self._transfer_stats[prefix] = TransferStats()
        stats = self._transfer_stats[prefix]
        if op == "read":
            stats.bytes_read += nbytes
            stats.read_ops += 1
            stats.total_read_ms += duration_ms
            if cache_hit is True:
                stats.cache_hits += 1
            elif cache_hit is False:
                stats.cache_misses += 1
        elif op == "write":
            stats.bytes_written += nbytes
            stats.write_ops += 1
            stats.total_write_ms += duration_ms


Now rewrite `__init__.py` as just re-exports:


Write

from mirage.workspace.workspace import Workspace

__all__ = ["Workspace"]


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


     1→from __future__ import annotations
     2→
     3→import time

from __future__ import annotations

import time


from mirage.workspace.workspace import Workspace

__all__ = ["Workspace"]


The file /Users/agent/strukto/mirage/mirage/workspace/__init__.py has been updated successfully.

from mirage.workspace.workspace import Workspace

__all__ = ["Workspace"]

+from mirage.workspace.workspace import Workspace

-from mirage.backend.base import BaseBackend

-from mirage.backend.filetype import guess_type

-from mirage.types import (ConsistencyPolicy, FileStat, Inode, MountMode,

-                          SyncPolicy, SyncResult, TransferStats)

-from mirage.workspace.cache import LRUCache

-from mirage.workspace.cache.policy import is_fresh

-from mirage.workspace.mount import MountRegistry

-from mirage.workspace.sync import DirtyTracker

-    def __init__(self, ws: Workspace, paths: list[str]) -> None:

-    def __exit__(self, *_: object) -> None:

-        self._ws.unlock_paths(self._paths)

-    Unified virtual filesystem over heterogeneous backends.

-    Mounts backends at virtual path prefixes and provides a command-level API

-    (cat, ls, write, cp, mv, rm, ...) with transparent caching

-            {"/data/": S3Backend(S3Config(bucket="my-bucket")),

-             "/tmp/":  MemoryBackend()},

-            sync_policy=SyncPolicy.STAGED,

-        content = ws.cat("/data/reports/q1.json")

-        ws.write("/tmp/summary.txt", b"hello")

-        backends: dict[str, BaseBackend | tuple],

-        cache_limit: str | int = "512MB",

-        sync_policy: SyncPolicy = SyncPolicy.STAGED,

-        mode: MountMode = MountMode.READ,

-        consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED,

-        self._registry = MountRegistry()

-        self._cache = LRUCache(cache_limit)

-        self._sync_policy = sync_policy

-        self._ttl_seconds = ttl_seconds

-        self._locked_paths: set[str] = set()

-        self._transfer_stats: dict[str, TransferStats] = {}

-        self._created_at: float = time.monotonic()

-        for prefix, value in backends.items():

-            if isinstance(value, tuple) and len(value) == 3:

-                backend, mount_mode, mount_consistency = value

-            elif isinstance(value, tuple):

-                backend, mount_mode = value

-                mount_consistency = consistency

-            self._registry.mount(prefix, backend, mount_mode,

-                                 mount_consistency)

-    # ── mount management ────────────────────────────────────────────────────

-              mode: MountMode = MountMode.READ,

-              consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED) -> None:

-        self._registry.mount(prefix, backend, mode, consistency)

-    def backend(self, path: str) -> BaseBackend:

-        backend, _, _ = self._registry.resolve(path)

-    # ── read ops ────────────────────────────────────────────────────────────

-        inode = self._tracker.get(path)

-            entry = self._cache.get(path)

-                self._record_transfer(path,

-                                      "read",

-                                      len(entry.data),

-                                      cache_hit=True)

-            consistency = self._registry.mount_for(path).consistency

-            backend, bp, _ = self._registry.resolve(path)

-            fresh, remote_stat = is_fresh(entry, consistency,

-                                          self._ttl_seconds, backend, bp)

-        duration_ms = int((time.monotonic() - t0) * 1000)

-                              len(data),

-                              duration_ms,

-                              cache_hit=False)

-            remote_stat = backend.stat(bp)

-                        etag=remote_stat.extra.get("etag"),

-                        modified=remote_stat.modified,

-                        size=remote_stat.size)

-        self._tracker.on_read(path, data, self._mount_prefix(path))

-    def ls(self, path: str) -> list[str]:

-        prefix = self._mount_prefix(path).rstrip("/")

-            backend_entries = backend.ls(bp)

-            prefix + "/" + e.lstrip("/")

-        norm_path = "/" + path.strip("/")

-        dir_prefix = norm_path.rstrip("/") + "/"

-        for inode in self._tracker.dirty():

-            if not vp.startswith(dir_prefix):

-            remainder = vp[len(dir_prefix):]

-            first = remainder.split("/")[0]

-                result.add(dir_prefix + first + "/")

-                result.add(dir_prefix + first)

-        if not backend_ok and not has_staged_children:

-            raise FileNotFoundError(path)

-    def ls_l(self, path: str) -> list[FileStat]:

-    def stat(self, path: str) -> FileStat:

-        # For staged writes, build FileStat from the tracker rather than

-        # hitting the backend (which hasn't received the write yet).

-        inode = self._tracker.get(path)

-        if inode and inode.dirty and inode.checksum is not None:

-            entry = self._cache.get(path)

-            size = len(entry.data) if entry is not None else inode.size

-    def exists(self, path: str) -> bool:

-        except (FileNotFoundError, ValueError):

-    def head(self, path: str, lines: int = 10) -> bytes:

-    def tail(self, path: str, lines: int = 10) -> bytes:

-    def read(self, path: str, offset: int, size: int) -> bytes:

-                return entry.data[offset:offset + size]

-        return backend.read(bp, offset, size)

-    def grep(self, pattern: str, path: str) -> list[str]:

-        return backend.grep(pattern, bp)

-    # ── write ops ───────────────────────────────────────────────────────────

-    def write(self, path: str, data: bytes) -> None:

-        self._tracker.on_write(path, data, self._mount_prefix(path))

-        self._record_transfer(path, "write", len(data), 0)

-        if self._sync_policy == SyncPolicy.IMMEDIATE:

-    def mkdir(self, path: str) -> None:

-        inode = self._tracker.get(path)

-        staged_only = inode and inode.dirty and inode.remote_checksum is None

-    def mv(self, src: str, dst: str) -> None:

-        sb, sbp, _ = self._registry.resolve(src)

-        db, dbp, _ = self._registry.resolve(dst)

-        src_inode = self._tracker.get(src)

-        staged_only = (src_inode and src_inode.dirty

-                       and src_inode.remote_checksum is None)

-            entry = self._cache.get(src)

-            data = entry.data if entry else b""

-            self._tracker.on_write(dst, data, self._mount_prefix(dst))

-            self._cache.invalidate(src)

-            self._tracker.on_delete(src)

-    def cp(self, src: str, dst: str) -> None:

-    # ── sync lifecycle ──────────────────────────────────────────────────────

-    def sync(self, path: str | None = None) -> list[SyncResult]:

-            i for i in self._tracker.dirty()

-            if path is None or i.virtual_path == path

-            backend, bp, _ = self._registry.resolve(inode.virtual_path)

-            entry = self._cache.get(inode.virtual_path)

-            data = entry.data if entry else None

-            result = self._tracker.on_sync(inode.virtual_path, bp, data,

-                                           backend)

-    # ── path locking ─────────────────────────────────────────────────────────

-    def _lock_path(self, path: str) -> None:

-    def unlock_paths(self, paths: list[str]) -> None:

-            self._locked_paths.discard(p)

-    def lock_paths(self, paths: list[str]) -> _PathLock:

-    def _assert_not_locked(self, path: str) -> None:

-                f"path {path!r} is locked by remote execution")

-    # ── helpers ─────────────────────────────────────────────────────────────

-    def _mount_prefix(self, path: str) -> str:

-        for m in self._registry.mounts():

-            if norm == m.prefix.rstrip("/") or norm.startswith(m.prefix):

-    def _assert_writable(self, path: str) -> None:

-        _, _, mode = self._registry.resolve(path)

-            raise PermissionError(f"mount at {path!r} is read-only")

-                         duration_ms: int,

-                         cache_hit: bool | None = None) -> None:

-        prefix = self._mount_prefix(path)

-        if prefix not in self._transfer_stats:

-            self._transfer_stats[prefix] = TransferStats()

-        stats = self._transfer_stats[prefix]

-            stats.total_read_ms += duration_ms

-                stats.cache_misses += 1

-            stats.bytes_written += nbytes

-            stats.total_write_ms += duration_ms

from __future__ import annotations

import time

from mirage.backend.base import BaseBackend
from mirage.backend.filetype import guess_type
from mirage.types import (ConsistencyPolicy, FileStat, Inode, MountMode,
                          SyncPolicy, SyncResult, TransferStats)
from mirage.workspace.cache import LRUCache
from mirage.workspace.cache.policy import is_fresh
from mirage.workspace.mount import MountRegistry
from mirage.workspace.sync import DirtyTracker

class _PathLock:

    def __init__(self, ws: Workspace, paths: list[str]) -> None:
        self._ws = ws
        self._paths = paths

    def __enter__(self) -> _PathLock:
        return self

    def __exit__(self, *_: object) -> None:
        self._ws.unlock_paths(self._paths)

class Workspace:
    """
    Unified virtual filesystem over heterogeneous backends.

    Mounts backends at virtual path prefixes and provides a command-level API
    (cat, ls, write, cp, mv, rm, ...) with transparent caching
    and write staging.

    Example::

        ws = Workspace(
            {"/data/": S3Backend(S3Config(bucket="my-bucket")),
             "/tmp/":  MemoryBackend()},
            cache_limit="256MB",
            sync_policy=SyncPolicy.STAGED,
        )
        content = ws.cat("/data/reports/q1.json")
        ws.write("/tmp/summary.txt", b"hello")
        ws.sync()
    """

    def __init__(
        self,
        backends: dict[str, BaseBackend | tuple],
        cache_limit: str | int = "512MB",
        sync_policy: SyncPolicy = SyncPolicy.STAGED,
        mode: MountMode = MountMode.READ,
        consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED,
        ttl_seconds: int = 300,
    ) -> None:
        self._registry = MountRegistry()
        self._cache = LRUCache(cache_limit)
        self._tracker = DirtyTracker()
        self._sync_policy = sync_policy
        self._ttl_seconds = ttl_seconds
        self._locked_paths: set[str] = set()
        self._transfer_stats: dict[str, TransferStats] = {}
        self._created_at: float = time.monotonic()

        for prefix, value in backends.items():
            if isinstance(value, tuple) and len(value) == 3:
                backend, mount_mode, mount_consistency = value
            elif isinstance(value, tuple):
                backend, mount_mode = value
                mount_consistency = consistency
            else:
                backend = value
                mount_mode = mode
                mount_consistency = consistency
            self._registry.mount(prefix, backend, mount_mode,
                                 mount_consistency)

    # ── mount management ────────────────────────────────────────────────────

    def mount(self,
              prefix: str,
              backend: BaseBackend,
              mode: MountMode = MountMode.READ,
              consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED) -> None:
        self._registry.mount(prefix, backend, mode, consistency)

    def backend(self, path: str) -> BaseBackend:
        backend, _, _ = self._registry.resolve(path)
        return backend

    # ── read ops ────────────────────────────────────────────────────────────

    def cat(self, path: str) -> bytes:
        inode = self._tracker.get(path)
        if inode and inode.dirty:
            entry = self._cache.get(path)
            if entry is not None:
                self._record_transfer(path,
                                      "read",
                                      len(entry.data),
                                      0,
                                      cache_hit=True)
                return entry.data

        entry = self._cache.get(path)
        remote_stat = None
        if entry is not None:
            consistency = self._registry.mount_for(path).consistency
            backend, bp, _ = self._registry.resolve(path)
            fresh, remote_stat = is_fresh(entry, consistency,
                                          self._ttl_seconds, backend, bp)
            if fresh:
                self._record_transfer(path,
                                      "read",
                                      len(entry.data),
                                      0,
                                      cache_hit=True)
                return entry.data
        else:
            backend, bp, _ = self._registry.resolve(path)

        t0 = time.monotonic()
        data = backend.cat(bp)
        duration_ms = int((time.monotonic() - t0) * 1000)
        self._record_transfer(path,
                              "read",
                              len(data),
                              duration_ms,
                              cache_hit=False)
        if remote_stat is None:
            remote_stat = backend.stat(bp)
        self._cache.put(path,
                        data,
                        etag=remote_stat.extra.get("etag"),
                        modified=remote_stat.modified,
                        size=remote_stat.size)
        self._tracker.on_read(path, data, self._mount_prefix(path))
        return data

    def ls(self, path: str) -> list[str]:
        backend, bp, _ = self._registry.resolve(path)
        prefix = self._mount_prefix(path).rstrip("/")
        backend_ok = False
        try:
            backend_entries = backend.ls(bp)
            backend_ok = True
        except FileNotFoundError:
            backend_entries = []
        result: set[str] = {
            prefix + "/" + e.lstrip("/")
            for e in backend_entries
        }
        norm_path = "/" + path.strip("/")
        dir_prefix = norm_path.rstrip("/") + "/"
        has_staged_children = False
        for inode in self._tracker.dirty():
            vp = inode.virtual_path
            if not vp.startswith(dir_prefix):
                continue
            if inode.checksum is None:
                result.discard(vp)
                continue
            has_staged_children = True
            remainder = vp[len(dir_prefix):]
            first = remainder.split("/")[0]
            if not first:
                continue
            if "/" in remainder:
                result.add(dir_prefix + first + "/")
            else:
                result.add(dir_prefix + first)
        if not backend_ok and not has_staged_children:
            raise FileNotFoundError(path)
        return sorted(result)

    def ls_l(self, path: str) -> list[FileStat]:
        backend, bp, _ = self._registry.resolve(path)
        return backend.ls_l(bp)

    def stat(self, path: str) -> FileStat:
        # For staged writes, build FileStat from the tracker rather than
        # hitting the backend (which hasn't received the write yet).
        inode = self._tracker.get(path)
        if inode and inode.dirty and inode.checksum is not None:
            entry = self._cache.get(path)
            size = len(entry.data) if entry is not None else inode.size
            return FileStat(
                name=path.rstrip("/").rsplit("/", 1)[-1],
                size=size,
                type=guess_type(path),
            )
        backend, bp, _ = self._registry.resolve(path)
        return backend.stat(bp)

    def exists(self, path: str) -> bool:
        try:
            self.stat(path)
            return True
        except (FileNotFoundError, ValueError):
            return False

    def head(self, path: str, lines: int = 10) -> bytes:
        backend, bp, _ = self._registry.resolve(path)
        return backend.head(bp, lines)

    def tail(self, path: str, lines: int = 10) -> bytes:
        backend, bp, _ = self._registry.resolve(path)
        return backend.tail(bp, lines)

    def read(self, path: str, offset: int, size: int) -> bytes:
        inode = self._tracker.get(path)
        if inode and inode.dirty:
            entry = self._cache.get(path)
            if entry is not None:
                return entry.data[offset:offset + size]
        backend, bp, _ = self._registry.resolve(path)
        return backend.read(bp, offset, size)

    def grep(self, pattern: str, path: str) -> list[str]:
        backend, bp, _ = self._registry.resolve(path)
        return backend.grep(pattern, bp)

    # ── write ops ───────────────────────────────────────────────────────────

    def write(self, path: str, data: bytes) -> None:
        self._assert_not_locked(path)
        self._assert_writable(path)
        backend, bp, _ = self._registry.resolve(path)
        self._cache.put(path, data)
        self._tracker.on_write(path, data, self._mount_prefix(path))
        self._record_transfer(path, "write", len(data), 0)
        if self._sync_policy == SyncPolicy.IMMEDIATE:
            backend.put(bp, data)
            self._tracker.on_read(path, data, self._mount_prefix(path))

    def mkdir(self, path: str) -> None:
        self._assert_writable(path)
        backend, bp, _ = self._registry.resolve(path)
        backend.mkdir(bp)

    def rm(self, path: str) -> None:
        self._assert_writable(path)
        backend, bp, _ = self._registry.resolve(path)
        inode = self._tracker.get(path)
        staged_only = inode and inode.dirty and inode.remote_checksum is None
        if not staged_only:
            backend.unlink(bp)
        self._cache.invalidate(path)
        self._tracker.on_delete(path)

    def mv(self, src: str, dst: str) -> None:
        self._assert_writable(src)
        sb, sbp, _ = self._registry.resolve(src)
        db, dbp, _ = self._registry.resolve(dst)
        src_inode = self._tracker.get(src)
        staged_only = (src_inode and src_inode.dirty
                       and src_inode.remote_checksum is None)
        if staged_only:
            entry = self._cache.get(src)
            data = entry.data if entry else b""
            self._cache.put(dst, data)
            self._tracker.on_write(dst, data, self._mount_prefix(dst))
            self._cache.invalidate(src)
            self._tracker.on_delete(src)
        elif sb is db:
            sb.rename(sbp, dbp)
            self._cache.invalidate(src)
        else:
            data = sb.cat(sbp)
            db.put(dbp, data)
            sb.unlink(sbp)
            self._cache.invalidate(src)

    def cp(self, src: str, dst: str) -> None:
        self._assert_writable(dst)
        data = self.cat(src)
        db, dbp, _ = self._registry.resolve(dst)
        db.put(dbp, data)

    # ── sync lifecycle ──────────────────────────────────────────────────────

    def sync(self, path: str | None = None) -> list[SyncResult]:
        results: list[SyncResult] = []
        targets = [
            i for i in self._tracker.dirty()
            if path is None or i.virtual_path == path
        ]
        for inode in targets:
            backend, bp, _ = self._registry.resolve(inode.virtual_path)
            entry = self._cache.get(inode.virtual_path)
            data = entry.data if entry else None
            result = self._tracker.on_sync(inode.virtual_path, bp, data,
                                           backend)
            results.append(result)
        return results

    def status(self) -> list[Inode]:
        return self._tracker.status()

    def reset(self) -> None:
        self._tracker.reset()
        self._cache.clear()

    # ── path locking ─────────────────────────────────────────────────────────

    def _lock_path(self, path: str) -> None:
        self._locked_paths.add(path)

    def unlock_paths(self, paths: list[str]) -> None:
        for p in paths:
            self._locked_paths.discard(p)

    def lock_paths(self, paths: list[str]) -> _PathLock:
        for p in paths:
            self._lock_path(p)
        return _PathLock(self, paths)

    def _assert_not_locked(self, path: str) -> None:
        if path in self._locked_paths:
            raise PermissionError(
                f"path {path!r} is locked by remote execution")

    # ── helpers ─────────────────────────────────────────────────────────────

    def _mount_prefix(self, path: str) -> str:
        norm = "/" + path.strip("/")
        for m in self._registry.mounts():
            if norm == m.prefix.rstrip("/") or norm.startswith(m.prefix):
                return m.prefix
        return "/"

    def _assert_writable(self, path: str) -> None:
        _, _, mode = self._registry.resolve(path)
        if mode == MountMode.READ:
            raise PermissionError(f"mount at {path!r} is read-only")

    def _record_transfer(self,
                         path: str,
                         op: str,
                         nbytes: int,
                         duration_ms: int,
                         cache_hit: bool | None = None) -> None:
        prefix = self._mount_prefix(path)
        if prefix not in self._transfer_stats:
            self._transfer_stats[prefix] = TransferStats()
        stats = self._transfer_stats[prefix]
        if op == "read":
            stats.bytes_read += nbytes
            stats.read_ops += 1
            stats.total_read_ms += duration_ms
            if cache_hit is True:
                stats.cache_hits += 1
            elif cache_hit is False:
                stats.cache_misses += 1
        elif op == "write":
            stats.bytes_written += nbytes
            stats.write_ops += 1
            stats.total_write_ms += duration_ms


TodoWrite

Rename ConsistencyPolicy.NONE → CACHED in types.py and all references

Renaming ConsistencyPolicy.NONE to CACHED

Make Mount.consistency always explicit (no None)

Making Mount.consistency always explicit

Move Workspace class to workspace/workspace.py

Rename test file test_init.py → test_workspace.py

Run tests and pre-commit to verify


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Rename ConsistencyPolicy.NONE → CACHED in types.py and all references

Renaming ConsistencyPolicy.NONE to CACHED

Make Mount.consistency always explicit (no None)

Making Mount.consistency always explicit

Move Workspace class to workspace/workspace.py

Rename test file test_init.py → test_workspace.py

Run tests and pre-commit to verify

Run tests and pre-commit to verify


Step 4: Rename the test file:


Bash

git mv tests/workspace/test_init.py tests/workspace/test_workspace.py

Rename test_init.py to test_workspace.py


TodoWrite

Rename ConsistencyPolicy.NONE → CACHED in types.py and all references

Renaming ConsistencyPolicy.NONE to CACHED

Make Mount.consistency always explicit (no None)

Making Mount.consistency always explicit

Move Workspace class to workspace/workspace.py

Rename test file test_init.py → test_workspace.py

Run tests and pre-commit to verify


Run tests and pre-commit to verify

Run tests and pre-commit to verify


Bash

Run tests to verify


FAILED tests/server/service/test_local.py::test_list_commands - ValueError: '...
FAILED tests/server/service/test_local.py::test_register_command - ValueError...
FAILED tests/server/service/test_local.py::test_io_write_read - ValueError: '...
FAILED tests/server/service/test_local.py::test_io_stat - ValueError: 'none' ...
FAILED tests/server/service/test_local.py::test_io_stat_missing - ValueError:...
FAILED tests/server/service/test_local.py::test_io_readdir - ValueError: 'non...
FAILED tests/server/service/test_local.py::test_io_mkdir_readdir - ValueError...
FAILED tests/server/service/test_local.py::test_io_unlink - ValueError: 'none...
FAILED tests/server/service/test_local.py::test_io_rename - ValueError: 'none...
FAILED tests/server/service/test_local.py::test_io_truncate - ValueError: 'no...
FAILED tests/server/service/test_local.py::test_io_fuse_cat - ValueError: 'no...
FAILED tests/server/service/test_local.py::test_list_fuse_ops - ValueError: '...
FAILED tests/server/service/test_local.py::test_register_fuse_op - ValueError...
FAILED tests/server/service/test_local.py::test_register_fuse_op_unknown - Va...
21 failed, 620 passed, 1 skipped, 2 warnings in 3.45s


The error is `'none' is not a valid ConsistencyPolicy` — there are stored/hardcoded `"none"` strings being passed to the enum. Let me check:


uv run pytest tests/server/service/test_local.py::test_create_workspace -x 2>&1 | tail -20

Get full error traceback


ERROR: not found: /Users/agent/strukto/mirage/tests/server/service/test_local.py::test_create_workspace
(no match in any of [<Module test_local.py>])

no tests ran in 0.04s


uv run pytest tests/server/service/test_local.py::test_list_commands -x 2>&1 | tail -30

Get full error traceback


        # (such as via super when trying to override __new__)
        if not cls._member_map_:
            raise TypeError("%r has no members defined" % cls)
        #
        # still not found -- try _missing_ hook
        try:
            exc = None
            result = cls._missing_(value)
        except Exception as e:
            exc = e
            result = None
        try:
            if isinstance(result, cls):
                return result
            elif (
                    Flag is not None and issubclass(cls, Flag)
                    and cls._boundary_ is EJECT and isinstance(result, int)
                ):
                return result
            else:
                ve_exc = ValueError("%r is not a valid %s" % (value, cls.__qualname__))
                if result is None and exc is None:
>                   raise ve_exc
E                   ValueError: 'none' is not a valid ConsistencyPolicy

../../.local/share/uv/python/cpython-3.12.10-macos-aarch64-none/lib/python3.12/enum.py:1170: ValueError
=========================== short test summary info ============================
FAILED tests/server/service/test_local.py::test_list_commands - ValueError: '...
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed in 0.08s


The test is constructing a `ConsistencyPolicy("none")` somewhere. Let me check the test:


No matches found


The test itself doesn't have "none" — the error comes from the service code. Let me check the `LocalService` constructor:


    60→            pass
    61→
    62→
    63→class LocalService(MirageService):
    64→
    65→    def __init__(self) -> None:
    66→        self._sessions: dict[str, WorkspaceSession] = {}
    67→        self._file_cache = FileCache()
    68→
    69→    def _get_session(self, ws_id: str) -> WorkspaceSession:
    70→        session = self._sessions.get(ws_id)
    71→        if session is None:
    72→            raise KeyError(f"workspace {ws_id!r} not found")
    73→        if session.fuse_mounted:
    74→            raise ValueError("not supported for fuse workspaces")
    75→        return session
    76→
    77→    def create_workspace(self, config: dict) -> str:
    78→        mode = config.get("mode", "write")
    79→        consistency = config.get("consistency", "none")
    80→        default_mode = MountMode(mode)
    81→        default_consistency = ConsistencyPolicy(consistency)
    82→        mounts = config.get("mounts", {})
    83→
    84→        backends: dict = {}
    85→        for prefix, value in mounts.items():
    86→            if isinstance(value, str):
    87→                backends[prefix] = parse_mount_uri(value)
    88→            else:
    89→                backend_spec = value.get("backend")

            pass

class LocalService(MirageService):

    def __init__(self) -> None:
        self._sessions: dict[str, WorkspaceSession] = {}
        self._file_cache = FileCache()

    def _get_session(self, ws_id: str) -> WorkspaceSession:
        session = self._sessions.get(ws_id)
        if session is None:
            raise KeyError(f"workspace {ws_id!r} not found")
        if session.fuse_mounted:
            raise ValueError("not supported for fuse workspaces")
        return session

    def create_workspace(self, config: dict) -> str:
        mode = config.get("mode", "write")
        consistency = config.get("consistency", "none")
        default_mode = MountMode(mode)
        default_consistency = ConsistencyPolicy(consistency)
        mounts = config.get("mounts", {})

        backends: dict = {}
        for prefix, value in mounts.items():
            if isinstance(value, str):
                backends[prefix] = parse_mount_uri(value)
            else:
                backend_spec = value.get("backend")


Line 79: `config.get("consistency", "none")` — the default is the string `"none"` which was the old enum value. Now it should be `"cached"`:


Edit

consistency = config.get("consistency", "none")

consistency = config.get("consistency", "cached")


The file /Users/agent/strukto/mirage/mirage/server/service/local.py has been updated successfully.

import os
import signal
import subprocess
import sys
import time
import uuid
from datetime import datetime, timezone

from pydantic import BaseModel, ConfigDict, Field

from mirage.backend.loader import load_backend_class
from mirage.cli.config import parse_mount_uri, split_mount_spec
from mirage.commands import COMMANDS, _resolve
from mirage.commands.builtin import nav
from mirage.commands.custom.registry import CommandRegistry, ExternalCommand
from mirage.fuse import FUSE_OPS
from mirage.fuse.loader import build_registry_from_config, load_fuse_op_class
from mirage.fuse.registry import FuseOpRegistry
from mirage.observe.entries import (CommandEntry, CommandResult, DispatchInfo,
                                    ParsedCommand, SessionEntry, WorkspaceInfo)
from mirage.observe.session_log import get_or_create_log
from mirage.runtime.base import _BLOCKED, CommandNotAllowedError
from mirage.server.service.base import MirageService
from mirage.server.service.cache import FileCache
from mirage.types import (CommandStats, ConsistencyPolicy, FileStat, MountMode,
                          RuntimeType, SyncPolicy)
from mirage.workspace import Workspace

class WorkspaceSession(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)

    id: str
    ws: Workspace | None
    registry: CommandRegistry | None
    cwd: str = "/"
    fuse_mounted: bool = False
    mode: MountMode = MountMode.WRITE
    mountpoint: str | None = None
    pid: int | None = None
    mounts: list[dict] = Field(default_factory=list)
    runtime_type: RuntimeType | None = None
    fuse_registry: FuseOpRegistry = Field(default_factory=FuseOpRegistry)
    command_stats: CommandStats = Field(default_factory=CommandStats)
    created_at: float = Field(default_factory=time.monotonic)
    reported_status: dict | None = None
    reported_transfers: dict | None = None

def _unmount_fuse(mountpoint: str, pid: int | None) -> None:
    if sys.platform == "darwin":
        subprocess.run(["diskutil", "unmount", "force", mountpoint],
                       capture_output=True)
    else:
        subprocess.run(["fusermount", "-u", mountpoint], capture_output=True)
    if pid:
        try:
            os.kill(pid, signal.SIGTERM)
        except ProcessLookupError:
            pass

class LocalService(MirageService):

    def __init__(self) -> None:
        self._sessions: dict[str, WorkspaceSession] = {}
        self._file_cache = FileCache()

    def _get_session(self, ws_id: str) -> WorkspaceSession:
        session = self._sessions.get(ws_id)
        if session is None:
            raise KeyError(f"workspace {ws_id!r} not found")
        if session.fuse_mounted:
            raise ValueError("not supported for fuse workspaces")
        return session

    def create_workspace(self, config: dict) -> str:
        mode = config.get("mode", "write")
        consistency = config.get("consistency", "none")
        default_mode = MountMode(mode)
        default_consistency = ConsistencyPolicy(consistency)
        mounts = config.get("mounts", {})

        backends: dict = {}
        for prefix, value in mounts.items():
            if isinstance(value, str):
                backends[prefix] = parse_mount_uri(value)
            else:
                backend_spec = value.get("backend")
                if backend_spec:
                    cls = load_backend_class(backend_spec)
                    backend = cls(**(value.get("args") or {}))
                else:
                    backend = parse_mount_uri(value.get("uri") or "")
                mount_mode = (MountMode(value["mode"])
                              if value.get("mode") else default_mode)
                mount_consistency = (ConsistencyPolicy(value["consistency"])
                                     if value.get("consistency")
                                     else default_consistency)
                if (mount_mode != default_mode
                        or mount_consistency != default_consistency):
                    backends[prefix] = (backend, mount_mode, mount_consistency)
                else:
                    backends[prefix] = backend

        ws = Workspace(
            backends,
            sync_policy=SyncPolicy(config.get("sync_policy", "staged")),
            mode=default_mode,
            consistency=default_consistency,
            ttl_seconds=config.get("ttl_seconds", 300),
        )

        mounts_info: list[dict] = []
        for prefix, value in mounts.items():
            if isinstance(value, str):
                uri = value
            else:
                uri = value.get("uri") or ""
                if not uri and value.get("backend"):
                    uri = f"{value['backend']}://"
            mounts_info.append({"prefix": prefix, "uri": uri})

        ws_id = str(uuid.uuid4())[:8]
        registry = CommandRegistry()
        fuse_ops_config = config.get("fuse_ops") or {}
        fuse_registry = build_registry_from_config(fuse_ops_config)
        runtime = config.get("runtime")
        rt_type = RuntimeType(runtime) if runtime else None

        self._sessions[ws_id] = WorkspaceSession(
            id=ws_id,
            ws=ws,
            registry=registry,
            mode=mode,
            mounts=mounts_info,
            runtime_type=rt_type,
            fuse_registry=fuse_registry,
        )

        log = get_or_create_log(ws_id)
        mount_info = {}
        for m in ws._registry.mounts():
            mount_info[m.prefix] = {
                "type": type(m.backend).__name__,
                "mode": m.mode.value,
            }
        session_entry = SessionEntry(
            timestamp=datetime.now(timezone.utc).isoformat(),
            workspace=WorkspaceInfo(
                mounts=mount_info,
                cache_limit=str(ws._cache.limit),
                runtime_type=rt_type.value if rt_type else None,
            ),
            commands=registry.list_commands(),
        )
        log.write_session(session_entry)
        return ws_id

    def delete_workspace(self, ws_id: str) -> None:
        session = self._sessions.get(ws_id)
        if session is None:
            raise KeyError(f"workspace {ws_id!r} not found")
        if session.fuse_mounted and session.mountpoint:
            _unmount_fuse(session.mountpoint, session.pid)
        self._file_cache.clear(ws_id)
        del self._sessions[ws_id]

    def list_workspaces(self) -> list[dict]:
        return [{
            "id": s.id,
            "fuse_mounted": s.fuse_mounted,
            "mode": s.mode,
            "mountpoint": s.mountpoint,
            "mounts": s.mounts,
            "runtime": s.runtime_type.value if s.runtime_type else None,
        } for s in self._sessions.values()]

    def exec(
        self,
        ws_id: str,
        command: str,
        cwd: str | None = None,
        stdin: bytes | None = None,
        input_files: dict[str, tuple[str, bytes | None]] | None = None,
    ) -> tuple[bool, str, dict[str, bytes] | None, dict[str, str] | None]:
        session = self._get_session(ws_id)
        if cwd is not None:
            session.cwd = cwd

        cache_status: dict[str, str] = {}
        if input_files:
            for path, (checksum, data) in input_files.items():
                hit, _ = self._file_cache.get(ws_id, path, checksum)
                if hit:
                    cache_status[path] = "hit"
                elif data is not None:
                    self._file_cache.put(ws_id, path, checksum, data)
                    session.ws.write(path, data)
                    cache_status[path] = "miss"
                else:
                    return (False, f"cache miss: {path}", None, None)

        cmd_name = command.strip().split()[0] if command.strip() else ""
        session.command_stats.total_commands += 1
        session.command_stats.command_counts[cmd_name] = (
            session.command_stats.command_counts.get(cmd_name, 0) + 1)
        session.command_stats.last_command = command
        session.command_stats.last_timestamp = datetime.now(
            timezone.utc).isoformat()
        t0 = time.monotonic()
        try:
            if session.runtime_type is not None and cmd_name in _BLOCKED:
                raise CommandNotAllowedError(
                    f"{cmd_name!r} is not allowed in this runtime")
            output = session.registry.run(session.ws,
                                          command,
                                          cwd=session.cwd,
                                          stdin=stdin)
            duration_ms = int((time.monotonic() - t0) * 1000)
            if command.strip().startswith("cd "):
                parts = command.strip().split(None, 1)
                if len(parts) > 1:
                    session.cwd = nav.cd(session.ws, session.cwd,
                                         _resolve(session.cwd, parts[1]))
            args = command.strip().split()[1:] if command.strip() else []
            source = "builtin" if cmd_name in COMMANDS else "external"
            log = get_or_create_log(ws_id)
            entry = CommandEntry(
                timestamp=datetime.now(timezone.utc).isoformat(),
                raw=command,
                parsed=ParsedCommand(
                    pipeline=[command],
                    args={cmd_name: args},
                ),
                dispatch=[
                    DispatchInfo(
                        command=cmd_name,
                        handler=f"_{cmd_name}"
                        if source == "builtin" else cmd_name,
                        source=source,
                    )
                ],
                result=CommandResult(
                    exit_code=0,
                    stdout_bytes=len(output.encode()) if output else 0,
                    duration_ms=duration_ms,
                ),
            )
            log.write_command(entry)

            return (True, output, None, cache_status or None)
        except Exception as e:
            duration_ms = int((time.monotonic() - t0) * 1000)
            session.command_stats.total_errors += 1
            log = get_or_create_log(ws_id)
            entry = CommandEntry(
                timestamp=datetime.now(timezone.utc).isoformat(),
                raw=command,
                result=CommandResult(exit_code=1,
                                     stdout_bytes=0,
                                     duration_ms=duration_ms),
            )
            log.write_command(entry)
            return (False, str(e), None, None)

    def list_commands(self, ws_id: str) -> list[str]:
        session = self._get_session(ws_id)
        return session.registry.list_commands()

    def register_command(self,
                         ws_id: str,
                         name: str,
                         exec_str: str,
                         description: str = "") -> None:
        session = self._get_session(ws_id)
        session.registry.register(
            ExternalCommand(name=name, exec=exec_str, description=description))

    def sync(self, ws_id: str) -> list[dict]:
        session = self._get_session(ws_id)
        results = session.ws.sync()
        return [{"path": r.path, "status": r.status} for r in results]

    def status(self, ws_id: str) -> list[dict]:
        session = self._get_session(ws_id)
        inodes = session.ws.status()
        return [{
            "path": i.virtual_path,
            "dirty": i.dirty,
            "size": i.size
        } for i in inodes]

    # ── File I/O (backend-level) ─────────────────────────────────────────

    def io_read(self, ws_id: str, path: str, offset: int, size: int) -> bytes:
        session = self._get_session(ws_id)
        backend, backend_path, _ = session.ws._registry.resolve(path)
        return backend.read(backend_path, offset, size)

    def io_write(self, ws_id: str, path: str, offset: int, data: bytes) -> int:
        session = self._get_session(ws_id)
        backend, backend_path, _ = session.ws._registry.resolve(path)
        return backend.write(backend_path, offset, data)

    def io_stat(self, ws_id: str, path: str) -> FileStat:
        session = self._get_session(ws_id)
        backend, backend_path, _ = session.ws._registry.resolve(path)
        return backend.stat(backend_path)

    def io_readdir(self,
                   ws_id: str,
                   path: str,
                   cursor: str | None = None) -> tuple[list[str], str | None]:
        session = self._get_session(ws_id)
        backend, backend_path, _ = session.ws._registry.resolve(path)
        return backend.readdir(backend_path, cursor)

    def io_create(self, ws_id: str, path: str) -> None:
        session = self._get_session(ws_id)
        backend, backend_path, _ = session.ws._registry.resolve(path)
        backend.create(backend_path)

    def io_unlink(self, ws_id: str, path: str) -> None:
        session = self._get_session(ws_id)
        backend, backend_path, _ = session.ws._registry.resolve(path)
        backend.unlink(backend_path)

    def io_mkdir(self, ws_id: str, path: str) -> None:
        session = self._get_session(ws_id)
        backend, backend_path, _ = session.ws._registry.resolve(path)
        backend.mkdir(backend_path)

    def io_rmdir(self, ws_id: str, path: str) -> None:
        session = self._get_session(ws_id)
        backend, backend_path, _ = session.ws._registry.resolve(path)
        backend.rmdir(backend_path)

    def io_rename(self, ws_id: str, src: str, dst: str) -> None:
        session = self._get_session(ws_id)
        backend, src_path, _ = session.ws._registry.resolve(src)
        _, dst_path, _ = session.ws._registry.resolve(dst)
        backend.rename(src_path, dst_path)

    def io_truncate(self, ws_id: str, path: str, size: int) -> None:
        session = self._get_session(ws_id)
        backend, backend_path, _ = session.ws._registry.resolve(path)
        backend.truncate(backend_path, size)

    def io_fuse_cat(self, ws_id: str, path: str) -> bytes:
        session = self._get_session(ws_id)
        st = session.ws.stat(path)
        op = session.fuse_registry.resolve(path, st.size or 0, session.ws)
        ctx = op.on_open(session.ws, path, st.size or 0)
        return op.read(session.ws, path, 0, st.size or 0, ctx)

    # ── FuseOp management ────────────────────────────────────────────────

    def list_fuse_ops(self, ws_id: str) -> list[dict]:
        session = self._get_session(ws_id)
        return session.fuse_registry.list_ops()

    def register_fuse_op(self, ws_id: str, pattern: str, handler: str) -> None:
        session = self._get_session(ws_id)
        op = FUSE_OPS.get(handler)
        if op is None:
            try:
                cls = load_fuse_op_class(handler)
                op = cls()
            except (ValueError, Exception) as e:
                raise ValueError(f"unknown handler: {handler} ({e})")
        session.fuse_registry.register(pattern, op)

    # ── FUSE-specific (not part of MirageService ABC) ────────────────────

    def register_fuse(self,
                      mountpoint: str,
                      pid: int,
                      mount_specs: list[str] | None = None,
                      runtime: str | None = None) -> str:
        fuse_mounts: list[dict] = []
        for spec in (mount_specs or []):
            try:
                uri, prefix = split_mount_spec(spec)
                fuse_mounts.append({"prefix": prefix, "uri": uri})
            except ValueError:
                pass
        ws_id = str(uuid.uuid4())[:8]
        rt_type = RuntimeType(runtime) if runtime else None
        self._sessions[ws_id] = WorkspaceSession(
            id=ws_id,
            ws=None,
            registry=None,
            fuse_mounted=True,
            mountpoint=mountpoint,
            pid=pid,
            mounts=fuse_mounts,
            runtime_type=rt_type,
        )
        mount_info = {m["prefix"]: {"type": m["uri"]} for m in fuse_mounts}
        log = get_or_create_log(ws_id)
        session_entry = SessionEntry(
            timestamp=datetime.now(timezone.utc).isoformat(),
            workspace=WorkspaceInfo(
                mounts=mount_info,
                runtime_type=rt_type.value if rt_type else None,
            ),
        )
        log.write_session(session_entry)
        return ws_id

    def report_fuse_stats(self,
                          mountpoint: str,
                          status: dict,
                          transfers: dict | None = None,
                          ops: list[dict] | None = None) -> bool:
        for s in self._sessions.values():
            if s.fuse_mounted and s.mountpoint == mountpoint:
                s.reported_status = status
                s.reported_transfers = transfers or {}
                if ops:
                    log = get_or_create_log(s.id)
                    for op in ops:
                        log._append(op)
                return True
        return False

-        consistency = config.get("consistency", "none")

+        consistency = config.get("consistency", "cached")


Let me also check for any other `"none"` string literals that might map to the old enum value:


Bash

Run tests again


mirage/workspace/sync.py                                   56      0   100%
mirage/workspace/workspace.py                             235     11    95%   87, 169, 216-217, 220-221, 269-272, 330
-------------------------------------------------------------------------------------
TOTAL                                                    4697   1193    75%
641 passed, 1 skipped, 2 warnings in 2.72s


All 641 tests pass. Now run pre-commit:


Run pre-commit hooks


Exit code 1
No commits to master.....................................................Failed
- hook id: no-commit-to-branch
- exit code: 1
End-of-file fixer........................................................Passed
mixed-line-ending........................................................Passed
Remove trailing whitespaces..............................................Passed
Check toml...............................................................Passed
Check yaml...............................................................Passed
Lint yaml................................................................Passed
Upgrade Python syntax....................................................Passed
Remove unused imports and variables......................................Passed
Format code..............................................................Failed
- hook id: yapf
- files were modified by this hook
Sort imports.............................................................Passed
Check PEP8...............................................................Passed
Ruff formatting..........................................................Passed
Format Markdown..........................................................Passed
Check Sphinx.........................................(no files to check)Skipped

No commits to master.....................................................Failed
- hook id: no-commit-to-branch
- exit code: 1
End-of-file fixer........................................................Passed
mixed-line-ending........................................................Passed
Remove trailing whitespaces..............................................Passed
Check toml...............................................................Passed
Check yaml...............................................................Passed
Lint yaml................................................................Passed
Upgrade Python syntax....................................................Passed
Remove unused imports and variables......................................Passed
Format code..............................................................Failed
- hook id: yapf
- files were modified by this hook
Sort imports.............................................................Passed
Check PEP8...............................................................Passed
Ruff formatting..........................................................Passed
Format Markdown..........................................................Passed
Check Sphinx.........................................(no files to check)Skipped


The yapf formatter modified some files (auto-formatting). Let me run it again to confirm it's clean:


Run pre-commit again after auto-formatting


Exit code 1
No commits to master.....................................................Failed
- hook id: no-commit-to-branch
- exit code: 1
End-of-file fixer........................................................Passed
mixed-line-ending........................................................Passed
Remove trailing whitespaces..............................................Passed
Check toml...............................................................Passed
Check yaml...............................................................Passed
Lint yaml................................................................Passed
Upgrade Python syntax....................................................Passed
Remove unused imports and variables......................................Passed
Format code..............................................................Passed
Sort imports.............................................................Passed
Check PEP8...............................................................Passed
Ruff formatting..........................................................Passed
Format Markdown..........................................................Passed
Check Sphinx.........................................(no files to check)Skipped

No commits to master.....................................................Failed
- hook id: no-commit-to-branch
- exit code: 1
End-of-file fixer........................................................Passed
mixed-line-ending........................................................Passed
Remove trailing whitespaces..............................................Passed
Check toml...............................................................Passed
Check yaml...............................................................Passed
Lint yaml................................................................Passed
Upgrade Python syntax....................................................Passed
Remove unused imports and variables......................................Passed
Format code..............................................................Passed
Sort imports.............................................................Passed
Check PEP8...............................................................Passed
Ruff formatting..........................................................Passed
Format Markdown..........................................................Passed
Check Sphinx.........................................(no files to check)Skipped


All clean (only `no-commit-to-branch` fails as expected). Let me verify tests still pass after formatter changes:


Final test verification


-------------------------------------------------------------------------------------
TOTAL                                                    4697   1193    75%
641 passed, 1 skipped, 2 warnings in 2.76s


TodoWrite

Rename ConsistencyPolicy.NONE → CACHED in types.py and all references

Renaming ConsistencyPolicy.NONE to CACHED

Make Mount.consistency always explicit (no None)

Making Mount.consistency always explicit

Move Workspace class to workspace/workspace.py

Rename test file test_init.py → test_workspace.py

Run tests and pre-commit to verify


Run tests and pre-commit to verify

Run tests and pre-commit to verify


All done. 641 tests pass, all lint checks clean.

Summary of changes:

1. **`ConsistencyPolicy.NONE` → `ConsistencyPolicy.CACHED`** — renamed in `mirage/types.py` and updated across 12 files (source + tests). Also updated CLI suffix strings from `#none` to `#cached` and the default string literal `"none"` → `"cached"` in `server/service/local.py`.

2. **`Mount.consistency` always explicit** — changed from `ConsistencyPolicy | None = None` to `ConsistencyPolicy = ConsistencyPolicy.CACHED` in `mount.py`. Removed `_effective_consistency()` from Workspace — mounts now always carry their own concrete policy. The workspace constructor passes the workspace-level `consistency` to mounts that don't specify one via tuple.

3. **Moved Workspace to `workspace/workspace.py`** — `workspace/__init__.py` is now just:
   ```python
   from mirage.workspace.workspace import Workspace
   __all__ = ["Workspace"]
   ```

4. **Renamed test** — `tests/workspace/test_init.py` → `tests/workspace/test_workspace.py`


Very thoroughly explore the workspace module. I need to understand:

1. Read `mirage/workspace/__init__.py` completely - what classes, functions, and logic does it contain?
2. Read `mirage/workspace/mount.py`, `mirage/workspace/sync.py` completely
3. Read `mirage/workspace/cache/` files (entry.py, policy.py, store.py)
4. Find all imports of `from mirage.workspace import ...` or `from mirage.workspace` across the entire codebase to understand what's used from __init__.py
5. Read `mirage/types.py` to understand ConsistencyPolicy and its usage

I need to understand:
- What's in workspace/__init__.py that should be extracted
- What ConsistencyPolicy values exist and how they're used
- What the Workspace class contains and how to split it
- All external consumers of the workspace module

Prompt:
Very thoroughly explore the workspace module. I need to understand:

1. Read `mirage/workspace/__init__.py` completely - what classes, functions, and logic does it contain?
2. Read `mirage/workspace/mount.py`, `mirage/workspace/sync.py` completely
3. Read `mirage/workspace/cache/` files (entry.py, policy.py, store.py)
4. Find all imports of `from mirage.workspace import ...` or `from mirage.workspace` across the entire codebase to understand what's used from __init__.py
5. Read `mirage/types.py` to understand ConsistencyPolicy and its usage

I need to understand:
- What's in workspace/__init__.py that should be extracted
- What ConsistencyPolicy values exist and how they're used
- What the Workspace class contains and how to split it
- All external consumers of the workspace module


     1→from __future__ import annotations
     2→
     3→import time
     4→
     5→from mirage.backend.base import BaseBackend
     6→from mirage.backend.filetype import guess_type
     7→from mirage.types import (ConsistencyPolicy, FileStat, Inode, MountMode,
     8→                          SyncPolicy, SyncResult, TransferStats)
     9→from mirage.workspace.cache import LRUCache
    10→from mirage.workspace.cache.policy import is_fresh
    11→from mirage.workspace.mount import MountRegistry
    12→from mirage.workspace.sync import DirtyTracker
    13→
    14→
    15→class _PathLock:
    16→
    17→    def __init__(self, ws: Workspace, paths: list[str]) -> None:
    18→        self._ws = ws
    19→        self._paths = paths
    20→
    21→    def __enter__(self) -> _PathLock:
    22→        return self
    23→
    24→    def __exit__(self, *_: object) -> None:
    25→        self._ws.unlock_paths(self._paths)
    26→
    27→
    28→class Workspace:
    29→    """
    30→    Unified virtual filesystem over heterogeneous backends.
    31→
    32→    Mounts backends at virtual path prefixes and provides a command-level API
    33→    (cat, ls, write, cp, mv, rm, ...) with transparent caching
    34→    and write staging.
    35→
    36→    Example::
    37→
    38→        ws = Workspace(
    39→            {"/data/": S3Backend(S3Config(bucket="my-bucket")),
    40→             "/tmp/":  MemoryBackend()},
    41→            cache_limit="256MB",
    42→            sync_policy=SyncPolicy.STAGED,
    43→        )
    44→        content = ws.cat("/data/reports/q1.json")
    45→        ws.write("/tmp/summary.txt", b"hello")
    46→        ws.sync()
    47→    """
    48→
    49→    def __init__(
    50→        self,
    51→        backends: dict[str, BaseBackend | tuple],
    52→        cache_limit: str | int = "512MB",
    53→        sync_policy: SyncPolicy = SyncPolicy.STAGED,
    54→        mode: MountMode = MountMode.READ,
    55→        consistency: ConsistencyPolicy = ConsistencyPolicy.NONE,
    56→        ttl_seconds: int = 300,
    57→    ) -> None:
    58→        self._registry = MountRegistry()
    59→        self._cache = LRUCache(cache_limit)
    60→        self._tracker = DirtyTracker()
    61→        self._sync_policy = sync_policy
    62→        self._consistency = consistency
    63→        self._ttl_seconds = ttl_seconds
    64→        self._locked_paths: set[str] = set()
    65→        self._transfer_stats: dict[str, TransferStats] = {}
    66→        self._created_at: float = time.monotonic()
    67→
    68→        for prefix, value in backends.items():
    69→            if isinstance(value, tuple) and len(value) == 3:
    70→                backend, mount_mode, mount_consistency = value
    71→            elif isinstance(value, tuple):
    72→                backend, mount_mode = value
    73→                mount_consistency = None
    74→            else:
    75→                backend = value
    76→                mount_mode = mode
    77→                mount_consistency = None
    78→            self._registry.mount(prefix, backend, mount_mode,
    79→                                 mount_consistency)
    80→
    81→    # ── mount management ────────────────────────────────────────────────────
    82→
    83→    def mount(self,
    84→              prefix: str,
    85→              backend: BaseBackend,
    86→              mode: MountMode = MountMode.READ,
    87→              consistency: ConsistencyPolicy | None = None) -> None:
    88→        self._registry.mount(prefix, backend, mode, consistency)
    89→
    90→    def backend(self, path: str) -> BaseBackend:
    91→        backend, _, _ = self._registry.resolve(path)
    92→        return backend
    93→
    94→    # ── read ops ────────────────────────────────────────────────────────────
    95→
    96→    def cat(self, path: str) -> bytes:
    97→        inode = self._tracker.get(path)
    98→        if inode and inode.dirty:
    99→            entry = self._cache.get(path)
   100→            if entry is not None:
   101→                self._record_transfer(path,
   102→                                      "read",
   103→                                      len(entry.data),
   104→                                      0,
   105→                                      cache_hit=True)
   106→                return entry.data
   107→
   108→        entry = self._cache.get(path)
   109→        remote_stat = None
   110→        if entry is not None:
   111→            consistency = self._effective_consistency(path)
   112→            backend, bp, _ = self._registry.resolve(path)
   113→            fresh, remote_stat = is_fresh(entry, consistency,
   114→                                          self._ttl_seconds, backend, bp)
   115→            if fresh:
   116→                self._record_transfer(path,
   117→                                      "read",
   118→                                      len(entry.data),
   119→                                      0,
   120→                                      cache_hit=True)
   121→                return entry.data
   122→        else:
   123→            backend, bp, _ = self._registry.resolve(path)
   124→
   125→        t0 = time.monotonic()
   126→        data = backend.cat(bp)
   127→        duration_ms = int((time.monotonic() - t0) * 1000)
   128→        self._record_transfer(path,
   129→                              "read",
   130→                              len(data),
   131→                              duration_ms,
   132→                              cache_hit=False)
   133→        if remote_stat is None:
   134→            remote_stat = backend.stat(bp)
   135→        self._cache.put(path,
   136→                        data,
   137→                        etag=remote_stat.extra.get("etag"),
   138→                        modified=remote_stat.modified,
   139→                        size=remote_stat.size)
   140→        self._tracker.on_read(path, data, self._mount_prefix(path))
   141→        return data
   142→
   143→    def ls(self, path: str) -> list[str]:
   144→        backend, bp, _ = self._registry.resolve(path)
   145→        prefix = self._mount_prefix(path).rstrip("/")
   146→        backend_ok = False
   147→        try:
   148→            backend_entries = backend.ls(bp)
   149→            backend_ok = True
   150→        except FileNotFoundError:
   151→            backend_entries = []
   152→        result: set[str] = {
   153→            prefix + "/" + e.lstrip("/")
   154→            for e in backend_entries
   155→        }
   156→        norm_path = "/" + path.strip("/")
   157→        dir_prefix = norm_path.rstrip("/") + "/"
   158→        has_staged_children = False
   159→        for inode in self._tracker.dirty():
   160→            vp = inode.virtual_path
   161→            if not vp.startswith(dir_prefix):
   162→                continue
   163→            if inode.checksum is None:
   164→                result.discard(vp)
   165→                continue
   166→            has_staged_children = True
   167→            remainder = vp[len(dir_prefix):]
   168→            first = remainder.split("/")[0]
   169→            if not first:
   170→                continue
   171→            if "/" in remainder:
   172→                result.add(dir_prefix + first + "/")
   173→            else:
   174→                result.add(dir_prefix + first)
   175→        if not backend_ok and not has_staged_children:
   176→            raise FileNotFoundError(path)
   177→        return sorted(result)
   178→
   179→    def ls_l(self, path: str) -> list[FileStat]:
   180→        backend, bp, _ = self._registry.resolve(path)
   181→        return backend.ls_l(bp)
   182→
   183→    def stat(self, path: str) -> FileStat:
   184→        # For staged writes, build FileStat from the tracker rather than
   185→        # hitting the backend (which hasn't received the write yet).
   186→        inode = self._tracker.get(path)
   187→        if inode and inode.dirty and inode.checksum is not None:
   188→            entry = self._cache.get(path)
   189→            size = len(entry.data) if entry is not None else inode.size
   190→            return FileStat(
   191→                name=path.rstrip("/").rsplit("/", 1)[-1],
   192→                size=size,
   193→                type=guess_type(path),
   194→            )
   195→        backend, bp, _ = self._registry.resolve(path)
   196→        return backend.stat(bp)
   197→
   198→    def exists(self, path: str) -> bool:
   199→        try:
   200→            self.stat(path)
   201→            return True
   202→        except (FileNotFoundError, ValueError):
   203→            return False
   204→
   205→    def head(self, path: str, lines: int = 10) -> bytes:
   206→        backend, bp, _ = self._registry.resolve(path)
   207→        return backend.head(bp, lines)
   208→
   209→    def tail(self, path: str, lines: int = 10) -> bytes:
   210→        backend, bp, _ = self._registry.resolve(path)
   211→        return backend.tail(bp, lines)
   212→
   213→    def read(self, path: str, offset: int, size: int) -> bytes:
   214→        inode = self._tracker.get(path)
   215→        if inode and inode.dirty:
   216→            entry = self._cache.get(path)
   217→            if entry is not None:
   218→                return entry.data[offset:offset + size]
   219→        backend, bp, _ = self._registry.resolve(path)
   220→        return backend.read(bp, offset, size)
   221→
   222→    def grep(self, pattern: str, path: str) -> list[str]:
   223→        backend, bp, _ = self._registry.resolve(path)
   224→        return backend.grep(pattern, bp)
   225→
   226→    # ── write ops ───────────────────────────────────────────────────────────
   227→
   228→    def write(self, path: str, data: bytes) -> None:
   229→        self._assert_not_locked(path)
   230→        self._assert_writable(path)
   231→        backend, bp, _ = self._registry.resolve(path)
   232→        self._cache.put(path, data)
   233→        self._tracker.on_write(path, data, self._mount_prefix(path))
   234→        self._record_transfer(path, "write", len(data), 0)
   235→        if self._sync_policy == SyncPolicy.IMMEDIATE:
   236→            backend.put(bp, data)
   237→            self._tracker.on_read(path, data, self._mount_prefix(path))
   238→
   239→    def mkdir(self, path: str) -> None:
   240→        self._assert_writable(path)
   241→        backend, bp, _ = self._registry.resolve(path)
   242→        backend.mkdir(bp)
   243→
   244→    def rm(self, path: str) -> None:
   245→        self._assert_writable(path)
   246→        backend, bp, _ = self._registry.resolve(path)
   247→        inode = self._tracker.get(path)
   248→        staged_only = inode and inode.dirty and inode.remote_checksum is None
   249→        if not staged_only:
   250→            backend.unlink(bp)
   251→        self._cache.invalidate(path)
   252→        self._tracker.on_delete(path)
   253→
   254→    def mv(self, src: str, dst: str) -> None:
   255→        self._assert_writable(src)
   256→        sb, sbp, _ = self._registry.resolve(src)
   257→        db, dbp, _ = self._registry.resolve(dst)
   258→        src_inode = self._tracker.get(src)
   259→        staged_only = (src_inode and src_inode.dirty
   260→                       and src_inode.remote_checksum is None)
   261→        if staged_only:
   262→            entry = self._cache.get(src)
   263→            data = entry.data if entry else b""
   264→            self._cache.put(dst, data)
   265→            self._tracker.on_write(dst, data, self._mount_prefix(dst))
   266→            self._cache.invalidate(src)
   267→            self._tracker.on_delete(src)
   268→        elif sb is db:
   269→            sb.rename(sbp, dbp)
   270→            self._cache.invalidate(src)
   271→        else:
   272→            data = sb.cat(sbp)
   273→            db.put(dbp, data)
   274→            sb.unlink(sbp)
   275→            self._cache.invalidate(src)
   276→
   277→    def cp(self, src: str, dst: str) -> None:
   278→        self._assert_writable(dst)
   279→        data = self.cat(src)
   280→        db, dbp, _ = self._registry.resolve(dst)
   281→        db.put(dbp, data)
   282→
   283→    # ── sync lifecycle ──────────────────────────────────────────────────────
   284→
   285→    def sync(self, path: str | None = None) -> list[SyncResult]:
   286→        results: list[SyncResult] = []
   287→        targets = [
   288→            i for i in self._tracker.dirty()
   289→            if path is None or i.virtual_path == path
   290→        ]
   291→        for inode in targets:
   292→            backend, bp, _ = self._registry.resolve(inode.virtual_path)
   293→            entry = self._cache.get(inode.virtual_path)
   294→            data = entry.data if entry else None
   295→            result = self._tracker.on_sync(inode.virtual_path, bp, data,
   296→                                           backend)
   297→            results.append(result)
   298→        return results
   299→
   300→    def status(self) -> list[Inode]:
   301→        return self._tracker.status()
   302→
   303→    def reset(self) -> None:
   304→        self._tracker.reset()
   305→        self._cache.clear()
   306→
   307→    # ── path locking ─────────────────────────────────────────────────────────
   308→
   309→    def _lock_path(self, path: str) -> None:
   310→        self._locked_paths.add(path)
   311→
   312→    def unlock_paths(self, paths: list[str]) -> None:
   313→        for p in paths:
   314→            self._locked_paths.discard(p)
   315→
   316→    def lock_paths(self, paths: list[str]) -> _PathLock:
   317→        for p in paths:
   318→            self._lock_path(p)
   319→        return _PathLock(self, paths)
   320→
   321→    def _assert_not_locked(self, path: str) -> None:
   322→        if path in self._locked_paths:
   323→            raise PermissionError(
   324→                f"path {path!r} is locked by remote execution")
   325→
   326→    # ── helpers ─────────────────────────────────────────────────────────────
   327→
   328→    def _mount_prefix(self, path: str) -> str:
   329→        norm = "/" + path.strip("/")
   330→        for m in self._registry.mounts():
   331→            if norm == m.prefix.rstrip("/") or norm.startswith(m.prefix):
   332→                return m.prefix
   333→        return "/"
   334→
   335→    def _effective_consistency(self, path: str) -> ConsistencyPolicy:
   336→        mount = self._registry.mount_for(path)
   337→        if mount.consistency is not None:
   338→            return mount.consistency
   339→        return self._consistency
   340→
   341→    def _assert_writable(self, path: str) -> None:
   342→        _, _, mode = self._registry.resolve(path)
   343→        if mode == MountMode.READ:
   344→            raise PermissionError(f"mount at {path!r} is read-only")
   345→
   346→    def _record_transfer(self,
   347→                         path: str,
   348→                         op: str,
   349→                         nbytes: int,
   350→                         duration_ms: int,
   351→                         cache_hit: bool | None = None) -> None:
   352→        prefix = self._mount_prefix(path)
   353→        if prefix not in self._transfer_stats:
   354→            self._transfer_stats[prefix] = TransferStats()
   355→        stats = self._transfer_stats[prefix]
   356→        if op == "read":
   357→            stats.bytes_read += nbytes
   358→            stats.read_ops += 1
   359→            stats.total_read_ms += duration_ms
   360→            if cache_hit is True:
   361→                stats.cache_hits += 1
   362→            elif cache_hit is False:
   363→                stats.cache_misses += 1
   364→        elif op == "write":
   365→            stats.bytes_written += nbytes
   366→            stats.write_ops += 1
   367→            stats.total_write_ms += duration_ms
   368→


     1→from mirage.backend.base import BaseBackend
     2→from mirage.types import ConsistencyPolicy, MountMode
     3→
     4→
     5→class Mount:
     6→
     7→    def __init__(self,
     8→                 prefix: str,
     9→                 backend: BaseBackend,
    10→                 mode: MountMode = MountMode.READ,
    11→                 consistency: ConsistencyPolicy | None = None) -> None:
    12→        stripped = prefix.strip("/")
    13→        self.prefix = "/" + stripped + "/" if stripped else "/"
    14→        self.backend = backend
    15→        self.mode = mode
    16→        self.consistency = consistency
    17→
    18→
    19→class MountRegistry:
    20→    """
    21→    Longest-prefix-match router.
    22→
    23→    Given a virtual path like "/s3-prod/data/file.json", resolves to the
    24→    backend mounted at "/s3-prod/" and returns the stripped path
    25→    "/data/file.json".
    26→    """
    27→
    28→    def __init__(self) -> None:
    29→        self._mounts: list[Mount] = []
    30→
    31→    def mount(self,
    32→              prefix: str,
    33→              backend: BaseBackend,
    34→              mode: MountMode = MountMode.READ,
    35→              consistency: ConsistencyPolicy | None = None) -> None:
    36→        m = Mount(prefix, backend, mode, consistency)
    37→        for existing in self._mounts:
    38→            if existing.prefix == m.prefix:
    39→                raise ValueError(f"duplicate mount prefix: {m.prefix!r}")
    40→        self._mounts.append(m)
    41→        self._mounts.sort(key=lambda x: len(x.prefix), reverse=True)
    42→
    43→    def resolve(self, path: str) -> tuple[BaseBackend, str, MountMode]:
    44→        """
    45→        Returns (backend, backend_path, mode).
    46→
    47→        backend_path has the mount prefix stripped.
    48→        Raises ValueError if no mount matches.
    49→        """
    50→        norm = "/" + path.strip("/")
    51→        # ensure trailing slash when checking prefix match for directories
    52→        for m in self._mounts:
    53→            if norm == m.prefix.rstrip("/") or norm.startswith(m.prefix):
    54→                backend_path = "/" + norm[len(m.prefix):]
    55→                return m.backend, backend_path, m.mode
    56→        raise ValueError(f"no mount matches path: {path!r}")
    57→
    58→    def mount_for(self, path: str) -> Mount:
    59→        norm = "/" + path.strip("/")
    60→        for m in self._mounts:
    61→            if norm == m.prefix.rstrip("/") or norm.startswith(m.prefix):
    62→                return m
    63→        raise ValueError(f"no mount matches path: {path!r}")
    64→
    65→    def mounts(self) -> list[Mount]:
    66→        return list(self._mounts)
    67→


     1→import hashlib
     2→import time
     3→
     4→from mirage.backend.base import BaseBackend
     5→from mirage.types import Inode, SyncResult
     6→
     7→
     8→class DirtyTracker:
     9→    """
    10→    Tracks which virtual paths have local changes that haven't been synced
    11→    to their backing backend.
    12→
    13→    Lifecycle:
    14→        on_read()  — record baseline remote checksum (not dirty)
    15→        on_write() — update local checksum, mark dirty=True
    16→        on_sync()  — upload to backend, clear dirty flag
    17→    """
    18→
    19→    def __init__(self) -> None:
    20→        self._inodes: dict[str, Inode] = {}
    21→
    22→    # ── event hooks ─────────────────────────────────────────────────────────
    23→
    24→    def on_read(self, path: str, data: bytes, mount: str) -> None:
    25→        checksum = _md5(data)
    26→        if path in self._inodes:
    27→            inode = self._inodes[path]
    28→            inode.remote_checksum = checksum
    29→            inode.checksum = checksum
    30→            inode.size = len(data)
    31→            inode.dirty = False
    32→        else:
    33→            self._inodes[path] = Inode(
    34→                virtual_path=path,
    35→                backend_mount=mount,
    36→                checksum=checksum,
    37→                size=len(data),
    38→                remote_checksum=checksum,
    39→                dirty=False,
    40→            )
    41→
    42→    def on_write(self, path: str, data: bytes, mount: str) -> None:
    43→        checksum = _md5(data)
    44→        if path in self._inodes:
    45→            inode = self._inodes[path]
    46→            inode.checksum = checksum
    47→            inode.size = len(data)
    48→            inode.dirty = True
    49→        else:
    50→            self._inodes[path] = Inode(
    51→                virtual_path=path,
    52→                backend_mount=mount,
    53→                checksum=checksum,
    54→                size=len(data),
    55→                remote_checksum=None,
    56→                dirty=True,
    57→            )
    58→
    59→    def on_delete(self, path: str) -> None:
    60→        if path in self._inodes:
    61→            self._inodes[path].dirty = True
    62→            self._inodes[path].checksum = None
    63→            self._inodes[path].size = None
    64→
    65→    def on_sync(self, virtual_path: str, backend_path: str, data: bytes | None,
    66→                backend: BaseBackend) -> SyncResult:
    67→        """
    68→        Upload `data` to backend at `backend_path`. Pass None for deletions.
    69→        `virtual_path` tracks the inode; `backend_path` goes to the backend.
    70→        Returns a SyncResult describing the outcome.
    71→        """
    72→        inode = self._inodes.get(virtual_path)
    73→        op = "delete" if data is None else ("create" if (
    74→            inode and inode.remote_checksum is None) else "modify")
    75→        t0 = time.monotonic()
    76→        try:
    77→            if data is None:
    78→                backend.unlink(backend_path)
    79→            else:
    80→                backend.put(backend_path, data)
    81→            duration_ms = int((time.monotonic() - t0) * 1000)
    82→            checksum = _md5(data) if data is not None else None
    83→            if inode:
    84→                inode.remote_checksum = checksum
    85→                inode.dirty = False
    86→            return SyncResult(path=virtual_path,
    87→                              op=op,
    88→                              status="ok",
    89→                              bytes=len(data) if data else None,
    90→                              duration_ms=duration_ms)
    91→        except Exception as exc:
    92→            duration_ms = int((time.monotonic() - t0) * 1000)
    93→            return SyncResult(path=virtual_path,
    94→                              op=op,
    95→                              status="error",
    96→                              duration_ms=duration_ms,
    97→                              error=str(exc))
    98→
    99→    # ── queries ─────────────────────────────────────────────────────────────
   100→
   101→    def dirty(self) -> list[Inode]:
   102→        return [i for i in self._inodes.values() if i.dirty]
   103→
   104→    def status(self) -> list[Inode]:
   105→        return list(self._inodes.values())
   106→
   107→    def get(self, path: str) -> Inode | None:
   108→        return self._inodes.get(path)
   109→
   110→    def reset(self) -> None:
   111→        self._inodes.clear()
   112→
   113→
   114→def _md5(data: bytes) -> str:
   115→    return hashlib.md5(data).hexdigest()
   116→


     1→from enum import Enum
     2→
     3→from pydantic import BaseModel, ConfigDict, Field
     4→
     5→
     6→class FileType(str, Enum):
     7→    DIRECTORY = "directory"
     8→    TEXT = "text"
     9→    BINARY = "binary"
    10→    JSON = "json"
    11→    CSV = "csv"
    12→    IMAGE_PNG = "image/png"
    13→    IMAGE_JPEG = "image/jpeg"
    14→    IMAGE_GIF = "image/gif"
    15→    ZIP = "application/zip"
    16→    GZIP = "application/gzip"
    17→    PDF = "application/pdf"
    18→
    19→
    20→class CostLevel(str, Enum):
    21→    CHEAP = "cheap"
    22→    PARTIAL = "partial"
    23→    EXPENSIVE = "expensive"
    24→    NETWORK = "network"
    25→
    26→
    27→class FileStat(BaseModel):
    28→    model_config = ConfigDict(frozen=True)
    29→
    30→    name: str
    31→    size: int | None = None
    32→    modified: str | None = None
    33→    type: FileType | None = None
    34→    extra: dict = Field(default_factory=dict)
    35→
    36→
    37→class SyncPolicy(str, Enum):
    38→    STAGED = "staged"
    39→    IMMEDIATE = "immediate"
    40→    MANUAL = "manual"
    41→
    42→
    43→class MountMode(str, Enum):
    44→    READ = "read"
    45→    WRITE = "write"
    46→    EXEC = "exec"
    47→
    48→
    49→class ConsistencyPolicy(str, Enum):
    50→    NONE = "none"
    51→    LAZY = "lazy"
    52→    TTL = "ttl"
    53→    ALWAYS = "always"
    54→
    55→
    56→class RuntimeType(str, Enum):
    57→    MEMORY = "memory"
    58→    LOCAL = "local"
    59→    REMOTE = "remote"
    60→
    61→
    62→class BackendType(str, Enum):
    63→    MEMORY = "memory"
    64→    LOCAL = "local"
    65→    S3 = "s3"
    66→    R2 = "r2"
    67→    HTTP = "http"
    68→    HTTPS = "https"
    69→
    70→
    71→class Inode(BaseModel):
    72→    virtual_path: str
    73→    backend_mount: str
    74→    checksum: str | None = None
    75→    size: int | None = None
    76→    remote_checksum: str | None = None
    77→    dirty: bool = False
    78→
    79→
    80→class SyncResult(BaseModel):
    81→    model_config = ConfigDict(frozen=True)
    82→
    83→    path: str
    84→    op: str
    85→    status: str
    86→    remote: str | None = None
    87→    bytes: int | None = None
    88→    duration_ms: int = 0
    89→    error: str | None = None
    90→
    91→
    92→class TransferStats(BaseModel):
    93→    bytes_read: int = 0
    94→    bytes_written: int = 0
    95→    read_ops: int = 0
    96→    write_ops: int = 0
    97→    cache_hits: int = 0
    98→    cache_misses: int = 0
    99→    total_read_ms: int = 0
   100→    total_write_ms: int = 0
   101→
   102→
   103→class CommandStats(BaseModel):
   104→    total_commands: int = 0
   105→    total_errors: int = 0
   106→    last_command: str = ""
   107→    last_timestamp: str = ""
   108→    command_counts: dict[str, int] = Field(default_factory=dict)
   109→
   110→
   111→class FilePayload(BaseModel):
   112→    checksum: str
   113→    data: bytes | None = None
   114→
   115→
   116→class RemoteCommandOptions(BaseModel):
   117→    max_file_size: str = "100MB"
   118→    cache: str = "enabled"
   119→
   120→
   121→class CommandResult(BaseModel):
   122→    model_config = ConfigDict(arbitrary_types_allowed=True)
   123→
   124→    stdout: str = ""
   125→    output_files: dict[str, bytes] = {}
   126→


     1→from pydantic import BaseModel
     2→
     3→
     4→class CacheEntry(BaseModel):
     5→    data: bytes
     6→    cached_at: float
     7→    etag: str | None = None
     8→    modified: str | None = None
     9→    size: int | None = None
    10→


     1→import time
     2→
     3→from mirage.backend.base import BaseBackend
     4→from mirage.types import ConsistencyPolicy, FileStat
     5→from mirage.workspace.cache.entry import CacheEntry
     6→
     7→
     8→def is_fresh(
     9→    entry: CacheEntry,
    10→    consistency: ConsistencyPolicy,
    11→    ttl_seconds: int,
    12→    backend: BaseBackend,
    13→    backend_path: str,
    14→) -> tuple[bool, FileStat | None]:
    15→    if consistency == ConsistencyPolicy.NONE:
    16→        return True, None
    17→    if consistency == ConsistencyPolicy.ALWAYS:
    18→        return False, None
    19→    if consistency == ConsistencyPolicy.TTL:
    20→        if (time.monotonic() - entry.cached_at) < ttl_seconds:
    21→            return True, None
    22→    try:
    23→        remote = backend.stat(backend_path)
    24→    except FileNotFoundError:
    25→        return False, None
    26→    return metadata_matches(entry, remote), remote
    27→
    28→
    29→def metadata_matches(entry: CacheEntry, remote: FileStat) -> bool:
    30→    remote_etag = remote.extra.get("etag")
    31→    if entry.etag and remote_etag:
    32→        return entry.etag == remote_etag
    33→    if entry.modified and remote.modified:
    34→        return entry.modified == remote.modified
    35→    if entry.size is not None and remote.size is not None:
    36→        return entry.size == remote.size
    37→    return False
    38→