This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.

Analysis:
Let me chronologically analyze this conversation to create a comprehensive summary.

1. **Session start**: Continuation from previous session. The conversation begins with the user asking about how ConsistencyPolicy is used in workspace and runtime, especially for remote deployments.

2. **Consistency analysis**: I traced how ConsistencyPolicy flows through the system - from API request → LocalService.create_workspace() → Workspace constructor → Mount objects → is_fresh() in cache/policy.py. Key finding: `io_*` methods in LocalService bypass Workspace entirely and go directly to backends (no caching, no consistency).

3. **User question about remote path**: User asked "sorry why the remote one does not share the same consistency policy? should we mv consistency to a separated module?"

4. **Before/after diagram**: I created a diagram showing current state (two separate paths: exec→workspace→consistency, io_read→backend directly) vs proposed state (unified through workspace + consistency in its own module).

5. **User approved ("Ok")**: I used the writing-plans skill and created a plan at `docs/plans/2026-03-17-consistency-module.md` with 3 tasks.

6. **User chose subagent execution**: I used subagent-driven-development skill.

7. **Task 1 executed**: Create `workspace/consistency.py` (move ConsistencyPolicy from types.py + is_fresh/metadata_matches from cache/policy.py). Subagent completed this.

8. **Spec review found 2 pre-existing test failures**: `test_mount_fuse_backend_mounts_at_root` and `test_mount_fuse_backend_with_extra_spec` failing with `AttributeError: mirage.fuse.fs has no attribute 'mount'`. Fix subagent corrected to patch `mirage.fuse.mount.mount`.

9. **Code quality review found 3 issues**: Function-level imports in test_app.py (_fuse_defaults), command.py (command_server_serve), workspace.py CLI (_report_loop, _mount_fuse). User denied the fix agent dispatch.

10. **User asked "what are you running?"**: I explained the subagent-driven development process and the 3 tasks.

11. **Continued to Task 2**: Apply consistency in Workspace.read(). The system reminders showed workspace.py was already updated with the new read() method. When I tried to dispatch Task 2 subagent, user denied.

12. **pytest stuck issue**: User reported pytest was hanging. Investigation revealed:
    - Full pytest with --cov was slow (coverage overhead)
    - Actual issue: `tests/cli/test_app.py` was hanging
    - Root cause: `test_mount_fuse_backend_mounts_at_root` was patching `mirage.fuse.mount.mount` but `fuse_mount_fn` is imported at module level in workspace.py as `from mirage.fuse.mount import mount as fuse_mount_fn` - so the patch target should be `mirage.cli.commands.workspace.fuse_mount_fn`
    - Fixed by changing both test patches

13. **User asked to revert pyproject.toml change**: I had removed --cov flags, user asked to revert. Done.

14. **Committed all Task 1+2 changes**: Single commit with workspace.py read() update, test fixes, CLI import moves.

15. **Task 3 executed**: Route io_read/io_stat through Workspace. Done successfully. All 656 tests pass.

16. **Dependency analysis**: User asked to analyze inter-module dependencies. Used Explore agent to map out all module dependencies, identified leaf modules, core modules, no circular deps.

17. **Refactoring candidates identified**:
    - `server/service/local.py` — split into session.py + focused service
    - `workspace/workspace.py` — extract _PathLock
    - `cli/commands/workspace.py` — extract FUSE parsing
    - `fuse/__init__.py` — lazy loading

18. **User said "let's have a plan to refactor this"**: I entered plan mode. Explored local.py (426 lines), base.py (105 lines), test_local.py (214 lines). Now ready to write the plan.

Key findings about local.py:
- Has 5 concerns: workspace lifecycle, command execution, file I/O, fuse op management, FUSE process management
- All methods share `_sessions: dict[str, WorkspaceSession]` and `_file_cache: FileCache`
- `WorkspaceSession` is a Pydantic model (31-48 lines) with all session state
- `_unmount_fuse()` is a helper function
- `register_fuse` and `report_fuse_stats` are "FUSE-specific (not part of MirageService ABC)"

The plan file at `/Users/agent/.claude/plans/shimmering-leaping-wozniak.md` currently has the OLD plan (workspace refactoring from previous session). I need to overwrite it with the new plan for local.py refactoring.

The cleanest split:
1. `session.py` — `WorkspaceSession` + `_unmount_fuse`
2. `local.py` — just `LocalService` class (imports from session.py)

Only `server/fastapi/app.py` uses `LocalService` externally. The test accesses `_sessions` but doesn't import `WorkspaceSession` directly.

Summary:
1. Primary Request and Intent:
   The user has been conducting a series of refactorings across multiple sessions. In this session:
   - Asked how ConsistencyPolicy is used in workspace and runtime, especially for remote deployments
   - Identified that the remote `io_*` path bypasses consistency entirely
   - Approved moving ConsistencyPolicy + freshness logic to `workspace/consistency.py` AND routing `io_read`/`io_stat` through Workspace
   - Approved a 3-task plan executed via subagent-driven development
   - After tasks completed, asked for analysis of inter-module dependencies to identify refactoring candidates
   - Decided to plan a refactor of `server/service/local.py` (the most tangled module with 15+ imports and 5 concerns), currently in plan mode

2. Key Technical Concepts:
   - `ConsistencyPolicy` enum: CACHED, LAZY, TTL, ALWAYS — controls cache freshness in `is_fresh()`
   - `is_fresh()` / `metadata_matches()` — cache freshness logic using etag/mtime/size
   - Python mock patching: must patch where a name is *used*, not where it's defined (e.g., `mirage.cli.commands.workspace.fuse_mount_fn` not `mirage.fuse.mount.mount`)
   - pytest `--import-mode=importlib` — handles duplicate test file names without `__init__.py`
   - pytest coverage (`--cov=mirage --cov-report=term-missing`) causing slow/stuck test runs
   - `LocalService._sessions: dict[str, WorkspaceSession]` — shared state making method extraction complex
   - `WorkspaceSession` — Pydantic model holding all session state (ws, registry, fuse_registry, cwd, etc.)
   - Subagent-driven development skill: dispatcher + spec reviewer + code quality reviewer per task

3. Files and Code Sections:

   - **`mirage/workspace/consistency.py`** (NEW — created in Task 1)
     - Consolidates `ConsistencyPolicy` enum + `is_fresh()` + `metadata_matches()` from `types.py` and `workspace/cache/policy.py`
     ```python
     import time
     from enum import Enum
     from mirage.backend.base import BaseBackend
     from mirage.types import FileStat
     from mirage.workspace.cache.entry import CacheEntry

     class ConsistencyPolicy(str, Enum):
         CACHED = "cached"
         LAZY = "lazy"
         TTL = "ttl"
         ALWAYS = "always"

     def is_fresh(entry, consistency, ttl_seconds, backend, backend_path) -> tuple[bool, FileStat | None]:
         if consistency == ConsistencyPolicy.CACHED: return True, None
         if consistency == ConsistencyPolicy.ALWAYS: return False, None
         if consistency == ConsistencyPolicy.TTL:
             if (time.monotonic() - entry.cached_at) < ttl_seconds: return True, None
         try:
             remote = backend.stat(backend_path)
         except FileNotFoundError:
             return False, None
         return metadata_matches(entry, remote), remote

     def metadata_matches(entry, remote) -> bool:
         remote_etag = remote.extra.get("etag")
         if entry.etag and remote_etag: return entry.etag == remote_etag
         if entry.modified and remote.modified: return entry.modified == remote.modified
         if entry.size is not None and remote.size is not None: return entry.size == remote.size
         return False
     ```

   - **`mirage/types.py`** — `ConsistencyPolicy` class removed (moved to workspace/consistency.py)

   - **`mirage/workspace/cache/policy.py`** — DELETED (content moved to consistency.py)

   - **`mirage/workspace/workspace.py`** — Updated imports + `read()` method now applies consistency:
     ```python
     from mirage.workspace.consistency import ConsistencyPolicy, is_fresh
     # (removed: from mirage.workspace.cache.policy import is_fresh)

     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]
         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:
                 return entry.data[offset:offset + size]
         else:
             backend, bp, _ = self._registry.resolve(path)
         data = backend.cat(bp)
         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[offset:offset + size]
     ```

   - **`mirage/server/service/local.py`** — Updated imports + io_read/io_stat route through Workspace:
     ```python
     from mirage.workspace.consistency import ConsistencyPolicy
     # (removed ConsistencyPolicy from mirage.types import)

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

     def io_stat(self, ws_id: str, path: str) -> FileStat:
         session = self._get_session(ws_id)
         return session.ws.stat(path)
     ```
     Full structure of `local.py` (426 lines):
     - `WorkspaceSession` (Pydantic model, lines 31-48): id, ws, registry, cwd, fuse_mounted, mode, mountpoint, pid, mounts, runtime_type, fuse_registry, command_stats, created_at, reported_status, reported_transfers
     - `_unmount_fuse()` helper (lines 51-61)
     - `LocalService(MirageService)` (lines 64-426): create_workspace, delete_workspace, list_workspaces, exec, list_commands, register_command, sync, status, io_read, io_write, io_stat, io_readdir, io_create, io_unlink, io_mkdir, io_rmdir, io_rename, io_truncate, io_fuse_cat, list_fuse_ops, register_fuse_op, register_fuse, report_fuse_stats

   - **`tests/cli/test_app.py`** — Fixed mock patch targets:
     ```python
     # WRONG (was patching definition site):
     with patch("mirage.fuse.mount.mount") as mock_fuse
     # CORRECT (patch usage site - module-level import alias):
     with patch("mirage.cli.commands.workspace.fuse_mount_fn") as mock_fuse
     ```

   - **`mirage/__init__.py`** — Updated to import ConsistencyPolicy from new location:
     ```python
     from mirage.workspace.consistency import ConsistencyPolicy
     ```

   - **`mirage/workspace/mount.py`** — Updated import:
     ```python
     from mirage.workspace.consistency import ConsistencyPolicy
     ```

   - **`mirage/server/service/base.py`** (105 lines) — Abstract base class `MirageService` defining all abstract methods: create_workspace, delete_workspace, list_workspaces, exec, list_commands, register_command, sync, status, io_read, io_write, io_stat, io_readdir, io_create, io_unlink, io_mkdir, io_rmdir, io_rename, io_truncate, io_fuse_cat, list_fuse_ops, register_fuse_op

   - **`pyproject.toml`** — `addopts = "--cov=mirage --cov-report=term-missing -q --import-mode=importlib"` (coverage flags retained, --import-mode=importlib required for duplicate test file names)

   - **`docs/plans/2026-03-17-consistency-module.md`** — Plan file for the 3-task consistency module refactor (completed)

   - **`/Users/agent/.claude/plans/shimmering-leaping-wozniak.md`** — OLD plan file (workspace refactoring from previous session) — needs to be overwritten with new local.py split plan

4. Errors and fixes:
   - **`mirage.fuse.fs has no attribute 'mount'`**: Tests were patching `mirage.fuse.fs.mount` (wrong). Fixed to `mirage.fuse.mount.mount` (still wrong — definition site). Then found root cause: `workspace.py` CLI imports `from mirage.fuse.mount import mount as fuse_mount_fn` at module level, so must patch `mirage.cli.commands.workspace.fuse_mount_fn` (usage site).

   - **pytest stuck on full suite**: `--cov=mirage --cov-report=term-missing` in addopts was making full runs very slow. Additionally `tests/cli/test_app.py::test_mount_fuse_backend_mounts_at_root` was hanging ~10s because the bad mock let real FUSE mount attempt run. Fixed by correcting mock path. User asked to revert the --cov removal.

   - **Code quality review found function-level imports**: Task 1 subagent moved some but not all function-level imports in `command.py` and `workspace.py` CLI. The agent that would have fixed remaining ones was denied by user.

5. Problem Solving:
   - Successfully completed all 3 tasks of the consistency module refactoring plan:
     1. ConsistencyPolicy + freshness logic moved to `workspace/consistency.py`
     2. `Workspace.read()` now applies consistency policy (cache + is_fresh)
     3. `io_read` / `io_stat` now route through Workspace
   - 656 tests pass, 1 skipped, full suite runs in ~2.2s without coverage
   - Identified the mock patching bug (patch where used, not where defined) which was causing test hangs

6. All user messages:
   - "Ok now give me how the consistency is used for the workspace and runtime? I am not sure for example how consistency is used for workspace deployed at remote?"
   - "sorry why the remote one does not share the same consistency policy? should we mv consistency to a separated module?"
   - "give me a simple diagram before and after changes"
   - "Ok"
   - "subagent"
   - "wait why init in tests remove them" (from previous session context)
   - "what are you running?"
   - "[interrupted] continue"
   - "why the pytest now stuck"
   - "not the workspace test / when you ran uv run pytest it stuck"
   - "I mean rerun the whole tests (which is very quick before) / and investigate why not get stuck / this is a bug"
   - "[interrupted] revert"
   - "ok let's continue the original task"
   - "Ok let's analyze the inter dependency and let's see which module can be refactored out?"
   - "let's have a plan to refactor this"

7. Pending Tasks:
   - **Write plan for `server/service/local.py` refactoring** — currently in plan mode, need to write to `/Users/agent/.claude/plans/shimmering-leaping-wozniak.md` and call ExitPlanMode

8. Current Work:
   In plan mode, planning the refactoring of `mirage/server/service/local.py` (426 lines). The file has 5 distinct concerns all sharing `_sessions: dict[str, WorkspaceSession]`:
   - Workspace lifecycle: `create_workspace`, `delete_workspace`, `list_workspaces`
   - Command execution: `exec`, `list_commands`, `register_command`, `sync`, `status`
   - File I/O: all `io_*` methods (10 methods)
   - FuseOp management: `list_fuse_ops`, `register_fuse_op`
   - FUSE process management (not in ABC): `register_fuse`, `report_fuse_stats`

   Also has data model (`WorkspaceSession`) and helper (`_unmount_fuse`) mixed in with service logic.

   The proposed refactoring: extract `WorkspaceSession` + `_unmount_fuse` to `mirage/server/service/session.py`, keeping `LocalService` in `local.py` (importing from session.py). `server/fastapi/app.py` only imports `LocalService`, not `WorkspaceSession`. Tests access `svc._sessions` dict but don't directly import `WorkspaceSession`.

9. Optional Next Step:
   Complete the plan by writing to `/Users/agent/.claude/plans/shimmering-leaping-wozniak.md` and calling `ExitPlanMode`. The plan should cover:
   - Extract `WorkspaceSession` + `_unmount_fuse` → `mirage/server/service/session.py`
   - Update `mirage/server/service/local.py` to import from `session.py`
   - Update test if needed
   - Verify with `uv run pytest --no-cov`

   Based on the user's most recent message: "let's have a plan to refactor this" (referring to `server/service/local.py` as the biggest refactoring candidate identified in the dependency analysis).

If you need specific details from before compaction (like exact code snippets, error messages, or content you generated), read the full transcript at: /Users/agent/.claude/projects/-Users-agent-strukto-mirage/664396f7-55a4-4604-8289-aa7142480db4.jsonl
Please continue the conversation from where we left off without asking the user any further questions. Continue with the last task that you were asked to work on.


Let me look at the current state of the plan file and the files I need to understand to write the plan for refactoring `server/service/local.py`.

Based on the summary:
- The plan file at `/Users/agent/.claude/plans/shimmering-leaping-wozniak.md` has an OLD plan that needs to be overwritten
- The goal is to plan the refactoring of `server/service/local.py` (426 lines)
- Proposed split: extract `WorkspaceSession` + `_unmount_fuse` → `session.py`, keep `LocalService` in `local.py`

- I already have the content of the main files from the system reminder, so now I need to examine the base service class, the FastAPI app integration, and the existing tests to understand the dependencies and usage patterns before drafting the refactoring plan.


I'm in plan mode, ready to write the plan for refactoring `server/service/local.py`. Let me read the remaining key files to inform the plan.


     1→from abc import ABC, abstractmethod
     2→
     3→from mirage.types import FileStat
     4→
     5→
     6→class MirageService(ABC):
     7→
     8→    @abstractmethod
     9→    def create_workspace(self, config: dict) -> str:
    10→        ...
    11→
    12→    @abstractmethod
    13→    def delete_workspace(self, ws_id: str) -> None:
    14→        ...
    15→
    16→    @abstractmethod
    17→    def list_workspaces(self) -> list[dict]:
    18→        ...
    19→
    20→    @abstractmethod
    21→    def exec(
    22→        self,
    23→        ws_id: str,
    24→        command: str,
    25→        cwd: str | None = None,
    26→        stdin: bytes | None = None,
    27→        input_files: dict[str, tuple[str, bytes | None]] | None = None,
    28→    ) -> tuple[bool, str, dict[str, bytes] | None, dict[str, str] | None]:
    29→        ...
    30→
    31→    @abstractmethod
    32→    def list_commands(self, ws_id: str) -> list[str]:
    33→        ...
    34→
    35→    @abstractmethod
    36→    def register_command(self,
    37→                         ws_id: str,
    38→                         name: str,
    39→                         exec_str: str,
    40→                         description: str = "") -> None:
    41→        ...
    42→
    43→    @abstractmethod
    44→    def sync(self, ws_id: str) -> list[dict]:
    45→        ...
    46→
    47→    @abstractmethod
    48→    def status(self, ws_id: str) -> list[dict]:
    49→        ...
    50→
    51→    @abstractmethod
    52→    def io_read(self, ws_id: str, path: str, offset: int, size: int) -> bytes:
    53→        ...
    54→
    55→    @abstractmethod
    56→    def io_write(self, ws_id: str, path: str, offset: int, data: bytes) -> int:
    57→        ...
    58→
    59→    @abstractmethod
    60→    def io_stat(self, ws_id: str, path: str) -> FileStat:
    61→        ...
    62→
    63→    @abstractmethod
    64→    def io_readdir(self,
    65→                   ws_id: str,
    66→                   path: str,
    67→                   cursor: str | None = None) -> tuple[list[str], str | None]:
    68→        ...
    69→
    70→    @abstractmethod
    71→    def io_create(self, ws_id: str, path: str) -> None:
    72→        ...
    73→
    74→    @abstractmethod
    75→    def io_unlink(self, ws_id: str, path: str) -> None:
    76→        ...
    77→
    78→    @abstractmethod
    79→    def io_mkdir(self, ws_id: str, path: str) -> None:
    80→        ...
    81→
    82→    @abstractmethod
    83→    def io_rmdir(self, ws_id: str, path: str) -> None:
    84→        ...
    85→
    86→    @abstractmethod
    87→    def io_rename(self, ws_id: str, src: str, dst: str) -> None:
    88→        ...
    89→
    90→    @abstractmethod
    91→    def io_truncate(self, ws_id: str, path: str, size: int) -> None:
    92→        ...
    93→
    94→    @abstractmethod
    95→    def io_fuse_cat(self, ws_id: str, path: str) -> bytes:
    96→        ...
    97→
    98→    @abstractmethod
    99→    def list_fuse_ops(self, ws_id: str) -> list[dict]:
   100→        ...
   101→
   102→    @abstractmethod
   103→    def register_fuse_op(self, ws_id: str, pattern: str, handler: str) -> None:
   104→        ...
   105→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

from abc import ABC, abstractmethod

from mirage.types import FileStat

class MirageService(ABC):

    @abstractmethod
    def create_workspace(self, config: dict) -> str:
        ...

    @abstractmethod
    def delete_workspace(self, ws_id: str) -> None:
        ...

    @abstractmethod
    def list_workspaces(self) -> list[dict]:
        ...

    @abstractmethod
    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]:
        ...

    @abstractmethod
    def list_commands(self, ws_id: str) -> list[str]:
        ...

    @abstractmethod
    def register_command(self,
                         ws_id: str,
                         name: str,
                         exec_str: str,
                         description: str = "") -> None:
        ...

    @abstractmethod
    def sync(self, ws_id: str) -> list[dict]:
        ...

    @abstractmethod
    def status(self, ws_id: str) -> list[dict]:
        ...

    @abstractmethod
    def io_read(self, ws_id: str, path: str, offset: int, size: int) -> bytes:
        ...

    @abstractmethod
    def io_write(self, ws_id: str, path: str, offset: int, data: bytes) -> int:
        ...

    @abstractmethod
    def io_stat(self, ws_id: str, path: str) -> FileStat:
        ...

    @abstractmethod
    def io_readdir(self,
                   ws_id: str,
                   path: str,
                   cursor: str | None = None) -> tuple[list[str], str | None]:
        ...

    @abstractmethod
    def io_create(self, ws_id: str, path: str) -> None:
        ...

    @abstractmethod
    def io_unlink(self, ws_id: str, path: str) -> None:
        ...

    @abstractmethod
    def io_mkdir(self, ws_id: str, path: str) -> None:
        ...

    @abstractmethod
    def io_rmdir(self, ws_id: str, path: str) -> None:
        ...

    @abstractmethod
    def io_rename(self, ws_id: str, src: str, dst: str) -> None:
        ...

    @abstractmethod
    def io_truncate(self, ws_id: str, path: str, size: int) -> None:
        ...

    @abstractmethod
    def io_fuse_cat(self, ws_id: str, path: str) -> bytes:
        ...

    @abstractmethod
    def list_fuse_ops(self, ws_id: str) -> list[dict]:
        ...

    @abstractmethod
    def register_fuse_op(self, ws_id: str, pattern: str, handler: str) -> None:
        ...


     1→import pytest
     2→
     3→from mirage.observe import session_log as observe_session_module
     4→from mirage.server.service.local import LocalService
     5→
     6→
     7→@pytest.fixture
     8→def svc():
     9→    s = LocalService()
    10→    observe_session_module._session_logs.clear()
    11→    yield s
    12→    s._sessions.clear()
    13→    observe_session_module._session_logs.clear()
    14→
    15→
    16→def test_create_and_list(svc):
    17→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    18→    assert isinstance(ws_id, str)
    19→    workspaces = svc.list_workspaces()
    20→    assert len(workspaces) == 1
    21→    assert workspaces[0]["id"] == ws_id
    22→
    23→
    24→def test_exec_touch_ls(svc):
    25→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    26→    ok, _, _, _ = svc.exec(ws_id, "touch /hello.txt")
    27→    assert ok
    28→    ok, output, _, _ = svc.exec(ws_id, "ls /")
    29→    assert ok
    30→    assert "hello.txt" in output
    31→
    32→
    33→def test_exec_blocked_with_runtime(svc):
    34→    ws_id = svc.create_workspace({
    35→        "mounts": {
    36→            "/": "memory"
    37→        },
    38→        "runtime": "memory",
    39→    })
    40→    ok, err, _, _ = svc.exec(ws_id, "python -c 'print(1)'")
    41→    assert not ok
    42→    assert "not allowed" in err
    43→
    44→
    45→def test_exec_not_blocked_without_runtime(svc):
    46→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    47→    ok, _, _, _ = svc.exec(ws_id, "ls /")
    48→    assert ok
    49→
    50→
    51→def test_delete_workspace(svc):
    52→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    53→    svc.delete_workspace(ws_id)
    54→    assert svc.list_workspaces() == []
    55→
    56→
    57→def test_delete_missing_raises(svc):
    58→    with pytest.raises(KeyError):
    59→        svc.delete_workspace("bad-id")
    60→
    61→
    62→def test_exec_missing_raises(svc):
    63→    with pytest.raises(KeyError):
    64→        svc.exec("bad-id", "ls /")
    65→
    66→
    67→def test_exec_on_fuse_raises(svc):
    68→    ws_id = svc.register_fuse(mountpoint="/tmp/ws", pid=99999)
    69→    with pytest.raises(ValueError, match="not supported"):
    70→        svc.exec(ws_id, "ls /")
    71→
    72→
    73→def test_sync(svc):
    74→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    75→    results = svc.sync(ws_id)
    76→    assert isinstance(results, list)
    77→
    78→
    79→def test_status(svc):
    80→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    81→    inodes = svc.status(ws_id)
    82→    assert isinstance(inodes, list)
    83→
    84→
    85→def test_list_commands(svc):
    86→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    87→    cmds = svc.list_commands(ws_id)
    88→    assert "ls" in cmds
    89→    assert "cat" in cmds
    90→
    91→
    92→def test_register_command(svc):
    93→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    94→    svc.register_command(ws_id, "hello", "echo hello")
    95→    cmds = svc.list_commands(ws_id)
    96→    assert "hello" in cmds
    97→
    98→
    99→def test_io_write_read(svc):
   100→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
   101→    svc.io_create(ws_id, "/f.txt")
   102→    written = svc.io_write(ws_id, "/f.txt", 0, b"hello world")
   103→    assert written == 11
   104→    data = svc.io_read(ws_id, "/f.txt", 0, 11)
   105→    assert data == b"hello world"
   106→
   107→
   108→def test_io_stat(svc):
   109→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
   110→    svc.io_create(ws_id, "/f.txt")
   111→    svc.io_write(ws_id, "/f.txt", 0, b"hello")
   112→    st = svc.io_stat(ws_id, "/f.txt")
   113→    assert st.size == 5
   114→
   115→
   116→def test_io_stat_missing(svc):
   117→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
   118→    with pytest.raises(FileNotFoundError):
   119→        svc.io_stat(ws_id, "/missing.txt")
   120→
   121→
   122→def test_io_readdir(svc):
   123→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
   124→    svc.io_create(ws_id, "/a.txt")
   125→    svc.io_create(ws_id, "/b.txt")
   126→    entries, cursor = svc.io_readdir(ws_id, "/")
   127→    names = [e.rsplit("/", 1)[-1] for e in entries]
   128→    assert "a.txt" in names
   129→    assert "b.txt" in names
   130→
   131→
   132→def test_io_mkdir_readdir(svc):
   133→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
   134→    svc.io_mkdir(ws_id, "/subdir")
   135→    entries, _ = svc.io_readdir(ws_id, "/")
   136→    names = [e.rsplit("/", 1)[-1] for e in entries]
   137→    assert "subdir" in names
   138→
   139→
   140→def test_io_unlink(svc):
   141→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
   142→    svc.io_create(ws_id, "/f.txt")
   143→    svc.io_unlink(ws_id, "/f.txt")
   144→    with pytest.raises(FileNotFoundError):
   145→        svc.io_stat(ws_id, "/f.txt")
   146→
   147→
   148→def test_io_rename(svc):
   149→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
   150→    svc.io_create(ws_id, "/a.txt")
   151→    svc.io_write(ws_id, "/a.txt", 0, b"data")
   152→    svc.io_rename(ws_id, "/a.txt", "/b.txt")
   153→    with pytest.raises(FileNotFoundError):
   154→        svc.io_stat(ws_id, "/a.txt")
   155→    st = svc.io_stat(ws_id, "/b.txt")
   156→    assert st.size == 4
   157→
   158→
   159→def test_io_truncate(svc):
   160→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
   161→    svc.io_create(ws_id, "/f.txt")
   162→    svc.io_write(ws_id, "/f.txt", 0, b"hello world")
   163→    svc.io_truncate(ws_id, "/f.txt", 5)
   164→    st = svc.io_stat(ws_id, "/f.txt")
   165→    assert st.size == 5
   166→
   167→
   168→def test_io_fuse_cat(svc):
   169→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
   170→    svc.io_create(ws_id, "/f.txt")
   171→    svc.io_write(ws_id, "/f.txt", 0, b"hello")
   172→    data = svc.io_fuse_cat(ws_id, "/f.txt")
   173→    assert data == b"hello"
   174→
   175→
   176→def test_list_fuse_ops(svc):
   177→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
   178→    ops = svc.list_fuse_ops(ws_id)
   179→    assert isinstance(ops, list)
   180→    assert any(o["pattern"] == "*" for o in ops)
   181→
   182→
   183→def test_register_fuse_op(svc):
   184→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
   185→    svc.register_fuse_op(ws_id, "*.bin", "s3")
   186→    ops = svc.list_fuse_ops(ws_id)
   187→    assert any(o["pattern"] == "*.bin" and o["name"] == "s3" for o in ops)
   188→
   189→
   190→def test_register_fuse_op_unknown(svc):
   191→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
   192→    with pytest.raises(ValueError, match="unknown handler"):
   193→        svc.register_fuse_op(ws_id, "*", "nonexistent")
   194→
   195→
   196→def test_io_read_uses_workspace_cache(svc):
   197→    ws_id = svc.create_workspace({
   198→        "mounts": {"/": "memory"},
   199→        "consistency": "cached",
   200→    })
   201→    session = svc._sessions[ws_id]
   202→    session.ws.write("/file.txt", b"hello world")
   203→    session.ws.sync()
   204→    data = svc.io_read(ws_id, "/file.txt", 0, 5)
   205→    assert data == b"hello"
   206→
   207→
   208→def test_io_stat_uses_workspace(svc):
   209→    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
   210→    session = svc._sessions[ws_id]
   211→    session.ws.write("/file.txt", b"hello")
   212→    st = svc.io_stat(ws_id, "/file.txt")
   213→    assert st.size == 5
   214→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import pytest

from mirage.observe import session_log as observe_session_module
from mirage.server.service.local import LocalService

@pytest.fixture
def svc():
    s = LocalService()
    observe_session_module._session_logs.clear()
    yield s
    s._sessions.clear()
    observe_session_module._session_logs.clear()

def test_create_and_list(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    assert isinstance(ws_id, str)
    workspaces = svc.list_workspaces()
    assert len(workspaces) == 1
    assert workspaces[0]["id"] == ws_id

def test_exec_touch_ls(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    ok, _, _, _ = svc.exec(ws_id, "touch /hello.txt")
    assert ok
    ok, output, _, _ = svc.exec(ws_id, "ls /")
    assert ok
    assert "hello.txt" in output

def test_exec_blocked_with_runtime(svc):
    ws_id = svc.create_workspace({
        "mounts": {
            "/": "memory"
        },
        "runtime": "memory",
    })
    ok, err, _, _ = svc.exec(ws_id, "python -c 'print(1)'")
    assert not ok
    assert "not allowed" in err

def test_exec_not_blocked_without_runtime(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    ok, _, _, _ = svc.exec(ws_id, "ls /")
    assert ok

def test_delete_workspace(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    svc.delete_workspace(ws_id)
    assert svc.list_workspaces() == []

def test_delete_missing_raises(svc):
    with pytest.raises(KeyError):
        svc.delete_workspace("bad-id")

def test_exec_missing_raises(svc):
    with pytest.raises(KeyError):
        svc.exec("bad-id", "ls /")

def test_exec_on_fuse_raises(svc):
    ws_id = svc.register_fuse(mountpoint="/tmp/ws", pid=99999)
    with pytest.raises(ValueError, match="not supported"):
        svc.exec(ws_id, "ls /")

def test_sync(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    results = svc.sync(ws_id)
    assert isinstance(results, list)

def test_status(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    inodes = svc.status(ws_id)
    assert isinstance(inodes, list)

def test_list_commands(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    cmds = svc.list_commands(ws_id)
    assert "ls" in cmds
    assert "cat" in cmds

def test_register_command(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    svc.register_command(ws_id, "hello", "echo hello")
    cmds = svc.list_commands(ws_id)
    assert "hello" in cmds

def test_io_write_read(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    svc.io_create(ws_id, "/f.txt")
    written = svc.io_write(ws_id, "/f.txt", 0, b"hello world")
    assert written == 11
    data = svc.io_read(ws_id, "/f.txt", 0, 11)
    assert data == b"hello world"

def test_io_stat(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    svc.io_create(ws_id, "/f.txt")
    svc.io_write(ws_id, "/f.txt", 0, b"hello")
    st = svc.io_stat(ws_id, "/f.txt")
    assert st.size == 5

def test_io_stat_missing(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    with pytest.raises(FileNotFoundError):
        svc.io_stat(ws_id, "/missing.txt")

def test_io_readdir(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    svc.io_create(ws_id, "/a.txt")
    svc.io_create(ws_id, "/b.txt")
    entries, cursor = svc.io_readdir(ws_id, "/")
    names = [e.rsplit("/", 1)[-1] for e in entries]
    assert "a.txt" in names
    assert "b.txt" in names

def test_io_mkdir_readdir(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    svc.io_mkdir(ws_id, "/subdir")
    entries, _ = svc.io_readdir(ws_id, "/")
    names = [e.rsplit("/", 1)[-1] for e in entries]
    assert "subdir" in names

def test_io_unlink(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    svc.io_create(ws_id, "/f.txt")
    svc.io_unlink(ws_id, "/f.txt")
    with pytest.raises(FileNotFoundError):
        svc.io_stat(ws_id, "/f.txt")

def test_io_rename(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    svc.io_create(ws_id, "/a.txt")
    svc.io_write(ws_id, "/a.txt", 0, b"data")
    svc.io_rename(ws_id, "/a.txt", "/b.txt")
    with pytest.raises(FileNotFoundError):
        svc.io_stat(ws_id, "/a.txt")
    st = svc.io_stat(ws_id, "/b.txt")
    assert st.size == 4

def test_io_truncate(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    svc.io_create(ws_id, "/f.txt")
    svc.io_write(ws_id, "/f.txt", 0, b"hello world")
    svc.io_truncate(ws_id, "/f.txt", 5)
    st = svc.io_stat(ws_id, "/f.txt")
    assert st.size == 5

def test_io_fuse_cat(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    svc.io_create(ws_id, "/f.txt")
    svc.io_write(ws_id, "/f.txt", 0, b"hello")
    data = svc.io_fuse_cat(ws_id, "/f.txt")
    assert data == b"hello"

def test_list_fuse_ops(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    ops = svc.list_fuse_ops(ws_id)
    assert isinstance(ops, list)
    assert any(o["pattern"] == "*" for o in ops)

def test_register_fuse_op(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    svc.register_fuse_op(ws_id, "*.bin", "s3")
    ops = svc.list_fuse_ops(ws_id)
    assert any(o["pattern"] == "*.bin" and o["name"] == "s3" for o in ops)

def test_register_fuse_op_unknown(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    with pytest.raises(ValueError, match="unknown handler"):
        svc.register_fuse_op(ws_id, "*", "nonexistent")

def test_io_read_uses_workspace_cache(svc):
    ws_id = svc.create_workspace({
        "mounts": {"/": "memory"},
        "consistency": "cached",
    })
    session = svc._sessions[ws_id]
    session.ws.write("/file.txt", b"hello world")
    session.ws.sync()
    data = svc.io_read(ws_id, "/file.txt", 0, 5)
    assert data == b"hello"

def test_io_stat_uses_workspace(svc):
    ws_id = svc.create_workspace({"mounts": {"/": "memory"}})
    session = svc._sessions[ws_id]
    session.ws.write("/file.txt", b"hello")
    st = svc.io_stat(ws_id, "/file.txt")
    assert st.size == 5


Now I have all the information I need. Let me write the plan for refactoring `server/service/local.py`.

Key observations:
1. `local.py` has 426 lines, mixes `WorkspaceSession` model + `_unmount_fuse` helper + `LocalService` class
2. `WorkspaceSession` is a Pydantic model (lines 31-48) with all session state
3. `_unmount_fuse()` is a standalone helper function (lines 51-61) used only by `LocalService.delete_workspace()`
4. Tests access `svc._sessions[ws_id]` to get session objects directly (lines 201, 202, 210, 211)

5. Tests only import `LocalService` directly, never `WorkspaceSession`
6. `base.py` keeps abstract methods and only imports `FileStat` from `mirage.types`

The refactoring is straightforward: extract `WorkspaceSession` and `_unmount_fuse` into a new `mirage/server/service/session.py` file, then update `local.py` to import from there.


Now I have everything I need. Let me write the plan.


Write

# Refactor `server/service/local.py` — Extract Session Model

## Context

`mirage/server/service/local.py` (426 lines) mixes two concerns:
1. **Data model** — `WorkspaceSession` (Pydantic model, lines 31–48) and `_unmount_fuse` helper (lines 51–61)
2. **Service logic** — `LocalService` class (lines 64–426, ~360 lines)

Every other service module follows the convention of keeping models/data classes separate from service logic. Extracting the session model reduces the cognitive load of reading `local.py` and makes `WorkspaceSession` independently importable.

---

## Step 1: Create `mirage/server/service/session.py`

Move `WorkspaceSession` and `_unmount_fuse` verbatim into a new file:

**`mirage/server/service/session.py`** (new file):
```python
import os
import signal
import subprocess
import sys
import time

from pydantic import BaseModel, ConfigDict, Field

from mirage.commands.custom.registry import CommandRegistry
from mirage.fuse.registry import FuseOpRegistry
from mirage.types import CommandStats, MountMode, RuntimeType
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
```

## Step 2: Update `mirage/server/service/local.py`

Remove `WorkspaceSession`, `_unmount_fuse`, and their imports from `local.py`. Add an import from `session.py` instead.

**Imports to remove from `local.py`**:
- `os`, `signal`, `subprocess`, `sys` (only used by `_unmount_fuse`)
- `time` (only used by `WorkspaceSession.created_at`)
- `from pydantic import BaseModel, ConfigDict, Field`
- `from mirage.commands.custom.registry import CommandRegistry`
- `from mirage.fuse.registry import FuseOpRegistry`
- `from mirage.types import CommandStats` (keep the others)

**Import to add**:
```python
from mirage.server.service.session import WorkspaceSession, _unmount_fuse
```

**Remove** the `WorkspaceSession` class definition (lines 31–48) and `_unmount_fuse` function (lines 51–61) from `local.py`.

## Key Files Modified

- `mirage/server/service/session.py` (new)
- `mirage/server/service/local.py` (remove model + helper, add import)

## Files NOT Changed

- `mirage/server/service/base.py` — untouched
- `mirage/server/fastapi/app.py` — imports only `LocalService`, unchanged
- `tests/server/service/test_local.py` — imports only `LocalService`, accesses `svc._sessions[ws_id]` which still works

## Verification

```bash
uv run pytest tests/server/service/test_local.py -v
uv run pytest -q
```

All 656+ tests should pass.


<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→# Refactor workspace/__init__.py + ConsistencyPolicy cleanup
     2→
     3→## Context
     4→
     5→`mirage/workspace/__init__.py` contains the entire `Workspace` class (~340 lines) plus a `_PathLock` helper. Every other package (`backend/`, `runtime/`, `fuse/`) uses `__init__.py` only for re-exports. Additionally, `ConsistencyPolicy.NONE` is confusing alongside Python's `None` used for "inherit default". This refactor:
     6→1. Moves the `Workspace` class to its own file
     7→2. Renames `ConsistencyPolicy.NONE` → `ConsistencyPolicy.CACHED`
     8→3. Makes `Mount.consistency` always explicit (no more `None` = inherit)
     9→
    10→---
    11→
    12→## Step 1: Rename `ConsistencyPolicy.NONE` → `CACHED`
    13→
    14→**`mirage/types.py`**: Change enum member `NONE = "none"` → `CACHED = "cached"`
    15→
    16→Update all references (~14 occurrences):
    17→- `mirage/workspace/__init__.py` (line 55)
    18→- `mirage/workspace/cache/policy.py` (line 15)
    19→- `mirage/cli/commands/workspace.py` (line 107)
    20→- `mirage/cli/commands/command.py` (lines 57, 69, 77, 78)
    21→- `mirage/cli/config.py` (line 106)
    22→- `mirage/server/fastapi/config.py` (line 18)
    23→- `tests/workspace/test_init.py` (lines 10, 29, 94)
    24→- `tests/cli/test_app.py` (line 36)
    25→- `tests/cli/test_config.py` (line 133)
    26→
    27→## Step 2: Make `Mount.consistency` always explicit
    28→
    29→**`mirage/workspace/mount.py`**:
    30→- `Mount.__init__`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `ConsistencyPolicy | None = None`)
    31→- `MountRegistry.mount()`: same signature change
    32→
    33→**`mirage/workspace/__init__.py`** (soon to be `workspace.py`):
    34→- In the `backends` dict parsing loop: when a tuple doesn't include consistency, use `self._consistency` (the workspace default) instead of `None`
    35→- `Workspace.mount()`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None = None`)
    36→- Remove `_effective_consistency()` method entirely
    37→- In `cat()`: replace `self._effective_consistency(path)` with `self._registry.mount_for(path).consistency`
    38→
    39→**`mirage/server/service/local.py`**: Update mount consistency parsing — use `ConsistencyPolicy.CACHED` instead of `None`
    40→
    41→**`mirage/server/fastapi/config.py`**: `MountConfig.consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None`)
    42→
    43→**`mirage/cli/commands/workspace.py`**: Update any `None` consistency defaults
    44→
    45→## Step 3: Move Workspace to `workspace/workspace.py`
    46→
    47→**Create: `mirage/workspace/workspace.py`**
    48→- Move `_PathLock` class and `Workspace` class from `__init__.py` (all imports stay the same)
    49→
    50→**Rewrite: `mirage/workspace/__init__.py`**
    51→```python
    52→from mirage.workspace.workspace import Workspace
    53→
    54→__all__ = ["Workspace"]
    55→```
    56→
    57→No other files need import changes — they all import `from mirage.workspace import Workspace` which continues to work.
    58→
    59→## Step 4: Rename test file
    60→
    61→- `git mv tests/workspace/test_init.py tests/workspace/test_workspace.py`
    62→- No content changes (imports stay `from mirage.workspace import Workspace`)
    63→
    64→## Key Files Modified
    65→
    66→- `mirage/types.py`
    67→- `mirage/workspace/workspace.py` (new)
    68→- `mirage/workspace/__init__.py` (rewritten to re-exports)
    69→- `mirage/workspace/mount.py`
    70→- `mirage/workspace/cache/policy.py`
    71→- `mirage/cli/commands/workspace.py`, `command.py`, `config.py`
    72→- `mirage/server/fastapi/config.py`, `mirage/server/service/local.py`
    73→- `tests/workspace/test_workspace.py` (renamed)
    74→- `tests/cli/test_app.py`, `tests/cli/test_config.py`
    75→
    76→## Verification
    77→
    78→1. `uv run pytest` — all 641 tests pass
    79→2. `pre-commit run --all-files` — lint/format clean
    80→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

# Refactor workspace/__init__.py + ConsistencyPolicy cleanup

## Context

`mirage/workspace/__init__.py` contains the entire `Workspace` class (~340 lines) plus a `_PathLock` helper. Every other package (`backend/`, `runtime/`, `fuse/`) uses `__init__.py` only for re-exports. Additionally, `ConsistencyPolicy.NONE` is confusing alongside Python's `None` used for "inherit default". This refactor:
1. Moves the `Workspace` class to its own file
2. Renames `ConsistencyPolicy.NONE` → `ConsistencyPolicy.CACHED`
3. Makes `Mount.consistency` always explicit (no more `None` = inherit)

---

## Step 1: Rename `ConsistencyPolicy.NONE` → `CACHED`

**`mirage/types.py`**: Change enum member `NONE = "none"` → `CACHED = "cached"`

Update all references (~14 occurrences):
- `mirage/workspace/__init__.py` (line 55)
- `mirage/workspace/cache/policy.py` (line 15)
- `mirage/cli/commands/workspace.py` (line 107)
- `mirage/cli/commands/command.py` (lines 57, 69, 77, 78)
- `mirage/cli/config.py` (line 106)
- `mirage/server/fastapi/config.py` (line 18)
- `tests/workspace/test_init.py` (lines 10, 29, 94)
- `tests/cli/test_app.py` (line 36)
- `tests/cli/test_config.py` (line 133)

## Step 2: Make `Mount.consistency` always explicit

**`mirage/workspace/mount.py`**:
- `Mount.__init__`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `ConsistencyPolicy | None = None`)
- `MountRegistry.mount()`: same signature change

**`mirage/workspace/__init__.py`** (soon to be `workspace.py`):
- In the `backends` dict parsing loop: when a tuple doesn't include consistency, use `self._consistency` (the workspace default) instead of `None`
- `Workspace.mount()`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None = None`)
- Remove `_effective_consistency()` method entirely
- In `cat()`: replace `self._effective_consistency(path)` with `self._registry.mount_for(path).consistency`

**`mirage/server/service/local.py`**: Update mount consistency parsing — use `ConsistencyPolicy.CACHED` instead of `None`

**`mirage/server/fastapi/config.py`**: `MountConfig.consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None`)

**`mirage/cli/commands/workspace.py`**: Update any `None` consistency defaults

## Step 3: Move Workspace to `workspace/workspace.py`

**Create: `mirage/workspace/workspace.py`**
- Move `_PathLock` class and `Workspace` class from `__init__.py` (all imports stay the same)

**Rewrite: `mirage/workspace/__init__.py`**
```python
from mirage.workspace.workspace import Workspace

__all__ = ["Workspace"]
```

No other files need import changes — they all import `from mirage.workspace import Workspace` which continues to work.

## Step 4: Rename test file

- `git mv tests/workspace/test_init.py tests/workspace/test_workspace.py`
- No content changes (imports stay `from mirage.workspace import Workspace`)

## Key Files Modified

- `mirage/types.py`
- `mirage/workspace/workspace.py` (new)
- `mirage/workspace/__init__.py` (rewritten to re-exports)
- `mirage/workspace/mount.py`
- `mirage/workspace/cache/policy.py`
- `mirage/cli/commands/workspace.py`, `command.py`, `config.py`
- `mirage/server/fastapi/config.py`, `mirage/server/service/local.py`
- `tests/workspace/test_workspace.py` (renamed)
- `tests/cli/test_app.py`, `tests/cli/test_config.py`

## Verification

1. `uv run pytest` — all 641 tests pass
2. `pre-commit run --all-files` — lint/format clean


Edit

# Refactor workspace/__init__.py + ConsistencyPolicy cleanup

## Context

`mirage/workspace/__init__.py` contains the entire `Workspace` class (~340 lines) plus a `_PathLock` helper. Every other package (`backend/`, `runtime/`, `fuse/`) uses `__init__.py` only for re-exports. Additionally, `ConsistencyPolicy.NONE` is confusing alongside Python's `None` used for "inherit default". This refactor:
1. Moves the `Workspace` class to its own file
2. Renames `ConsistencyPolicy.NONE` → `ConsistencyPolicy.CACHED`
3. Makes `Mount.consistency` always explicit (no more `None` = inherit)

---

## Step 1: Rename `ConsistencyPolicy.NONE` → `CACHED`

**`mirage/types.py`**: Change enum member `NONE = "none"` → `CACHED = "cached"`

Update all references (~14 occurrences):
- `mirage/workspace/__init__.py` (line 55)
- `mirage/workspace/cache/policy.py` (line 15)
- `mirage/cli/commands/workspace.py` (line 107)
- `mirage/cli/commands/command.py` (lines 57, 69, 77, 78)
- `mirage/cli/config.py` (line 106)
- `mirage/server/fastapi/config.py` (line 18)
- `tests/workspace/test_init.py` (lines 10, 29, 94)
- `tests/cli/test_app.py` (line 36)
- `tests/cli/test_config.py` (line 133)

## Step 2: Make `Mount.consistency` always explicit

**`mirage/workspace/mount.py`**:
- `Mount.__init__`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `ConsistencyPolicy | None = None`)
- `MountRegistry.mount()`: same signature change

**`mirage/workspace/__init__.py`** (soon to be `workspace.py`):
- In the `backends` dict parsing loop: when a tuple doesn't include consistency, use `self._consistency` (the workspace default) instead of `None`
- `Workspace.mount()`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None = None`)
- Remove `_effective_consistency()` method entirely
- In `cat()`: replace `self._effective_consistency(path)` with `self._registry.mount_for(path).consistency`

**`mirage/server/service/local.py`**: Update mount consistency parsing — use `ConsistencyPolicy.CACHED` instead of `None`

**`mirage/server/fastapi/config.py`**: `MountConfig.consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None`)

**`mirage/cli/commands/workspace.py`**: Update any `None` consistency defaults

## Step 3: Move Workspace to `workspace/workspace.py`

**Create: `mirage/workspace/workspace.py`**
- Move `_PathLock` class and `Workspace` class from `__init__.py` (all imports stay the same)

**Rewrite: `mirage/workspace/__init__.py`**
```python
from mirage.workspace.workspace import Workspace

__all__ = ["Workspace"]
```

No other files need import changes — they all import `from mirage.workspace import Workspace` which continues to work.

## Step 4: Rename test file

- `git mv tests/workspace/test_init.py tests/workspace/test_workspace.py`
- No content changes (imports stay `from mirage.workspace import Workspace`)

## Key Files Modified

- `mirage/types.py`
- `mirage/workspace/workspace.py` (new)
- `mirage/workspace/__init__.py` (rewritten to re-exports)
- `mirage/workspace/mount.py`
- `mirage/workspace/cache/policy.py`
- `mirage/cli/commands/workspace.py`, `command.py`, `config.py`
- `mirage/server/fastapi/config.py`, `mirage/server/service/local.py`
- `tests/workspace/test_workspace.py` (renamed)
- `tests/cli/test_app.py`, `tests/cli/test_config.py`

## Verification

1. `uv run pytest` — all 641 tests pass
2. `pre-commit run --all-files` — lint/format clean

# Refactor `server/service/local.py` — Extract Session Model

## Context

`mirage/server/service/local.py` (426 lines) mixes two concerns:
1. **Data model** — `WorkspaceSession` (Pydantic model, lines 31–48) and `_unmount_fuse` helper (lines 51–61)
2. **Service logic** — `LocalService` class (lines 64–426, ~360 lines)

Extracting the session model reduces cognitive load when reading `local.py` and follows the same convention already used across `backend/`, `workspace/`, and `fuse/` — where models and helpers live in focused sub-modules.

---

## Step 1: Create `mirage/server/service/session.py`

Move `WorkspaceSession` and `_unmount_fuse` verbatim into a new file.

**Imports needed in `session.py`**:
```python
import os
import signal
import subprocess
import sys
import time

from pydantic import BaseModel, ConfigDict, Field

from mirage.commands.custom.registry import CommandRegistry
from mirage.fuse.registry import FuseOpRegistry
from mirage.types import CommandStats, MountMode, RuntimeType
from mirage.workspace import Workspace
```

Then paste `WorkspaceSession` (lines 31–48 of `local.py`) and `_unmount_fuse` (lines 51–61) unchanged.

## Step 2: Update `mirage/server/service/local.py`

**Remove** the `WorkspaceSession` class definition (lines 31–48) and `_unmount_fuse` function (lines 51–61).

**Remove imports** now only used by the deleted code:
- `os`, `signal`, `subprocess`, `sys` (only used by `_unmount_fuse`)
- `time` (only used by `WorkspaceSession.created_at` default)
- `from pydantic import BaseModel, ConfigDict, Field`
- `CommandStats` from `mirage.types` (keep remaining types)
- `from mirage.commands.custom.registry import CommandRegistry` (keep `ExternalCommand`)
- `from mirage.fuse.registry import FuseOpRegistry`

**Add** one import at the top:
```python
from mirage.server.service.session import WorkspaceSession, _unmount_fuse
```

## Key Files

- `mirage/server/service/session.py` — new file
- `mirage/server/service/local.py` — remove model/helper, add import

## Files NOT Changed

- `mirage/server/service/base.py` — untouched
- `mirage/server/fastapi/app.py` — imports only `LocalService`
- `tests/server/service/test_local.py` — imports only `LocalService`; accesses `svc._sessions[ws_id]` which still works

## Verification

```bash
uv run pytest tests/server/service/test_local.py -v
uv run pytest -q
```


The file /Users/agent/.claude/plans/shimmering-leaping-wozniak.md has been updated successfully.

# Refactor workspace/__init__.py + ConsistencyPolicy cleanup

## Context

`mirage/workspace/__init__.py` contains the entire `Workspace` class (~340 lines) plus a `_PathLock` helper. Every other package (`backend/`, `runtime/`, `fuse/`) uses `__init__.py` only for re-exports. Additionally, `ConsistencyPolicy.NONE` is confusing alongside Python's `None` used for "inherit default". This refactor:
1. Moves the `Workspace` class to its own file
2. Renames `ConsistencyPolicy.NONE` → `ConsistencyPolicy.CACHED`
3. Makes `Mount.consistency` always explicit (no more `None` = inherit)

---

## Step 1: Rename `ConsistencyPolicy.NONE` → `CACHED`

**`mirage/types.py`**: Change enum member `NONE = "none"` → `CACHED = "cached"`

Update all references (~14 occurrences):
- `mirage/workspace/__init__.py` (line 55)
- `mirage/workspace/cache/policy.py` (line 15)
- `mirage/cli/commands/workspace.py` (line 107)
- `mirage/cli/commands/command.py` (lines 57, 69, 77, 78)
- `mirage/cli/config.py` (line 106)
- `mirage/server/fastapi/config.py` (line 18)
- `tests/workspace/test_init.py` (lines 10, 29, 94)
- `tests/cli/test_app.py` (line 36)
- `tests/cli/test_config.py` (line 133)

## Step 2: Make `Mount.consistency` always explicit

**`mirage/workspace/mount.py`**:
- `Mount.__init__`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `ConsistencyPolicy | None = None`)
- `MountRegistry.mount()`: same signature change

**`mirage/workspace/__init__.py`** (soon to be `workspace.py`):
- In the `backends` dict parsing loop: when a tuple doesn't include consistency, use `self._consistency` (the workspace default) instead of `None`
- `Workspace.mount()`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None = None`)
- Remove `_effective_consistency()` method entirely
- In `cat()`: replace `self._effective_consistency(path)` with `self._registry.mount_for(path).consistency`

**`mirage/server/service/local.py`**: Update mount consistency parsing — use `ConsistencyPolicy.CACHED` instead of `None`

**`mirage/server/fastapi/config.py`**: `MountConfig.consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None`)

**`mirage/cli/commands/workspace.py`**: Update any `None` consistency defaults

## Step 3: Move Workspace to `workspace/workspace.py`

**Create: `mirage/workspace/workspace.py`**
- Move `_PathLock` class and `Workspace` class from `__init__.py` (all imports stay the same)

**Rewrite: `mirage/workspace/__init__.py`**
```python
from mirage.workspace.workspace import Workspace

__all__ = ["Workspace"]
```

No other files need import changes — they all import `from mirage.workspace import Workspace` which continues to work.

## Step 4: Rename test file

- `git mv tests/workspace/test_init.py tests/workspace/test_workspace.py`
- No content changes (imports stay `from mirage.workspace import Workspace`)

## Key Files Modified

- `mirage/types.py`
- `mirage/workspace/workspace.py` (new)
- `mirage/workspace/__init__.py` (rewritten to re-exports)
- `mirage/workspace/mount.py`
- `mirage/workspace/cache/policy.py`
- `mirage/cli/commands/workspace.py`, `command.py`, `config.py`
- `mirage/server/fastapi/config.py`, `mirage/server/service/local.py`
- `tests/workspace/test_workspace.py` (renamed)
- `tests/cli/test_app.py`, `tests/cli/test_config.py`

## Verification

1. `uv run pytest` — all 641 tests pass
2. `pre-commit run --all-files` — lint/format clean

# Refactor `server/service/local.py` — Extract Session Model

## Context

`mirage/server/service/local.py` (426 lines) mixes two concerns:
1. **Data model** — `WorkspaceSession` (Pydantic model, lines 31–48) and `_unmount_fuse` helper (lines 51–61)
2. **Service logic** — `LocalService` class (lines 64–426, ~360 lines)

Extracting the session model reduces cognitive load when reading `local.py` and follows the same convention already used across `backend/`, `workspace/`, and `fuse/` — where models and helpers live in focused sub-modules.

---

## Step 1: Create `mirage/server/service/session.py`

Move `WorkspaceSession` and `_unmount_fuse` verbatim into a new file.

**Imports needed in `session.py`**:
```python
import os
import signal
import subprocess
import sys
import time

from pydantic import BaseModel, ConfigDict, Field

from mirage.commands.custom.registry import CommandRegistry
from mirage.fuse.registry import FuseOpRegistry
from mirage.types import CommandStats, MountMode, RuntimeType
from mirage.workspace import Workspace
```

Then paste `WorkspaceSession` (lines 31–48 of `local.py`) and `_unmount_fuse` (lines 51–61) unchanged.

## Step 2: Update `mirage/server/service/local.py`

**Remove** the `WorkspaceSession` class definition (lines 31–48) and `_unmount_fuse` function (lines 51–61).

**Remove imports** now only used by the deleted code:
- `os`, `signal`, `subprocess`, `sys` (only used by `_unmount_fuse`)
- `time` (only used by `WorkspaceSession.created_at` default)
- `from pydantic import BaseModel, ConfigDict, Field`
- `CommandStats` from `mirage.types` (keep remaining types)
- `from mirage.commands.custom.registry import CommandRegistry` (keep `ExternalCommand`)
- `from mirage.fuse.registry import FuseOpRegistry`

**Add** one import at the top:
```python
from mirage.server.service.session import WorkspaceSession, _unmount_fuse
```

## Key Files

- `mirage/server/service/session.py` — new file
- `mirage/server/service/local.py` — remove model/helper, add import

## Files NOT Changed

- `mirage/server/service/base.py` — untouched
- `mirage/server/fastapi/app.py` — imports only `LocalService`
- `tests/server/service/test_local.py` — imports only `LocalService`; accesses `svc._sessions[ws_id]` which still works

## Verification

```bash
uv run pytest tests/server/service/test_local.py -v
uv run pytest -q
```

-# Refactor workspace/__init__.py + ConsistencyPolicy cleanup

+# Refactor `server/service/local.py` — Extract Session Model

-`mirage/workspace/__init__.py` contains the entire `Workspace` class (~340 lines) plus a `_PathLock` helper. Every other package (`backend/`, `runtime/`, `fuse/`) uses `__init__.py` only for re-exports. Additionally, `ConsistencyPolicy.NONE` is confusing alongside Python's `None` used for "inherit default". This refactor:

-1. Moves the `Workspace` class to its own file

-2. Renames `ConsistencyPolicy.NONE` → `ConsistencyPolicy.CACHED`

-3. Makes `Mount.consistency` always explicit (no more `None` = inherit)

+`mirage/server/service/local.py` (426 lines) mixes two concerns:

+1. **Data model** — `WorkspaceSession` (Pydantic model, lines 31–48) and `_unmount_fuse` helper (lines 51–61)

+2. **Service logic** — `LocalService` class (lines 64–426, ~360 lines)

+Extracting the session model reduces cognitive load when reading `local.py` and follows the same convention already used across `backend/`, `workspace/`, and `fuse/` — where models and helpers live in focused sub-modules.

-## Step 1: Rename `ConsistencyPolicy.NONE` → `CACHED`

+## Step 1: Create `mirage/server/service/session.py`

-**`mirage/types.py`**: Change enum member `NONE = "none"` → `CACHED = "cached"`

+Move `WorkspaceSession` and `_unmount_fuse` verbatim into a new file.

-Update all references (~14 occurrences):

-- `mirage/workspace/__init__.py` (line 55)

-- `mirage/workspace/cache/policy.py` (line 15)

-- `mirage/cli/commands/workspace.py` (line 107)

-- `mirage/cli/commands/command.py` (lines 57, 69, 77, 78)

-- `mirage/server/fastapi/config.py` (line 18)

-- `tests/workspace/test_init.py` (lines 10, 29, 94)

-- `tests/cli/test_config.py` (line 133)

-## Step 2: Make `Mount.consistency` always explicit

+from pydantic import BaseModel, ConfigDict, Field

-- `Mount.__init__`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `ConsistencyPolicy | None = None`)

-- `MountRegistry.mount()`: same signature change

+from mirage.commands.custom.registry import CommandRegistry

+from mirage.fuse.registry import FuseOpRegistry

+from mirage.types import CommandStats, MountMode, RuntimeType

-**`mirage/workspace/__init__.py`** (soon to be `workspace.py`):

-- In the `backends` dict parsing loop: when a tuple doesn't include consistency, use `self._consistency` (the workspace default) instead of `None`

-- `Workspace.mount()`: `consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None = None`)

-- Remove `_effective_consistency()` method entirely

-- In `cat()`: replace `self._effective_consistency(path)` with `self._registry.mount_for(path).consistency`

+Then paste `WorkspaceSession` (lines 31–48 of `local.py`) and `_unmount_fuse` (lines 51–61) unchanged.

-**`mirage/server/service/local.py`**: Update mount consistency parsing — use `ConsistencyPolicy.CACHED` instead of `None`

+## Step 2: Update `mirage/server/service/local.py`

-**`mirage/server/fastapi/config.py`**: `MountConfig.consistency: ConsistencyPolicy = ConsistencyPolicy.CACHED` (was `| None`)

+**Remove** the `WorkspaceSession` class definition (lines 31–48) and `_unmount_fuse` function (lines 51–61).

-**`mirage/cli/commands/workspace.py`**: Update any `None` consistency defaults

+**Remove imports** now only used by the deleted code:

+- `os`, `signal`, `subprocess`, `sys` (only used by `_unmount_fuse`)

+- `time` (only used by `WorkspaceSession.created_at` default)

+- `from pydantic import BaseModel, ConfigDict, Field`

+- `CommandStats` from `mirage.types` (keep remaining types)

+- `from mirage.commands.custom.registry import CommandRegistry` (keep `ExternalCommand`)

+- `from mirage.fuse.registry import FuseOpRegistry`

-## Step 3: Move Workspace to `workspace/workspace.py`

-- Move `_PathLock` class and `Workspace` class from `__init__.py` (all imports stay the same)

-from mirage.workspace.workspace import Workspace

+from mirage.server.service.session import WorkspaceSession, _unmount_fuse

-No other files need import changes — they all import `from mirage.workspace import Workspace` which continues to work.

+- `mirage/server/service/session.py` — new file

+- `mirage/server/service/local.py` — remove model/helper, add import

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

-- No content changes (imports stay `from mirage.workspace import Workspace`)

+- `mirage/server/service/base.py` — untouched

+- `mirage/server/fastapi/app.py` — imports only `LocalService`

+- `tests/server/service/test_local.py` — imports only `LocalService`; accesses `svc._sessions[ws_id]` which still works

-- `mirage/workspace/__init__.py` (rewritten to re-exports)

-- `mirage/cli/commands/workspace.py`, `command.py`, `config.py`

-1. `uv run pytest` — all 641 tests pass

-2. `pre-commit run --all-files` — lint/format clean

+uv run pytest tests/server/service/test_local.py -v


ExitPlanMode

# Refactor `server/service/local.py` — Extract Session Model

## Context

`mirage/server/service/local.py` (426 lines) mixes two concerns:
1. **Data model** — `WorkspaceSession` (Pydantic model, lines 31–48) and `_unmount_fuse` helper (lines 51–61)
2. **Service logic** — `LocalService` class (lines 64–426, ~360 lines)

Extracting the session model reduces cognitive load when reading `local.py` and follows the same convention already used across `backend/`, `workspace/`, and `fuse/` — where models and helpers live in focused sub-modules.

---

## Step 1: Create `mirage/server/service/session.py`

Move `WorkspaceSession` and `_unmount_fuse` verbatim into a new file.

**Imports needed in `session.py`**:
```python
import os
import signal
import subprocess
import sys
import time

from pydantic import BaseModel, ConfigDict, Field

from mirage.commands.custom.registry import CommandRegistry
from mirage.fuse.registry import FuseOpRegistry
from mirage.types import CommandStats, MountMode, RuntimeType
from mirage.workspace import Workspace
```

Then paste `WorkspaceSession` (lines 31–48 of `local.py`) and `_unmount_fuse` (lines 51–61) unchanged.

## Step 2: Update `mirage/server/service/local.py`

**Remove** the `WorkspaceSession` class definition (lines 31–48) and `_unmount_fuse` function (lines 51–61).

**Remove imports** now only used by the deleted code:
- `os`, `signal`, `subprocess`, `sys` (only used by `_unmount_fuse`)
- `time` (only used by `WorkspaceSession.created_at` default)
- `from pydantic import BaseModel, ConfigDict, Field`
- `CommandStats` from `mirage.types` (keep remaining types)
- `from mirage.commands.custom.registry import CommandRegistry` (keep `ExternalCommand`)
- `from mirage.fuse.registry import FuseOpRegistry`

**Add** one import at the top:
```python
from mirage.server.service.session import WorkspaceSession, _unmount_fuse
```

## Key Files

- `mirage/server/service/session.py` — new file
- `mirage/server/service/local.py` — remove model/helper, add import

## Files NOT Changed

- `mirage/server/service/base.py` — untouched
- `mirage/server/fastapi/app.py` — imports only `LocalService`
- `tests/server/service/test_local.py` — imports only `LocalService`; accesses `svc._sessions[ws_id]` which still works

## Verification

```bash
uv run pytest tests/server/service/test_local.py -v
uv run pytest -q
```


User has approved your plan. You can now start coding. Start with updating your todo list if applicable

Your plan has been saved to: /Users/agent/.claude/plans/shimmering-leaping-wozniak.md
You can refer back to it if needed during implementation.

## Approved Plan:
# Refactor `server/service/local.py` — Extract Session Model

## Context

`mirage/server/service/local.py` (426 lines) mixes two concerns:
1. **Data model** — `WorkspaceSession` (Pydantic model, lines 31–48) and `_unmount_fuse` helper (lines 51–61)
2. **Service logic** — `LocalService` class (lines 64–426, ~360 lines)

Extracting the session model reduces cognitive load when reading `local.py` and follows the same convention already used across `backend/`, `workspace/`, and `fuse/` — where models and helpers live in focused sub-modules.

---

## Step 1: Create `mirage/server/service/session.py`

Move `WorkspaceSession` and `_unmount_fuse` verbatim into a new file.

**Imports needed in `session.py`**:
```python
import os
import signal
import subprocess
import sys
import time

from pydantic import BaseModel, ConfigDict, Field

from mirage.commands.custom.registry import CommandRegistry
from mirage.fuse.registry import FuseOpRegistry
from mirage.types import CommandStats, MountMode, RuntimeType
from mirage.workspace import Workspace
```

Then paste `WorkspaceSession` (lines 31–48 of `local.py`) and `_unmount_fuse` (lines 51–61) unchanged.

## Step 2: Update `mirage/server/service/local.py`

**Remove** the `WorkspaceSession` class definition (lines 31–48) and `_unmount_fuse` function (lines 51–61).

**Remove imports** now only used by the deleted code:
- `os`, `signal`, `subprocess`, `sys` (only used by `_unmount_fuse`)
- `time` (only used by `WorkspaceSession.created_at` default)
- `from pydantic import BaseModel, ConfigDict, Field`
- `CommandStats` from `mirage.types` (keep remaining types)
- `from mirage.commands.custom.registry import CommandRegistry` (keep `ExternalCommand`)
- `from mirage.fuse.registry import FuseOpRegistry`

**Add** one import at the top:
```python
from mirage.server.service.session import WorkspaceSession, _unmount_fuse
```

## Key Files

- `mirage/server/service/session.py` — new file
- `mirage/server/service/local.py` — remove model/helper, add import

## Files NOT Changed

- `mirage/server/service/base.py` — untouched
- `mirage/server/fastapi/app.py` — imports only `LocalService`
- `tests/server/service/test_local.py` — imports only `LocalService`; accesses `svc._sessions[ws_id]` which still works

## Verification

```bash
uv run pytest tests/server/service/test_local.py -v
uv run pytest -q
```


Now I can implement the plan. Let me execute it:

1. Create `mirage/server/service/session.py` with `WorkspaceSession` and `_unmount_fuse`
2. Update `mirage/server/service/local.py` to import from `session.py` and remove the moved code

I already have the content of `local.py` from earlier reads. Let me proceed.


TodoWrite

Create mirage/server/service/session.py

Creating session.py with WorkspaceSession and _unmount_fuse

Update local.py to import from session.py

Verify tests pass


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

Create mirage/server/service/session.py

Creating session.py with WorkspaceSession and _unmount_fuse

Update local.py to import from session.py

Verify tests pass


Write

import os
import signal
import subprocess
import sys
import time

from pydantic import BaseModel, ConfigDict, Field

from mirage.commands.custom.registry import CommandRegistry
from mirage.fuse.registry import FuseOpRegistry
from mirage.types import CommandStats, MountMode, RuntimeType
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


File created successfully at: /Users/agent/strukto/mirage/mirage/server/service/session.py

import os
import signal
import subprocess
import sys
import time

from pydantic import BaseModel, ConfigDict, Field

from mirage.commands.custom.registry import CommandRegistry
from mirage.fuse.registry import FuseOpRegistry
from mirage.types import CommandStats, MountMode, RuntimeType
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


TodoWrite

Create mirage/server/service/session.py

Creating session.py with WorkspaceSession and _unmount_fuse

Update local.py to import from session.py

Verify tests pass


Create mirage/server/service/session.py

Verify tests pass

Create mirage/server/service/session.py

Verify tests pass


Now update `local.py` — remove the model/helper and add the import:


Edit

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, FileStat, MountMode, RuntimeType,
                          SyncPolicy)
from mirage.workspace import Workspace
from mirage.workspace.consistency import ConsistencyPolicy

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

import uuid
from datetime import datetime, timezone

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 ExternalCommand
from mirage.fuse import FUSE_OPS
from mirage.fuse.loader import build_registry_from_config, load_fuse_op_class
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.server.service.session import WorkspaceSession, _unmount_fuse
from mirage.types import (FileStat, MountMode, RuntimeType, SyncPolicy)
from mirage.workspace import Workspace
from mirage.workspace.consistency import ConsistencyPolicy


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

import uuid
from datetime import datetime, timezone

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 ExternalCommand
from mirage.fuse import FUSE_OPS
from mirage.fuse.loader import build_registry_from_config, load_fuse_op_class
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.server.service.session import WorkspaceSession, _unmount_fuse
from mirage.types import (FileStat, MountMode, RuntimeType, SyncPolicy)
from mirage.workspace import Workspace
from mirage.workspace.consistency import ConsistencyPolicy

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, FileStat, MountMode, RuntimeType,
                          SyncPolicy)
from mirage.workspace import Workspace
from mirage.workspace.consistency import ConsistencyPolicy

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", "cached")
        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)
        return session.ws.read(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)
        return session.ws.stat(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

-from pydantic import BaseModel, ConfigDict, Field

-from mirage.commands.custom.registry import CommandRegistry, ExternalCommand

+from mirage.commands.custom.registry import ExternalCommand

-from mirage.fuse.registry import FuseOpRegistry

-from mirage.types import (CommandStats, FileStat, MountMode, RuntimeType,

+from mirage.server.service.session import WorkspaceSession, _unmount_fuse

+from mirage.types import (FileStat, MountMode, RuntimeType, SyncPolicy)

-    model_config = ConfigDict(arbitrary_types_allowed=True)

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

-        subprocess.run(["diskutil", "unmount", "force", mountpoint],

-                       capture_output=True)

-        subprocess.run(["fusermount", "-u", mountpoint], capture_output=True)

-            os.kill(pid, signal.SIGTERM)


TodoWrite

Create mirage/server/service/session.py

Creating session.py with WorkspaceSession and _unmount_fuse

Update local.py to import from session.py

Verify tests pass


Bash

uv run pytest tests/server/service/test_local.py -v --no-header -q 2>&1 | tail -20

Run service tests to verify refactor