    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")
    90→                if backend_spec:
    91→                    cls = load_backend_class(backend_spec)
    92→                    backend = cls(**(value.get("args") or {}))
    93→                else:
    94→                    backend = parse_mount_uri(value.get("uri") or "")
    95→                mount_mode = (MountMode(value["mode"])
    96→                              if value.get("mode") else default_mode)
    97→                mount_consistency = (ConsistencyPolicy(value["consistency"])
    98→                                     if value.get("consistency") else None)
    99→                has_override = (mount_mode != default_mode
   100→                                or mount_consistency is not None)
   101→                if has_override and mount_consistency is not None:
   102→                    backends[prefix] = (backend, mount_mode, mount_consistency)
   103→                elif has_override:
   104→                    backends[prefix] = (backend, mount_mode)
   105→                else:
   106→                    backends[prefix] = backend
   107→
   108→        ws = Workspace(
   109→            backends,
   110→            sync_policy=SyncPolicy(config.get("sync_policy", "staged")),
   111→            mode=default_mode,
   112→            consistency=default_consistency,
   113→            ttl_seconds=config.get("ttl_seconds", 300),
   114→        )

        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 None)
                has_override = (mount_mode != default_mode
                                or mount_consistency is not None)
                if has_override and mount_consistency is not None:
                    backends[prefix] = (backend, mount_mode, mount_consistency)
                elif has_override:
                    backends[prefix] = (backend, mount_mode)
                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),
        )


Now that consistency is always explicit, simplify this logic — when consistency is specified, include it in the tuple; otherwise the workspace default applies (handled by `Workspace.__init__`):


Edit

                mount_mode = (MountMode(value["mode"])
                              if value.get("mode") else default_mode)
                mount_consistency = (ConsistencyPolicy(value["consistency"])
                                     if value.get("consistency") else None)
                has_override = (mount_mode != default_mode
                                or mount_consistency is not None)
                if has_override and mount_consistency is not None:
                    backends[prefix] = (backend, mount_mode, mount_consistency)
                elif has_override:
                    backends[prefix] = (backend, mount_mode)
                else:
                    backends[prefix] = backend

                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


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

                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

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 None)
                has_override = (mount_mode != default_mode
                                or mount_consistency is not None)
                if has_override and mount_consistency is not None:
                    backends[prefix] = (backend, mount_mode, mount_consistency)
                elif has_override:
                    backends[prefix] = (backend, mount_mode)
                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

-                                     if value.get("consistency") else None)

-                has_override = (mount_mode != default_mode

-                                or mount_consistency is not None)

-                if has_override and mount_consistency is not None:

+                                     if value.get("consistency")

+                                     else default_consistency)

+                if (mount_mode != default_mode

+                        or mount_consistency != default_consistency):

-                    backends[prefix] = (backend, mount_mode)


Now update `server/fastapi/config.py` — make `MountConfig.consistency` non-optional:


consistency: ConsistencyPolicy | None = None

consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED


<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.


     6→class MountConfig(BaseModel):
     7→    uri: str | None = None
     8→    backend: str | None = None
     9→    args: dict[str, str] | None = None
    10→    mode: MountMode | None = None
    11→    consistency: ConsistencyPolicy | None = None

class MountConfig(BaseModel):
    uri: str | None = None
    backend: str | None = None
    args: dict[str, str] | None = None
    mode: MountMode | None = None
    consistency: ConsistencyPolicy | None = None


Wait, `MountConfig` is a Pydantic model for API requests. The `consistency` field being `| None` there serves a different purpose — it's an optional field in the JSON request body. If the user doesn't specify consistency in the API request, it should default to... the workspace default. This is similar to how `mode: MountMode | None = None` works — if not specified, it falls back to the workspace-level default.

But the user said "Require explicit" for mount consistency. However, this is an API request model, not the internal `Mount` class. For API requests, it makes sense to keep optional fields (so users don't have to specify everything).

I'm checking how the consistency field gets resolved in the local.py file — there's logic that checks if consistency was provided in the request and falls back to the default if not. So the API model can stay optional as a way to signal "use the workspace default", but by the time it reaches the internal Mount class, it's always resolved to a concrete value.


Let me check how `MountConfig` is actually consumed:


mirage/server/fastapi/config.py:6:class MountConfig(BaseModel):
mirage/server/fastapi/config.py:15:    mounts: dict[str, str | MountConfig]


`MountConfig` is only used in `CreateWorkspaceRequest.mounts`. Since it's an API request body, it's fine to keep `consistency` as optional in the API model (users can omit it to use the workspace-level default). The internal `Mount` class is what needs to always be explicit. The `local.py` code already resolves it to `default_consistency` now. So I'll leave `MountConfig` as-is but still with a sensible default:


consistency: ConsistencyPolicy | None = None

consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED


The file /Users/agent/strukto/mirage/mirage/server/fastapi/config.py has been updated successfully.

from pydantic import BaseModel

from mirage.types import ConsistencyPolicy, MountMode, SyncPolicy

class MountConfig(BaseModel):
    uri: str | None = None
    backend: str | None = None
    args: dict[str, str] | None = None
    mode: MountMode | None = None
    consistency: ConsistencyPolicy | None = None

class CreateWorkspaceRequest(BaseModel):
    mounts: dict[str, str | MountConfig]
    sync_policy: SyncPolicy = SyncPolicy.STAGED
    mode: MountMode = MountMode.WRITE
    consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED
    ttl_seconds: int = 300
    runtime: str | None = None
    fuse_ops: dict[str, str] | None = None

class RegisterFuseRequest(BaseModel):
    mountpoint: str
    pid: int
    mount_specs: list[str] = []
    runtime: str | None = None

class FilePayloadRequest(BaseModel):
    checksum: str
    data: str | None = None

class ExecRequest(BaseModel):
    command: str
    cwd: str | None = None
    stdin: str | None = None
    input_files: dict[str, FilePayloadRequest] | None = None

class FuseReportRequest(BaseModel):
    mountpoint: str
    status: dict
    transfers: dict = {}
    ops: list[dict] = []

class RegisterCommandRequest(BaseModel):
    name: str
    exec: str
    description: str = ""

class RegisterFuseOpRequest(BaseModel):
    pattern: str
    handler: str

class ReadRequest(BaseModel):
    path: str
    offset: int
    size: int

class WriteRequest(BaseModel):
    path: str
    offset: int
    data: str

class PathRequest(BaseModel):
    path: str

class ReaddirRequest(BaseModel):
    path: str
    cursor: str | None = None

class RenameRequest(BaseModel):
    src: str
    dst: str

class TruncateRequest(BaseModel):
    path: str
    size: int

-    consistency: ConsistencyPolicy | None = None

+    consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED


Now update the CLI `workspace.py` — the `mount_consistency = None` assignments:


   155→        ttl = cfg.get("ttl", ttl)
   156→    consistency_suffixes = ("#cached", "#lazy", "#ttl", "#always")
   157→    for spec in specs:
   158→        try:
   159→            uri, prefix = split_mount_spec(spec)
   160→        except ValueError as e:
   161→            raise typer.BadParameter(str(e))
   162→        mount_consistency = None
   163→        for suffix in consistency_suffixes:
   164→            if uri.endswith(suffix):
   165→                mount_consistency = suffix[1:]
   166→                uri = uri[:-len(suffix)]
   167→                break
   168→        mount_mode = None
   169→        for suffix in ("@read", "@write", "@exec"):
   170→            if uri.endswith(suffix):
   171→                mount_mode = suffix[1:]
   172→                uri = uri[:-len(suffix)]
   173→                break
   174→        entry: dict[str, str] = {"uri": uri}
   175→        if mount_mode:
   176→            entry["mode"] = mount_mode
   177→        if mount_consistency:
   178→            entry["consistency"] = mount_consistency
   179→        if len(entry) == 1:
   180→            mounts[prefix] = uri
   181→        else:
   182→            mounts[prefix] = entry
   183→    ws_id = _client().create_workspace(
   184→        mounts,
   185→        sync_policy=sync_policy,
   186→        mode=mode.value,
   187→        consistency=consistency.value,
   188→        ttl_seconds=ttl,
   189→        runtime=runtime.value,

        ttl = cfg.get("ttl", ttl)
    consistency_suffixes = ("#cached", "#lazy", "#ttl", "#always")
    for spec in specs:
        try:
            uri, prefix = split_mount_spec(spec)
        except ValueError as e:
            raise typer.BadParameter(str(e))
        mount_consistency = None
        for suffix in consistency_suffixes:
            if uri.endswith(suffix):
                mount_consistency = suffix[1:]
                uri = uri[:-len(suffix)]
                break
        mount_mode = None
        for suffix in ("@read", "@write", "@exec"):
            if uri.endswith(suffix):
                mount_mode = suffix[1:]
                uri = uri[:-len(suffix)]
                break
        entry: dict[str, str] = {"uri": uri}
        if mount_mode:
            entry["mode"] = mount_mode
        if mount_consistency:
            entry["consistency"] = mount_consistency
        if len(entry) == 1:
            mounts[prefix] = uri
        else:
            mounts[prefix] = entry
    ws_id = _client().create_workspace(
        mounts,
        sync_policy=sync_policy,
        mode=mode.value,
        consistency=consistency.value,
        ttl_seconds=ttl,
        runtime=runtime.value,


This code builds a dict that's sent as a request to the server API. The `mount_consistency = None` here is just a local variable that's checked with `if mount_consistency:` — it's a string or None, not the enum. This is fine since it's just constructing the API payload. No change needed here.

Now let me also check the `test_init.py` (`test_workspace.py` soon) for the `per_mount_consistency` test that uses a tuple with consistency override:


    75→
    76→def test_dirty_file_skips_revalidation():
    77→    ws, backend = _make_consistency_ws(ConsistencyPolicy.ALWAYS)
    78→    ws.write("/data/file.txt", b"local-edit")
    79→    backend._files["/file.txt"] = b"remote-change"
    80→    assert ws.cat("/data/file.txt") == b"local-edit"
    81→
    82→
    83→def test_per_mount_consistency():
    84→    be_none = MemoryBackend()
    85→    be_none.put("/f.txt", b"v1")
    86→    be_always = MemoryBackend()
    87→    be_always.put("/f.txt", b"v1")
    88→    ws = Workspace(
    89→        {
    90→            "/a/": be_none,
    91→            "/b/": (be_always, MountMode.READ, ConsistencyPolicy.ALWAYS),
    92→        },
    93→        mode=MountMode.READ,
    94→        consistency=ConsistencyPolicy.CACHED,
    95→    )
    96→    assert ws.cat("/a/f.txt") == b"v1"

def test_dirty_file_skips_revalidation():
    ws, backend = _make_consistency_ws(ConsistencyPolicy.ALWAYS)
    ws.write("/data/file.txt", b"local-edit")
    backend._files["/file.txt"] = b"remote-change"
    assert ws.cat("/data/file.txt") == b"local-edit"

def test_per_mount_consistency():
    be_none = MemoryBackend()
    be_none.put("/f.txt", b"v1")
    be_always = MemoryBackend()
    be_always.put("/f.txt", b"v1")
    ws = Workspace(
        {
            "/a/": be_none,
            "/b/": (be_always, MountMode.READ, ConsistencyPolicy.ALWAYS),
        },
        mode=MountMode.READ,
        consistency=ConsistencyPolicy.CACHED,
    )
    assert ws.cat("/a/f.txt") == b"v1"


Good — this test passes the workspace-level `ConsistencyPolicy.CACHED` and mount-level `ConsistencyPolicy.ALWAYS` via the 3-tuple. With our changes, `/a/` will get `ConsistencyPolicy.CACHED` (from the workspace default) and `/b/` will get `ConsistencyPolicy.ALWAYS` (explicit in tuple). This is correct.

Also need to verify that `self._consistency` is still used somewhere — let me check:


62:        self._consistency = consistency


`self._consistency` is only stored but no longer read (since we removed `_effective_consistency` and the tuple parsing now uses the local `consistency` parameter). It can be removed:


        self._sync_policy = sync_policy
        self._consistency = consistency
        self._ttl_seconds = ttl_seconds

        self._sync_policy = sync_policy
        self._ttl_seconds = ttl_seconds


    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→

        self._registry = MountRegistry()
        self._cache = LRUCache(cache_limit)
        self._tracker = DirtyTracker()
        self._sync_policy = sync_policy
        self._consistency = consistency
        self._ttl_seconds = ttl_seconds
        self._locked_paths: set[str] = set()
        self._transfer_stats: dict[str, TransferStats] = {}
        self._created_at: float = time.monotonic()


        self._sync_policy = sync_policy
        self._consistency = consistency
        self._ttl_seconds = ttl_seconds

        self._sync_policy = sync_policy
        self._ttl_seconds = ttl_seconds


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

        self._sync_policy = sync_policy
        self._ttl_seconds = ttl_seconds

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._consistency = consistency
        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

-        self._consistency = consistency


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 3: Move Workspace class to `workspace/workspace.py`. Read the full current `__init__.py`:


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

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


Now create `workspace.py` with the full content and rewrite `__init__.py`:


Write

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