Skip to content

Projects and workspaces

octomate.managers.project

The project registry: the code locations declared in the projects: block.

ProjectManager

Bases: Manager

The project registry: every code location Octomate knows by name.

A project gets here one way: the operator declares it in the projects: block, which reconcile upserts at startup. Declaring is a trust act — a declared project's AGENTS.md reaches an agent as instructions — so a session running where nothing is declared files its work under no project rather than minting one.

Every enabled project resolves. Projects are expected to remain a small registry and are all cached.

Note the name collision: projects: here is the operator's registry of code locations, and is unrelated to ~/.claude/projects/, which is where Claude Code stores transcripts.

Source code in octomate/managers/project.py
class ProjectManager(Manager):
    """The project registry: every code location Octomate knows by name.

    A project gets here one way: the operator declares it in the ``projects:`` block,
    which `reconcile` upserts at startup. Declaring is a trust act — a declared
    project's `AGENTS.md` reaches an agent as instructions — so a session running
    where nothing is declared files its work under no project rather than minting one.

    Every enabled project resolves. Projects are expected to remain a small registry
    and are all cached.

    Note the name collision: ``projects:`` here is the operator's registry of code
    locations, and is unrelated to ``~/.claude/projects/``, which is where Claude
    Code stores transcripts.
    """

    def __init__(self, config: ProjectsConfig | None = None) -> None:
        self.config = config if config is not None else {}
        self.projects: dict[str, Project] = {}
        # (resolved root, project name), deepest first, so the first hit is the
        # longest prefix and a monorepo plus a sub-package can both be projects.
        self.roots: list[tuple[Path, str]] = []

    def get(self, name: str) -> Project | None:
        """The project called ``name``, or None if none is registered as it. Disabled
        ones answer too — a thread filed under one still has to say where it was.

        Reads the cache the registry keeps, so it does no IO and is safe from a sync
        caller.
        """
        return self.projects.get(name)

    async def of(self, thread: Thread) -> Project | None:
        """The project this thread's work runs in, or None when it runs in none.

        The thread is where a project is bound, so its attribution is the question;
        whether that attribution still names somewhere an agent may run is this
        registry's answer, and the two are not the same. A thread naming a project
        the block no longer declares is in none, and so is one whose project is
        disabled — the row is retained either way, because it still has to say where
        the work was filed, but neither has a root left to offer.

        Read through the registry rather than off the row, which is exactly why this
        is not just `thread.project`.
        """
        attributed = await thread.project
        if attributed is None:
            return None
        project = self.get(attributed.name)
        return project if project is not None and project.enabled else None

    def list(self) -> list[Project]:
        """Every registered project."""
        return list(self.projects.values())

    def resolve(self, cwd: Path) -> str | None:
        """The name of the project ``cwd`` is in, or None if none holds it yet.

        Resolved before comparing, because `is_relative_to` is lexical: `/tmp` against
        a registered `/private/tmp`, or a symlinked home, would otherwise miss silently.
        """
        resolved = cwd.resolve()
        for root, name in self.roots:
            if resolved.is_relative_to(root):
                return name
        return None

    async def load(self) -> None:
        """Read every stored project into the cache and the resolution index.

        One pass at startup, because a project resolves by existing: nothing narrows
        the registry to a subset that some other source still vouches for.
        """
        async with async_session() as session:
            self.index(await session.list(Project, limit=None))

    async def reconcile(self) -> None:
        """Upsert the declared block into the registry, then index what disk still has.

        A declaration is matched to its row by `root`, not by name: the root is the
        unique column and the identity, so declaring a directory that already has a
        row — left by an earlier block, or by the era when sessions registered
        projects — adopts that row under the declared name rather than colliding with
        it on the way to a second row for one directory.

        A row the block no longer declares is left exactly as it is. The threads and
        runs recorded under it still name it, so dropping a declaration is not a
        request to forget the history filed there.

        What does get cleared is `enabled`, for every row whose root is no longer a
        directory on disk — a deleted checkout, an unmounted drive. The row stays, so
        the threads and runs that name it still read back, and it re-enables by itself
        the moment the directory is there again.

        Both conflict checks run over the reconciled registry rather than over the
        declared block, because a declaration collides with a row an earlier block
        left behind as readily as with another declaration, and the operator reading
        the error needs to be told which two.
        """
        async with async_session() as session:
            stored = list(await session.list(Project, limit=None))
            by_root = {project.root.resolve(): project for project in stored}
            for name, config in self.config.items():
                # Shelled here and not at config time: a transmuter validated outside
                # the materia carries no row, and this is the first place inside one.
                declared = Project.shell(config)
                declared.name = name
                project = by_root.get(declared.root.resolve())
                if project is None:
                    session.add(declared)
                    stored.append(declared)
                    continue
                project.name = name
                project.extra_roots = declared.extra_roots
                project.upstream = declared.upstream
                project.description = declared.description
            named: dict[str, Path] = {}
            rooted: dict[Path, str] = {}
            for project in stored:
                # Ahead of the unique constraint, which would refuse the same rename
                # without saying which two directories wanted the name.
                if (other := named.get(project.name)) is not None:
                    raise ValueError(
                        f"{other} and {project.root} are both named {project.name!r}"
                    )
                named[project.name] = project.root
                # Every root a project answers to, so an `extra_roots` entry pointing at
                # another project is refused too — nothing constrains those, and a
                # directory two projects claim resolves to whichever sorted first.
                for root in project.roots:
                    if (claimed_by := rooted.get(root.resolve())) is not None:
                        raise ValueError(
                            f"{root} is claimed by both {claimed_by!r} and "
                            f"{project.name!r}"
                        )
                    rooted[root.resolve()] = project.name
                project.enabled = project.root.is_dir()
            await session.commit()
            self.index(stored)

    def index(self, projects: Iterable[Project]) -> None:
        """Rebuild the by-name map and the resolution index from `projects`.

        Every project is kept by name, disabled ones included, so a thread filed under
        one still reads back and its name stays taken. Only enabled projects get roots:
        a directory that is gone resolves nothing, and a run must not be sent there.

        Roots are sorted deepest first, so the first hit is the longest prefix and a
        monorepo plus a package inside it can both be projects.
        """
        self.projects = {project.name: project for project in projects}
        self.roots = sorted(
            (
                (root.resolve(), project.name)
                for project in self.projects.values()
                if project.enabled
                for root in (project.root, *project.extra_roots)
            ),
            key=lambda pair: len(pair[0].parts),
            reverse=True,
        )

config instance-attribute

config = config if config is not None else {}

projects instance-attribute

projects: dict[str, Project] = {}

roots instance-attribute

roots: list[tuple[Path, str]] = []

get

get(name: str) -> Project | None

The project called name, or None if none is registered as it. Disabled ones answer too — a thread filed under one still has to say where it was.

Reads the cache the registry keeps, so it does no IO and is safe from a sync caller.

Source code in octomate/managers/project.py
def get(self, name: str) -> Project | None:
    """The project called ``name``, or None if none is registered as it. Disabled
    ones answer too — a thread filed under one still has to say where it was.

    Reads the cache the registry keeps, so it does no IO and is safe from a sync
    caller.
    """
    return self.projects.get(name)

of async

of(thread: Thread) -> Project | None

The project this thread's work runs in, or None when it runs in none.

The thread is where a project is bound, so its attribution is the question; whether that attribution still names somewhere an agent may run is this registry's answer, and the two are not the same. A thread naming a project the block no longer declares is in none, and so is one whose project is disabled — the row is retained either way, because it still has to say where the work was filed, but neither has a root left to offer.

Read through the registry rather than off the row, which is exactly why this is not just thread.project.

Source code in octomate/managers/project.py
async def of(self, thread: Thread) -> Project | None:
    """The project this thread's work runs in, or None when it runs in none.

    The thread is where a project is bound, so its attribution is the question;
    whether that attribution still names somewhere an agent may run is this
    registry's answer, and the two are not the same. A thread naming a project
    the block no longer declares is in none, and so is one whose project is
    disabled — the row is retained either way, because it still has to say where
    the work was filed, but neither has a root left to offer.

    Read through the registry rather than off the row, which is exactly why this
    is not just `thread.project`.
    """
    attributed = await thread.project
    if attributed is None:
        return None
    project = self.get(attributed.name)
    return project if project is not None and project.enabled else None

list

list() -> list[Project]

Every registered project.

Source code in octomate/managers/project.py
def list(self) -> list[Project]:
    """Every registered project."""
    return list(self.projects.values())

resolve

resolve(cwd: Path) -> str | None

The name of the project cwd is in, or None if none holds it yet.

Resolved before comparing, because is_relative_to is lexical: /tmp against a registered /private/tmp, or a symlinked home, would otherwise miss silently.

Source code in octomate/managers/project.py
def resolve(self, cwd: Path) -> str | None:
    """The name of the project ``cwd`` is in, or None if none holds it yet.

    Resolved before comparing, because `is_relative_to` is lexical: `/tmp` against
    a registered `/private/tmp`, or a symlinked home, would otherwise miss silently.
    """
    resolved = cwd.resolve()
    for root, name in self.roots:
        if resolved.is_relative_to(root):
            return name
    return None

load async

load() -> None

Read every stored project into the cache and the resolution index.

One pass at startup, because a project resolves by existing: nothing narrows the registry to a subset that some other source still vouches for.

Source code in octomate/managers/project.py
async def load(self) -> None:
    """Read every stored project into the cache and the resolution index.

    One pass at startup, because a project resolves by existing: nothing narrows
    the registry to a subset that some other source still vouches for.
    """
    async with async_session() as session:
        self.index(await session.list(Project, limit=None))

reconcile async

reconcile() -> None

Upsert the declared block into the registry, then index what disk still has.

A declaration is matched to its row by root, not by name: the root is the unique column and the identity, so declaring a directory that already has a row — left by an earlier block, or by the era when sessions registered projects — adopts that row under the declared name rather than colliding with it on the way to a second row for one directory.

A row the block no longer declares is left exactly as it is. The threads and runs recorded under it still name it, so dropping a declaration is not a request to forget the history filed there.

What does get cleared is enabled, for every row whose root is no longer a directory on disk — a deleted checkout, an unmounted drive. The row stays, so the threads and runs that name it still read back, and it re-enables by itself the moment the directory is there again.

Both conflict checks run over the reconciled registry rather than over the declared block, because a declaration collides with a row an earlier block left behind as readily as with another declaration, and the operator reading the error needs to be told which two.

Source code in octomate/managers/project.py
async def reconcile(self) -> None:
    """Upsert the declared block into the registry, then index what disk still has.

    A declaration is matched to its row by `root`, not by name: the root is the
    unique column and the identity, so declaring a directory that already has a
    row — left by an earlier block, or by the era when sessions registered
    projects — adopts that row under the declared name rather than colliding with
    it on the way to a second row for one directory.

    A row the block no longer declares is left exactly as it is. The threads and
    runs recorded under it still name it, so dropping a declaration is not a
    request to forget the history filed there.

    What does get cleared is `enabled`, for every row whose root is no longer a
    directory on disk — a deleted checkout, an unmounted drive. The row stays, so
    the threads and runs that name it still read back, and it re-enables by itself
    the moment the directory is there again.

    Both conflict checks run over the reconciled registry rather than over the
    declared block, because a declaration collides with a row an earlier block
    left behind as readily as with another declaration, and the operator reading
    the error needs to be told which two.
    """
    async with async_session() as session:
        stored = list(await session.list(Project, limit=None))
        by_root = {project.root.resolve(): project for project in stored}
        for name, config in self.config.items():
            # Shelled here and not at config time: a transmuter validated outside
            # the materia carries no row, and this is the first place inside one.
            declared = Project.shell(config)
            declared.name = name
            project = by_root.get(declared.root.resolve())
            if project is None:
                session.add(declared)
                stored.append(declared)
                continue
            project.name = name
            project.extra_roots = declared.extra_roots
            project.upstream = declared.upstream
            project.description = declared.description
        named: dict[str, Path] = {}
        rooted: dict[Path, str] = {}
        for project in stored:
            # Ahead of the unique constraint, which would refuse the same rename
            # without saying which two directories wanted the name.
            if (other := named.get(project.name)) is not None:
                raise ValueError(
                    f"{other} and {project.root} are both named {project.name!r}"
                )
            named[project.name] = project.root
            # Every root a project answers to, so an `extra_roots` entry pointing at
            # another project is refused too — nothing constrains those, and a
            # directory two projects claim resolves to whichever sorted first.
            for root in project.roots:
                if (claimed_by := rooted.get(root.resolve())) is not None:
                    raise ValueError(
                        f"{root} is claimed by both {claimed_by!r} and "
                        f"{project.name!r}"
                    )
                rooted[root.resolve()] = project.name
            project.enabled = project.root.is_dir()
        await session.commit()
        self.index(stored)

index

index(projects: Iterable[Project]) -> None

Rebuild the by-name map and the resolution index from projects.

Every project is kept by name, disabled ones included, so a thread filed under one still reads back and its name stays taken. Only enabled projects get roots: a directory that is gone resolves nothing, and a run must not be sent there.

Roots are sorted deepest first, so the first hit is the longest prefix and a monorepo plus a package inside it can both be projects.

Source code in octomate/managers/project.py
def index(self, projects: Iterable[Project]) -> None:
    """Rebuild the by-name map and the resolution index from `projects`.

    Every project is kept by name, disabled ones included, so a thread filed under
    one still reads back and its name stays taken. Only enabled projects get roots:
    a directory that is gone resolves nothing, and a run must not be sent there.

    Roots are sorted deepest first, so the first hit is the longest prefix and a
    monorepo plus a package inside it can both be projects.
    """
    self.projects = {project.name: project for project in projects}
    self.roots = sorted(
        (
            (root.resolve(), project.name)
            for project in self.projects.values()
            if project.enabled
            for root in (project.root, *project.extra_roots)
        ),
        key=lambda pair: len(pair[0].parts),
        reverse=True,
    )

octomate.managers.workspaces.base

Thread workspaces: the forks of a project mirror a run happens in, and their lifecycle from open to save to release.

logger module-attribute

logger = logging.getLogger(__name__)
REFLINK_FLAGS = ('-c', '--reflink=always')

SAVED_REF module-attribute

SAVED_REF = 'refs/octomate/saved'

SNAPSHOT_PREFIX module-attribute

SNAPSHOT_PREFIX = 'octomate: '

CopyError

Bases: RuntimeError

A cp exited nonzero; the message carries its stderr.

Source code in octomate/managers/workspaces/base.py
class CopyError(RuntimeError):
    """A `cp` exited nonzero; the message carries its stderr."""

Workspace dataclass

Bases: ABC

The tree one run happens in, and what becomes of it when the run ends.

Handed out by WorkspaceManager.open and entered by the run that was given it. Knowing where it is costs nothing — the path is the thread's — so a caller has it before anything is forked, which is what lets a runtime be configured with its own working directory before the process that will run there exists.

Two variants, and they differ in exactly three things: where the tree lives, which mirror it is forked from, and whether leaving it keeps or throws away what the run did. Everything else — the fork itself, the branch it lands on, the lock that stops two turns forking it twice — is one mechanism on the manager, and both go through it.

Source code in octomate/managers/workspaces/base.py
@dataclass
class Workspace(ABC):
    """The tree one run happens in, and what becomes of it when the run ends.

    Handed out by `WorkspaceManager.open` and entered by the run that was given it.
    Knowing where it is costs nothing — the path is the thread's — so a caller has
    it before anything is forked, which is what lets a runtime be configured with
    its own working directory before the process that will run there exists.

    Two variants, and they differ in exactly three things: where the tree lives,
    which mirror it is forked from, and whether leaving it keeps or throws away
    what the run did. Everything else — the fork itself, the branch it lands on,
    the lock that stops two turns forking it twice — is one mechanism on the
    manager, and both go through it.
    """

    workspaces: WorkspaceManager
    thread_id: UUID7

    @property
    @abstractmethod
    def path(self) -> Path:
        """Where this run happens. Absolute, because it becomes the working
        directory of a process Octomate did not start in its own, and known
        without touching the disk."""

    @abstractmethod
    async def prepare(self) -> Path:
        """The tree, forked and ready for a run to happen in."""

    async def __aenter__(self) -> Self:
        await self.prepare()
        return self

    @abstractmethod
    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        traceback: TracebackType | None,
    ) -> None:
        """End the run's claim on the tree, however the run ended."""

workspaces instance-attribute

workspaces: WorkspaceManager

thread_id instance-attribute

thread_id: UUID7

path abstractmethod property

path: Path

Where this run happens. Absolute, because it becomes the working directory of a process Octomate did not start in its own, and known without touching the disk.

prepare abstractmethod async

prepare() -> Path

The tree, forked and ready for a run to happen in.

Source code in octomate/managers/workspaces/base.py
@abstractmethod
async def prepare(self) -> Path:
    """The tree, forked and ready for a run to happen in."""

ProjectWorkspace dataclass

Bases: Workspace

This thread's own checkout of its project, at workspaces_dir/<thread_id>.

A thread runs here rather than in project.root, so two threads on one project cannot walk over each other's uncommitted work — and the root stays the person's, untouched by anything an agent does.

Source code in octomate/managers/workspaces/base.py
@dataclass
class ProjectWorkspace(Workspace):
    """This thread's own checkout of its project, at ``workspaces_dir/<thread_id>``.

    A thread runs here rather than in `project.root`, so two threads on one project
    cannot walk over each other's uncommitted work — and the root stays the
    person's, untouched by anything an agent does.
    """

    project: Project

    @property
    def path(self) -> Path:
        return self.workspaces.path(self.thread_id)

    async def prepare(self, ref: str | None = None) -> Path:
        """This thread's checkout, resumed as it stands or forked when it is gone.

        Only a thread's first turn does any of that. A workspace that exists is
        resumed into as it stands, and its mirror is not synced for it either: a
        workspace is not re-made from the mirror once it is there, so a fetch would
        buy the turn nothing and would couple continuing a thread to the upstream
        being reachable. `materialize` answers an existing workspace anyway; the
        check is here to keep the sync in front of it from running — and to stamp
        the workspace, which is what tells the sweep this thread is still running.
        """
        workspace = self.workspaces.existing(self.thread_id)
        if workspace is not None:
            return workspace
        mirror = await self.workspaces.mirrors.sync(self.project)
        return await self.workspaces.materialize(self, mirror, ref)

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        traceback: TracebackType | None,
    ) -> None:
        """Nothing. A project's workspace outlives the run that used it — that is
        what makes the next turn a resume — and what the turn owes is a `save`,
        which the graph does once for the turn rather than once per agent that
        ran in it."""

project instance-attribute

project: Project

path property

path: Path

prepare async

prepare(ref: str | None = None) -> Path

This thread's checkout, resumed as it stands or forked when it is gone.

Only a thread's first turn does any of that. A workspace that exists is resumed into as it stands, and its mirror is not synced for it either: a workspace is not re-made from the mirror once it is there, so a fetch would buy the turn nothing and would couple continuing a thread to the upstream being reachable. materialize answers an existing workspace anyway; the check is here to keep the sync in front of it from running — and to stamp the workspace, which is what tells the sweep this thread is still running.

Source code in octomate/managers/workspaces/base.py
async def prepare(self, ref: str | None = None) -> Path:
    """This thread's checkout, resumed as it stands or forked when it is gone.

    Only a thread's first turn does any of that. A workspace that exists is
    resumed into as it stands, and its mirror is not synced for it either: a
    workspace is not re-made from the mirror once it is there, so a fetch would
    buy the turn nothing and would couple continuing a thread to the upstream
    being reachable. `materialize` answers an existing workspace anyway; the
    check is here to keep the sync in front of it from running — and to stamp
    the workspace, which is what tells the sweep this thread is still running.
    """
    workspace = self.workspaces.existing(self.thread_id)
    if workspace is not None:
        return workspace
    mirror = await self.workspaces.mirrors.sync(self.project)
    return await self.workspaces.materialize(self, mirror, ref)

ChatWorkspace dataclass

Bases: Workspace

The tree a thread in no project runs in: a fork of the empty repository, at workspaces_dir/chat/<thread_id>, gone when the run ends.

A workspace rather than a bare directory, so a run in no project is a run like any other — it may write, unpack, and script inside its own tree, bounded by the same directory every run is bounded by. Forking nothing is what makes that affordable: an empty repository costs a git init's worth of disk, and binding to a project stops meaning "you may write now" and starts meaning "your work is kept now".

Under chat/ rather than among the project workspaces, because up there the directory's existence is the answer to "is this thread's workspace already forked": a chat fork left at path(thread_id) would be resumed into by the same thread's first project turn, which would then run in an empty repository instead of the project's code.

The path is the thread's and does not change between turns, even though what is in it does. Both runtimes key a resumable session by the directory it ran in — Claude's --resume answers "no conversation found" from anywhere else — so a tree named after the run rather than the thread would be a run that cannot continue the conversation it belongs to.

Source code in octomate/managers/workspaces/base.py
@dataclass
class ChatWorkspace(Workspace):
    """The tree a thread in no project runs in: a fork of the empty repository, at
    ``workspaces_dir/chat/<thread_id>``, gone when the run ends.

    A workspace rather than a bare directory, so a run in no project is a run like
    any other — it may write, unpack, and script inside its own tree, bounded by
    the same directory every run is bounded by. Forking nothing is what makes that
    affordable: an empty repository costs a `git init`'s worth of disk, and binding
    to a project stops meaning "you may write now" and starts meaning "your work is
    kept now".

    Under `chat/` rather than among the project workspaces, because up there the
    directory's existence is the answer to "is this thread's workspace already
    forked": a chat fork left at `path(thread_id)` would be resumed into by the same
    thread's first project turn, which would then run in an empty repository instead
    of the project's code.

    The path is the thread's and does not change between turns, even though what is
    in it does. Both runtimes key a resumable session by the directory it ran in —
    Claude's `--resume` answers "no conversation found" from anywhere else — so a
    tree named after the run rather than the thread would be a run that cannot
    continue the conversation it belongs to.
    """

    @property
    def path(self) -> Path:
        return (self.workspaces.workspaces_dir / "chat" / str(self.thread_id)).resolve()

    async def prepare(self) -> Path:
        """An empty tree of this run's own, forked from the blank mirror.

        Claimed first, which is what throws away whatever a killed run left at this
        path: nothing resumes a chat workspace, so anything there when nobody is in
        it belongs to a run that is over.

        Nothing here is ever saved. The blank mirror would otherwise grow a ref per
        conversation for work nobody intends to keep, which is the opposite of what
        a mirror is for.
        """
        await self.workspaces.claim(self)
        # The mirror of no project, which is what this thread is in.
        mirror = await self.workspaces.mirrors.create()
        return await self.workspaces.materialize(self, mirror)

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        traceback: TracebackType | None,
    ) -> None:
        """Throw the tree away. In `__aexit__` rather than after the run's last
        event, so a turn that raised does not leave its files behind — they are
        exactly the files nothing will ever ask for again."""
        await self.workspaces.discard(self)

path property

path: Path

prepare async

prepare() -> Path

An empty tree of this run's own, forked from the blank mirror.

Claimed first, which is what throws away whatever a killed run left at this path: nothing resumes a chat workspace, so anything there when nobody is in it belongs to a run that is over.

Nothing here is ever saved. The blank mirror would otherwise grow a ref per conversation for work nobody intends to keep, which is the opposite of what a mirror is for.

Source code in octomate/managers/workspaces/base.py
async def prepare(self) -> Path:
    """An empty tree of this run's own, forked from the blank mirror.

    Claimed first, which is what throws away whatever a killed run left at this
    path: nothing resumes a chat workspace, so anything there when nobody is in
    it belongs to a run that is over.

    Nothing here is ever saved. The blank mirror would otherwise grow a ref per
    conversation for work nobody intends to keep, which is the opposite of what
    a mirror is for.
    """
    await self.workspaces.claim(self)
    # The mirror of no project, which is what this thread is in.
    mirror = await self.workspaces.mirrors.create()
    return await self.workspaces.materialize(self, mirror)

WorkspaceManager

Bases: Manager, Locks[UUID7]

Every project-bound thread's workspace: a fork of the project's mirror at workspaces_dir/<thread_id>, checked out on the thread's own branch and released when the disk is wanted back.

A fork is a complete, independent repository — its own .git, its own object store — made by cloning the mirror's extents where the filesystem does copy-on-write and by git clone where it does not. Never a git worktree, whose .git is a file pointing back at the mirror: commits in one write outside the workspace, which an agent's write sandbox refuses.

A workspace is a cache rather than the only copy: each turn's tree is snapshotted and pushed to the mirror as the thread's own ref, so releasing one is a disk decision and resuming lays that ref back over a fresh fork. What the agent did with git in there is the agent's — nothing here commits.

A thread in no project gets one too, at workspaces_dir/chat/<thread_id>, forked from the blank mirror and thrown away when the run that made it ends. Same mechanism, opposite ending: there is no project to keep it for, so binding to one is what turns a run's tree into work that outlives the run.

The whole lifecycle is here. A turn asks open for the workspace its thread runs in and enters it, which is what forks the tree and what ends it; the turn itself owes one more thing, save, which the graph does after the run — or dismiss, once the agent has said the thread's work is done. Those need the registry to say which project a thread is in and the mirrors to say where that project's is, which is why this holds both — which fork a thread gets is exactly what the registry says about it, so knowing what binds one is not a dependency it is reaching for. The layer below stays free of it: every method that touches git takes a mirror path and answers for a directory.

Source code in octomate/managers/workspaces/base.py
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
class WorkspaceManager(Manager, Locks[UUID7]):
    """Every project-bound thread's workspace: a fork of the project's mirror at
    ``workspaces_dir/<thread_id>``, checked out on the thread's own branch and
    released when the disk is wanted back.

    A fork is a complete, independent repository — its own `.git`, its own object
    store — made by cloning the mirror's extents where the filesystem does
    copy-on-write and by `git clone` where it does not. Never a `git worktree`,
    whose `.git` is a file pointing back at the mirror: commits in one write
    outside the workspace, which an agent's write sandbox refuses.

    A workspace is a cache rather than the only copy: each turn's tree is
    snapshotted and pushed to the mirror as the thread's own ref, so releasing one
    is a disk decision and resuming lays that ref back over a fresh fork. What the
    agent did with git in there is the agent's — nothing here commits.

    A thread in no project gets one too, at ``workspaces_dir/chat/<thread_id>``,
    forked from the blank mirror and thrown away when the run that made it ends.
    Same mechanism, opposite ending: there is no project to keep it for, so binding
    to one is what turns a run's tree into work that outlives the run.

    The whole lifecycle is here. A turn asks `open` for the workspace its thread
    runs in and enters it, which is what forks the tree and what ends it; the turn
    itself owes one more thing, `save`, which the graph does after the run — or
    `dismiss`, once the agent has said the thread's work is done. Those
    need the registry to say which project a thread is in and the mirrors to say
    where that project's is, which is why this holds both — which fork a thread gets
    is exactly what the registry says about it, so knowing what binds one is not a
    dependency it is reaching for. The layer below stays free of it: every method
    that touches git takes a mirror path and answers for a directory.
    """

    def __init__(
        self,
        *,
        projects: ProjectManager | None = None,
        mirrors: MirrorManager | None = None,
        config: WorkspacesConfig | None = None,
        workspaces_dir: Path = Path(".octomate/workspaces"),
    ) -> None:
        # A workspace is a project-bound thread's, so the registry that says which
        # project and the mirrors that say where are what this works through. Its
        # host replaces both with its own when it takes ownership — a second
        # registry would resolve no thread to a project, and a second mirror
        # manager would keep its own sync locks — so a manager built without them
        # is one nobody has handed to a host yet.
        self.projects = projects if projects is not None else ProjectManager()
        self.mirrors = mirrors if mirrors is not None else MirrorManager()
        self.config = config if config is not None else WorkspacesConfig()
        # Resolved once, here: a workspace path leaves this manager to become a
        # subprocess's cwd and a `GIT_INDEX_FILE` beside it, and the default is
        # relative to wherever Octomate was started. Pinning it at construction
        # also stops the root moving if anything ever chdirs underneath.
        self.workspaces_dir = workspaces_dir.resolve()
        self.reflink: str | None = None
        self.probed = False
        # How many runs are in each thread's chat workspace right now. The tree
        # is the run's, and two overlapping runs of one conversation share it —
        # this is what stops the first to finish taking it from the second.
        self.chatting: dict[UUID7, int] = {}

    @property
    def identity(self) -> GitIdentity:
        """Who the machine is when it writes a snapshot — the `mirrors:` block's,
        read rather than copied, so a turn's work and a folder sync into a mirror
        carry one name and cannot drift into two."""
        return self.mirrors.config.identity

    def path(self, thread_id: UUID7) -> Path:
        """Where this thread's workspace lives. Absolute, because it becomes the
        working directory of a process Octomate did not start in its own."""
        return (self.workspaces_dir / str(thread_id)).resolve()

    def existing(self, thread_id: UUID7) -> Path | None:
        """This thread's workspace if it is already forked, else None — what saves a
        resumed turn the fork and the sync in front of it.

        Finding one stamps it as used, so the sweep measures idleness from the turn
        that just asked rather than from the last one that wrote something.
        """
        path = self.path(thread_id)
        if not path.is_dir():
            return None
        os.utime(path)
        return path

    @overload
    def open(self, thread_id: UUID7, project: Project) -> ProjectWorkspace: ...

    @overload
    def open(self, thread_id: UUID7, project: None) -> ChatWorkspace: ...

    @overload
    def open(self, thread_id: UUID7, project: Project | None) -> Workspace: ...

    def open(self, thread_id: UUID7, project: Project | None) -> Workspace:
        """The workspace a run in this thread happens in, ready to be entered.

        Which one it is, is the whole of what a project decides about a run: a
        thread in one gets a checkout of it that outlives the turn, and a thread in
        none gets an empty tree that does not. Nothing else about the two runs
        differs — both may write where they are, and both are bounded by it.

        `project` is passed rather than looked up because the caller has already
        asked, and the answer decides more than the directory. No disk is touched
        here: the path is settled, so a runtime can be told where it will run before
        anything is forked, and the fork happens when the run enters it.

        A thread in no project used to run in the agent's configured `cwd`, which
        defaults to `"."`: on a server, Octomate's own install directory.
        """
        if project is None:
            return ChatWorkspace(self, thread_id)
        return ProjectWorkspace(self, thread_id, project)

    async def claim(self, workspace: ChatWorkspace) -> None:
        """Count this run into `workspace`, and clear whatever the last one left.

        Counted, because a mid-run follow-up starts its turn before the turn it
        supersedes has finished losing its own: both runs are in this tree, and the
        first to leave must not take the floor out from under the second. They share
        it while they overlap, which is what two runs of one conversation are
        entitled to.

        A tree nobody is in belongs to a run that is over — one that was killed
        before it could discard — so it goes, and this run forks its own. That is
        also why a chat workspace needs no staging directory to be forked through:
        nothing ever resumes into one, so a fork that died half-made is thrown away
        here rather than mistaken for a tree.
        """
        # `holders` is the count after this run joined, and the number to read a
        # chat workspace's life by: it is what a `discard` has to bring back to
        # zero, so a tree still on disk long after its run is one whose claim and
        # discard did not pair.
        with workspace_logfire.span(
            "workspace.claim", thread_id=str(workspace.thread_id)
        ) as span:
            async with self.lock(workspace.thread_id):
                holders = self.chatting.get(workspace.thread_id, 0)
                if holders == 0 and workspace.path.exists():
                    # What a run killed before it could discard left behind.
                    span.set_attribute("abandoned_tree", True)
                    await asyncio.to_thread(shutil.rmtree, workspace.path)
                self.chatting[workspace.thread_id] = holders + 1
                span.set_attribute("holders", holders + 1)

    async def discard(self, workspace: ChatWorkspace) -> None:
        """Give this tree back to the disk, once no run is still in it."""
        with workspace_logfire.span(
            "workspace.discard", thread_id=str(workspace.thread_id)
        ) as span:
            async with self.lock(workspace.thread_id):
                holders = self.chatting.get(workspace.thread_id, 0) - 1
                span.set_attribute("holders", max(holders, 0))
                if holders > 0:
                    # An overlapping run of the same conversation is still in this
                    # tree, so the tree stays and its life is that run's to end.
                    span.set_attribute("removed", False)
                    self.chatting[workspace.thread_id] = holders
                    return
                self.chatting.pop(workspace.thread_id, None)
                span.set_attribute("removed", workspace.path.is_dir())
                if workspace.path.is_dir():
                    await asyncio.to_thread(shutil.rmtree, workspace.path)

    async def detect(self) -> str | None:
        """The `cp` flag that forks without copying bytes on this host, or None
        where nothing does and workspaces are cloned instead.

        Probed once, by actually cloning a file where workspaces land: the answer
        belongs to that filesystem rather than to the platform, and it cannot
        change under a running host. A host with no copy-on-write filesystem is
        not a failure — it pays a full tree per workspace and works.
        """
        if self.probed:
            return self.reflink
        self.workspaces_dir.mkdir(parents=True, exist_ok=True)
        probe = Path(tempfile.mkdtemp(dir=self.workspaces_dir))
        try:
            source = probe / "source"
            source.write_bytes(b"probe")
            for flag in REFLINK_FLAGS:
                try:
                    await copy(source, probe / "target", flag)
                except CopyError:
                    continue
                self.reflink = flag
                break
        finally:
            shutil.rmtree(probe)
        self.probed = True
        logger.info(
            "thread workspaces fork by %s",
            f"cp -a {self.reflink}" if self.reflink else "git clone",
        )
        return self.reflink

    async def materialize(
        self,
        workspace: Workspace,
        mirror: Path,
        ref: str | None = None,
    ) -> Path:
        """Fork `mirror` into `workspace`, and answer the tree.

        The one place a workspace is made, whichever kind it is: a project's
        checkout and a chat thread's empty tree are the same fork of the same
        shape, differing only in what they were forked from. Both land on the
        thread's own branch, and neither carries the mirror's `origin` unless the
        mirror has a real one to inherit.

        A workspace that already exists is answered as it stands — a project
        thread's live tree, uncommitted work included, where forking again would
        throw that work away, and for a chat thread the tree an overlapping run of
        the same conversation is already in. Serialized per thread, so two turns
        arriving together materialize once.

        The fork is made beside the workspace and moved into place, so a workspace
        only ever exists whole: a rename within one directory is atomic, where a
        copy the machine dies in the middle of would leave half a tree that every
        later turn reads as this thread's own. That is what makes "the directory is
        there" mean "the workspace is there", here and for a caller that asks the
        same question. A fork that merely fails is removed the same way, before the
        error travels on.

        It is installed before it is moved into place, so a workspace is ready to
        run the moment it exists. A copy inherits the mirror's installed trees and
        that costs nothing; a clone carries no untracked file, so an ext4 host
        builds its own out of the store the mirror already warmed.
        """
        path = workspace.path
        # Outside the lock, so a turn waiting on another turn's fork is time this
        # span accounts for. From the far end the wait and a slow fork look the
        # same, and they want opposite fixes.
        with workspace_logfire.span(
            "workspace.materialize",
            thread_id=str(workspace.thread_id),
            kind=type(workspace).__name__,
            path=str(path),
        ) as span:
            async with self.lock(workspace.thread_id):
                if path.is_dir():
                    # Where every turn after a thread's first arrives. Saying so is
                    # what leaves the forks findable among them.
                    span.set_attribute("forked", False)
                    return path
                span.set_attribute("forked", True)
                path.parent.mkdir(parents=True, exist_ok=True)
                flag = await self.detect()
                # The one thing about a fork that another host would do differently,
                # so a trace of one says it before anything else.
                span.set_attribute("mechanism", f"cp -a {flag}" if flag else "clone")
                # Hidden, and named after the thread, so it is never mistaken for a
                # workspace and the next fork of this thread reclaims whatever a
                # killed one left — the case the cleanup below cannot reach.
                staging = path.with_name(f".{workspace.thread_id}.forking")
                if staging.exists():
                    shutil.rmtree(staging)
                try:
                    if flag is not None:
                        await copy(mirror, staging, flag)
                    else:
                        await run_git("clone", str(mirror), str(staging))
                    await self.inherit_remotes(mirror, staging)
                    await self.discard_octomate_refs(staging)
                    await self.checkout(workspace.thread_id, mirror, staging, ref)
                    await install(staging)
                    staging.rename(path)
                    # `cp -a` preserves the mirror's timestamps and a rename does not
                    # refresh them, so without this a fork inherits an mtime that can
                    # already be past the idle window — a workspace made seconds ago,
                    # born ready for the sweep.
                    os.utime(path)
                except BaseException:
                    shutil.rmtree(staging, ignore_errors=True)
                    raise
            return path

    async def inherit_remotes(self, mirror: Path, workspace: Path) -> None:
        """Give the fork the mirror's own `origin`, or no remote where the mirror
        has none.

        Otherwise where a workspace points depends on how it was forked: a copy
        inherits the mirror's remotes, and a clone gets an `origin` pointing at the
        mirror itself. An agent asked to push its branch would find a different
        answer on a copy-on-write host than on one that clones, which is not
        something the host's filesystem gets to decide.

        The mirror's `origin` is the project's real upstream, and is what an agent
        means by pushing — a branch it named for the job, on the repository people
        will read it in. Nothing Octomate does travels that way: a turn is saved by
        pushing to the mirror's path, never through a remote name.

        A folder project's mirror has no upstream to inherit, and neither does its
        fork. There is nowhere to push a directory, and saying so by having no
        remote is clearer to an agent than an `origin` that fails when it is used.
        """
        if "origin" in (await run_git("remote", cwd=workspace)).split():
            await run_git("remote", "remove", "origin", cwd=workspace)
        if "origin" not in (await run_git("remote", cwd=mirror)).split():
            return
        url = await run_git("remote", "get-url", "origin", cwd=mirror)
        await run_git("remote", "add", "origin", url.strip(), cwd=workspace)

    async def discard_octomate_refs(self, workspace: Path) -> None:
        """Take Octomate's own refs out of a fresh fork, however it was made.

        `git clone` asks for `refs/heads/*` and tags, so a cloned fork never had
        them. `cp` does not know what git is: it brings `.git` across whole, and
        with it every other thread's `refs/octomate/threads/*` — their snapshots
        sitting in a workspace that was never told about them, out of `git branch`
        but not out of `git log --all`. Which of the two a host does is `detect`'s
        answer about its filesystem, and what one thread can read of another is not
        something the filesystem gets to decide.

        The whole namespace rather than the foreign entries: a mirror that somehow
        carried a `refs/octomate/saved` would hand a fresh fork a workspace that
        reads as already saved, which the sweep answers by reclaiming work that was
        never pushed. Dropping this thread's own along with the rest costs nothing —
        the mirror is where it lives, and `checkout` fetches it back next.
        """
        listing = await run_git(
            "for-each-ref", "--format=%(refname)", "refs/octomate/", cwd=workspace
        )
        for ref in listing.split():
            await run_git("update-ref", "-d", ref, cwd=workspace)

    async def checkout(
        self,
        thread_id: UUID7,
        mirror: Path,
        workspace: Path,
        ref: str | None = None,
    ) -> None:
        """Put a freshly forked `workspace` back the way this thread's last turn left
        it, when `mirror` is keeping a snapshot of it.

        A thread whose workspace was pruned resumes here, and resumes into the
        state it was interrupted in rather than a tidied version of it: the branch
        goes back to where the agent's HEAD was, which is the snapshot's parent,
        and the tree it could see is laid down over that from the snapshot itself.
        The index then drops back to HEAD, so work the agent had not committed
        reads as uncommitted rather than as staged.

        A thread forking for the first time starts on its own branch, where the
        mirror's branch is — or where `ref` names, when the bind asked for one.
        The default branch is the wrong answer often enough: continuing someone's
        feature branch, reproducing against a tag, working from a PR head.

        `ref` is only consulted by a first fork. A thread whose workspace was
        pruned resumes into its own snapshot, which is where its work is, and no
        starting point competes with that.
        """
        saved = thread_ref(thread_id)
        # The fork's two endings, and the only place that knows which this is: a
        # thread starting, or one coming back to work a sweep reclaimed the tree
        # of. `resumed` is what separates them in a trace, and the snapshot it
        # came back from is what makes the tree it landed on checkable.
        with workspace_logfire.span(
            "workspace.checkout", thread_id=str(thread_id), ref=ref
        ) as span:
            if not await run_git("ls-remote", str(mirror), saved):
                span.set_attribute("resumed", False)
                branch = thread_branch(thread_id)
                if ref is None:
                    await run_git("checkout", "-b", branch, cwd=workspace)
                    return
                # Fetched from the mirror rather than named directly: a clone's
                # remote-tracking refs are removed with the `origin` `inherit_remotes`
                # replaces, so the only commit a fork is certain to resolve is the one
                # it was left on. A ref the mirror does not have fails here, which is
                # what the caller has to be able to report.
                await run_git("fetch", str(mirror), ref, cwd=workspace)
                await run_git("checkout", "-b", branch, "FETCH_HEAD", cwd=workspace)
                return
            span.set_attribute("resumed", True)
            # Every fork arrives without it, whichever way it was made, so this is how
            # the workspace comes to have it at all.
            await run_git("fetch", str(mirror), f"+{saved}:{saved}", cwd=workspace)
            subject = await run_git("show", "-s", "--format=%s", saved, cwd=workspace)
            branch = subject.strip().removeprefix(SNAPSHOT_PREFIX)
            span.set_attribute("branch", branch)
            span.set_attribute(
                "snapshot",
                (await run_git("rev-parse", saved, cwd=workspace)).strip(),
            )
            head = (
                ("--detach", f"{saved}^")
                if branch == "HEAD"
                else ("-B", branch, f"{saved}^")
            )
            await run_git("checkout", *head, cwd=workspace)
            await run_git("read-tree", "-u", "--reset", saved, cwd=workspace)
            await run_git("reset", cwd=workspace)
            # A restored workspace is the mirror's snapshot by construction, and says
            # so: the ref that records it was local to the workspace this one replaces,
            # and without this a resume that never reached its next save would look
            # like unsaved work forever, which is a workspace that cannot be reclaimed.
            await run_git("update-ref", SAVED_REF, saved, cwd=workspace)

    async def snapshot(self, path: Path) -> str:
        """A commit holding this workspace exactly as it stands — staged, unstaged
        and untracked alike — made without disturbing it.

        Built rather than committed, so restoring a workspace is all this costs
        the repository inside it: HEAD stays where the agent left it, its branch
        carries the agent's commits and no others, and whatever the agent was in
        the middle of is not finished on its behalf. Resolving that history is the
        agent's job; putting the tree back is this one.

        The parent is HEAD, so the branch's own history comes along, and the
        snapshot belongs to no branch — reachable only from the thread's ref, the
        way a stash is reachable only from `refs/stash`. Its message carries the
        branch, which is the one thing a tree and a parent cannot say.

        The index it is written through is a copy of the workspace's, which keeps
        the real one untouched and carries git's stat cache with it, so staging
        the tree compares timestamps instead of rehashing every file.
        """
        index = path / ".git" / "octomate-snapshot-index"
        await asyncio.to_thread(shutil.copyfile, path / ".git" / "index", index)
        env = {"GIT_INDEX_FILE": str(index)}
        await run_git("add", "-A", cwd=path, env=env)
        tree = (await run_git("write-tree", cwd=path, env=env)).strip()
        branch = (await run_git("rev-parse", "--abbrev-ref", "HEAD", cwd=path)).strip()
        commit = await run_git(
            *self.identity.commit_flags,
            "commit-tree",
            tree,
            "-p",
            "HEAD",
            "-m",
            f"{SNAPSHOT_PREFIX}{branch}",
            cwd=path,
        )
        return commit.strip()

    async def save(self, thread: Thread) -> None:
        """Leave this thread's finished turn where losing its workspace cannot cost
        it: a snapshot of the whole tree, pushed to the project's mirror as the
        thread's own ref.

        Snapshot *and* push. A fork owns its objects outright — nothing in a
        workspace survives its removal — so without the push the workspace is the
        only copy of what the turn did, and reclaiming it would be losing work
        rather than evicting a cache. That is the whole of what makes a workspace
        disposable, and why the sweep is a disk decision.

        A thread with no workspace — in no project, or one whose turn never forked
        one — has nothing to leave. Nothing the agent asks for or is told about
        either: it knows the directory it was given, not that the directory is a
        fork, and not that the sweep will take it back.

        Forced, because the ref records what the workspace holds rather than a
        history to protect: an amend or a reset inside the workspace moves the ref
        with it. The mirror is the only place this ever pushes; nothing here
        reaches the upstream, which sees a thread's work when a person asks for a
        pull request and not before.

        A failure is logged rather than raised. The turn has already happened and
        already answered; failing it now would report a delivered answer as an
        error, and the next turn saves the same work again.
        """
        # The turn's work leaving the workspace. Until this succeeds the fork is
        # the only copy, so `saved` — which is what the sweep reads — is the
        # attribute to look at when a workspace nobody can reclaim turns up.
        with workspace_logfire.span(
            "workspace.save", thread_id=str(thread.id), saved=False
        ) as span:
            project = await self.projects.of(thread)
            path = self.path(thread.id)
            span.set_attribute("project", project.name if project else None)
            if project is None or not path.is_dir():
                return
            try:
                async with self.lock(thread.id):
                    snapshot = await self.snapshot(path)
                    await run_git(
                        "push",
                        "--force",
                        str(self.mirrors.path(project)),
                        f"{snapshot}:{thread_ref(thread.id)}",
                        cwd=path,
                    )
                    # Only now, and only here: this is what tells the sweep that
                    # letting this workspace go costs a resume rather than the work.
                    await run_git("update-ref", SAVED_REF, snapshot, cwd=path)
                span.set_attribute("snapshot", snapshot)
                span.set_attribute("saved", True)
            except (GitCommandError, OSError):
                logger.exception(
                    "the workspace for thread %s could not be saved; its work is only "
                    "in %s until a later turn saves it",
                    thread.id,
                    path,
                )

    async def saved(self, path: Path) -> bool:
        """Whether the mirror already holds everything in this workspace: the same
        tree, on the same commit, as the last push carried.

        The question the sweep has to answer before reclaiming anything, and one
        `git status` cannot answer any more — nothing here commits, so a saved
        workspace is a dirty workspace and being dirty says nothing. Snapshotting
        again and comparing does say it. A push that failed leaves a workspace
        that looks saved from the outside, which is why what it is compared
        against is a ref the push itself writes.
        """
        pushed = await run_git(
            "for-each-ref", "--format=%(objectname)", SAVED_REF, cwd=path
        )
        if not pushed.strip():
            return False
        state = ("show", "-s", "--format=%T %P")
        return await run_git(*state, pushed.strip(), cwd=path) == await run_git(
            *state, await self.snapshot(path), cwd=path
        )

    async def prune(self, idle: float) -> list[UUID7]:
        """Release every workspace nothing has used for `idle` seconds, and answer
        with the threads that lost theirs.

        Eviction, not a decision about whether a thread is over: abandonment is
        unobservable — a thread parked on a question and one silently dropped look
        identical — so rather than guessing, being wrong here costs the next turn a
        fork instead of costing anyone their work.

        Which is only true of a workspace the mirror already has, so one holding
        anything unsaved is kept and said out loud. That covers a turn still
        running, too: it has changed something since its last turn was saved,
        almost by definition.
        """
        released: list[UUID7] = []
        # Three counts, because "why is that workspace still there" has three
        # answers and the disk cannot tell them apart: nothing has been idle long
        # enough, or something is idle and holding work the mirror never got.
        kept_busy = kept_unsaved = 0
        with workspace_logfire.span("workspace.prune", idle=idle) as span:
            if not self.workspaces_dir.is_dir():
                return released
            for path in sorted(self.workspaces_dir.iterdir()):
                try:
                    thread_id = uuid.UUID(path.name)
                except ValueError:
                    # A staging directory, a probe, or `chat/`: none of them is a
                    # workspace this may reclaim. A chat workspace is its run's to
                    # throw away, and is not kept anywhere the sweep could restore it
                    # from.
                    continue
                if time.time() - path.stat().st_mtime < idle:
                    kept_busy += 1
                    continue
                if not await self.saved(path):
                    kept_unsaved += 1
                    logger.warning(
                        "the workspace for thread %s is idle but holds work the "
                        "mirror does not have; keeping it",
                        thread_id,
                    )
                    continue
                await self.release(thread_id)
                released.append(thread_id)
            span.set_attribute("released", len(released))
            span.set_attribute("kept_busy", kept_busy)
            span.set_attribute("kept_unsaved", kept_unsaved)
            if released:
                logger.info("released %d idle workspace(s)", len(released))
            return released

    async def sweep(self) -> None:
        """Prune on the interval, for as long as the host runs.

        A sweep that fails is logged and the next one still happens: reclaiming
        disk is maintenance, and a host that stops serving because it could not
        tidy up has the priority backwards.
        """
        while True:
            await asyncio.sleep(self.config.sweep_interval)
            try:
                await self.prune(self.config.idle_window)
            except Exception:
                logger.exception("the workspace sweep failed")

    async def carry(self, source: Thread, thread: Thread) -> None:
        """Give `thread`, landed in `source`'s project, a workspace holding the work
        as `source` left it — what a conversation moving to another thread takes of
        its work — and fork it.

        Saved first: the source's last turn may not have been, and what it left
        uncommitted has to travel as uncommitted. The snapshot is filed under
        `thread` too, so its first fork resumes into it the way a pruned workspace
        does; on a branch of its own when the source was on its own, since two
        threads pushing one branch would overwrite each other. A source that never
        forked a workspace has no snapshot, and `thread` forks the project fresh.

        Ignored files are copied across, an `.env` among them, since a snapshot
        never holds them. Ignored directories are not: they are dependencies and
        build output, which the fork's install and the next build make again.
        """
        project = await self.projects.of(source)
        if project is None:
            return
        await self.save(source)
        mirror = self.mirrors.path(project)
        saved = thread_ref(source.id)
        if await run_git("ls-remote", str(mirror), saved):
            subject = await run_git("show", "-s", "--format=%s", saved, cwd=mirror)
            filed = saved
            if subject.strip() == f"{SNAPSHOT_PREFIX}{thread_branch(source.id)}":
                filed = await run_git(
                    *self.identity.commit_flags,
                    "commit-tree",
                    f"{saved}^{{tree}}",
                    "-p",
                    f"{saved}^",
                    "-m",
                    f"{SNAPSHOT_PREFIX}{thread_branch(thread.id)}",
                    cwd=mirror,
                )
            await run_git(
                "update-ref", thread_ref(thread.id), filed.strip(), cwd=mirror
            )
        landed = await self.open(thread.id, project).prepare()
        origin = self.existing(source.id)
        if origin is None:
            return
        ignored = await run_git(
            "ls-files",
            "--others",
            "--ignored",
            "--exclude-standard",
            "--directory",
            "-z",
            cwd=origin,
        )
        for name in ignored.split("\0"):
            if not name or name.endswith("/") or (landed / name).exists():
                continue
            (landed / name).parent.mkdir(parents=True, exist_ok=True)
            await asyncio.to_thread(
                shutil.copy2, origin / name, landed / name, follow_symlinks=False
            )

    async def dismiss(self, thread: Thread) -> bool:
        """Release this thread's workspace on its agent's word that the work in it
        is done — the one release that is asked for rather than swept up — and
        answer whether it went.

        Saved first, and released only when the save took: the sweep's rule, that
        a workspace holding work the mirror has not seen is never thrown away,
        does not bend for being asked. So what this costs is what a prune costs,
        a fork on the thread's next turn with the ref laid back over it.
        """
        with workspace_logfire.span(
            "workspace.dismiss", thread_id=str(thread.id), released=False
        ) as span:
            await self.save(thread)
            path = self.path(thread.id)
            if not path.is_dir():
                return False
            if not await self.saved(path):
                logger.warning(
                    "the workspace for thread %s was dismissed but holds work the "
                    "mirror does not have; keeping it",
                    thread.id,
                )
                return False
            await self.release(thread.id)
            span.set_attribute("released", True)
            return True

    async def release(self, thread_id: UUID7) -> None:
        """Give this thread's workspace back to the disk, or nothing when it is
        already gone — pruning and releasing a thread that never had one both
        arrive here, and a workspace is a cache either way.

        The mirror is untouched: a copied fork owns its objects outright, and a
        cloned one holds hard links, where unlinking a link leaves the file. In
        a thread, off the loop: a released tree is as big as the project.
        """
        path = self.path(thread_id)
        # The end of a tree, and the span a resume is read against: a `materialize`
        # with `resumed` true is this one's other half, however long after.
        with workspace_logfire.span(
            "workspace.release", thread_id=str(thread_id)
        ) as span:
            async with self.lock(thread_id):
                # False where a thread never had one, or a sweep and a caller both
                # reached for the same tree — neither is a failure, and both would
                # otherwise read as one released workspace too many.
                span.set_attribute("removed", path.is_dir())
                if path.is_dir():
                    await asyncio.to_thread(shutil.rmtree, path)

projects instance-attribute

projects = (
    projects if projects is not None else ProjectManager()
)

mirrors instance-attribute

mirrors = (
    mirrors if mirrors is not None else MirrorManager()
)

config instance-attribute

config = (
    config if config is not None else WorkspacesConfig()
)

workspaces_dir instance-attribute

workspaces_dir = workspaces_dir.resolve()
reflink: str | None = None

probed instance-attribute

probed = False

chatting instance-attribute

chatting: dict[UUID7, int] = {}

identity property

identity: GitIdentity

Who the machine is when it writes a snapshot — the mirrors: block's, read rather than copied, so a turn's work and a folder sync into a mirror carry one name and cannot drift into two.

path

path(thread_id: UUID7) -> Path

Where this thread's workspace lives. Absolute, because it becomes the working directory of a process Octomate did not start in its own.

Source code in octomate/managers/workspaces/base.py
def path(self, thread_id: UUID7) -> Path:
    """Where this thread's workspace lives. Absolute, because it becomes the
    working directory of a process Octomate did not start in its own."""
    return (self.workspaces_dir / str(thread_id)).resolve()

existing

existing(thread_id: UUID7) -> Path | None

This thread's workspace if it is already forked, else None — what saves a resumed turn the fork and the sync in front of it.

Finding one stamps it as used, so the sweep measures idleness from the turn that just asked rather than from the last one that wrote something.

Source code in octomate/managers/workspaces/base.py
def existing(self, thread_id: UUID7) -> Path | None:
    """This thread's workspace if it is already forked, else None — what saves a
    resumed turn the fork and the sync in front of it.

    Finding one stamps it as used, so the sweep measures idleness from the turn
    that just asked rather than from the last one that wrote something.
    """
    path = self.path(thread_id)
    if not path.is_dir():
        return None
    os.utime(path)
    return path

open

open(
    thread_id: UUID7, project: Project
) -> ProjectWorkspace
open(thread_id: UUID7, project: None) -> ChatWorkspace
open(
    thread_id: UUID7, project: Project | None
) -> Workspace
open(
    thread_id: UUID7, project: Project | None
) -> Workspace

The workspace a run in this thread happens in, ready to be entered.

Which one it is, is the whole of what a project decides about a run: a thread in one gets a checkout of it that outlives the turn, and a thread in none gets an empty tree that does not. Nothing else about the two runs differs — both may write where they are, and both are bounded by it.

project is passed rather than looked up because the caller has already asked, and the answer decides more than the directory. No disk is touched here: the path is settled, so a runtime can be told where it will run before anything is forked, and the fork happens when the run enters it.

A thread in no project used to run in the agent's configured cwd, which defaults to ".": on a server, Octomate's own install directory.

Source code in octomate/managers/workspaces/base.py
def open(self, thread_id: UUID7, project: Project | None) -> Workspace:
    """The workspace a run in this thread happens in, ready to be entered.

    Which one it is, is the whole of what a project decides about a run: a
    thread in one gets a checkout of it that outlives the turn, and a thread in
    none gets an empty tree that does not. Nothing else about the two runs
    differs — both may write where they are, and both are bounded by it.

    `project` is passed rather than looked up because the caller has already
    asked, and the answer decides more than the directory. No disk is touched
    here: the path is settled, so a runtime can be told where it will run before
    anything is forked, and the fork happens when the run enters it.

    A thread in no project used to run in the agent's configured `cwd`, which
    defaults to `"."`: on a server, Octomate's own install directory.
    """
    if project is None:
        return ChatWorkspace(self, thread_id)
    return ProjectWorkspace(self, thread_id, project)

claim async

claim(workspace: ChatWorkspace) -> None

Count this run into workspace, and clear whatever the last one left.

Counted, because a mid-run follow-up starts its turn before the turn it supersedes has finished losing its own: both runs are in this tree, and the first to leave must not take the floor out from under the second. They share it while they overlap, which is what two runs of one conversation are entitled to.

A tree nobody is in belongs to a run that is over — one that was killed before it could discard — so it goes, and this run forks its own. That is also why a chat workspace needs no staging directory to be forked through: nothing ever resumes into one, so a fork that died half-made is thrown away here rather than mistaken for a tree.

Source code in octomate/managers/workspaces/base.py
async def claim(self, workspace: ChatWorkspace) -> None:
    """Count this run into `workspace`, and clear whatever the last one left.

    Counted, because a mid-run follow-up starts its turn before the turn it
    supersedes has finished losing its own: both runs are in this tree, and the
    first to leave must not take the floor out from under the second. They share
    it while they overlap, which is what two runs of one conversation are
    entitled to.

    A tree nobody is in belongs to a run that is over — one that was killed
    before it could discard — so it goes, and this run forks its own. That is
    also why a chat workspace needs no staging directory to be forked through:
    nothing ever resumes into one, so a fork that died half-made is thrown away
    here rather than mistaken for a tree.
    """
    # `holders` is the count after this run joined, and the number to read a
    # chat workspace's life by: it is what a `discard` has to bring back to
    # zero, so a tree still on disk long after its run is one whose claim and
    # discard did not pair.
    with workspace_logfire.span(
        "workspace.claim", thread_id=str(workspace.thread_id)
    ) as span:
        async with self.lock(workspace.thread_id):
            holders = self.chatting.get(workspace.thread_id, 0)
            if holders == 0 and workspace.path.exists():
                # What a run killed before it could discard left behind.
                span.set_attribute("abandoned_tree", True)
                await asyncio.to_thread(shutil.rmtree, workspace.path)
            self.chatting[workspace.thread_id] = holders + 1
            span.set_attribute("holders", holders + 1)

discard async

discard(workspace: ChatWorkspace) -> None

Give this tree back to the disk, once no run is still in it.

Source code in octomate/managers/workspaces/base.py
async def discard(self, workspace: ChatWorkspace) -> None:
    """Give this tree back to the disk, once no run is still in it."""
    with workspace_logfire.span(
        "workspace.discard", thread_id=str(workspace.thread_id)
    ) as span:
        async with self.lock(workspace.thread_id):
            holders = self.chatting.get(workspace.thread_id, 0) - 1
            span.set_attribute("holders", max(holders, 0))
            if holders > 0:
                # An overlapping run of the same conversation is still in this
                # tree, so the tree stays and its life is that run's to end.
                span.set_attribute("removed", False)
                self.chatting[workspace.thread_id] = holders
                return
            self.chatting.pop(workspace.thread_id, None)
            span.set_attribute("removed", workspace.path.is_dir())
            if workspace.path.is_dir():
                await asyncio.to_thread(shutil.rmtree, workspace.path)

detect async

detect() -> str | None

The cp flag that forks without copying bytes on this host, or None where nothing does and workspaces are cloned instead.

Probed once, by actually cloning a file where workspaces land: the answer belongs to that filesystem rather than to the platform, and it cannot change under a running host. A host with no copy-on-write filesystem is not a failure — it pays a full tree per workspace and works.

Source code in octomate/managers/workspaces/base.py
async def detect(self) -> str | None:
    """The `cp` flag that forks without copying bytes on this host, or None
    where nothing does and workspaces are cloned instead.

    Probed once, by actually cloning a file where workspaces land: the answer
    belongs to that filesystem rather than to the platform, and it cannot
    change under a running host. A host with no copy-on-write filesystem is
    not a failure — it pays a full tree per workspace and works.
    """
    if self.probed:
        return self.reflink
    self.workspaces_dir.mkdir(parents=True, exist_ok=True)
    probe = Path(tempfile.mkdtemp(dir=self.workspaces_dir))
    try:
        source = probe / "source"
        source.write_bytes(b"probe")
        for flag in REFLINK_FLAGS:
            try:
                await copy(source, probe / "target", flag)
            except CopyError:
                continue
            self.reflink = flag
            break
    finally:
        shutil.rmtree(probe)
    self.probed = True
    logger.info(
        "thread workspaces fork by %s",
        f"cp -a {self.reflink}" if self.reflink else "git clone",
    )
    return self.reflink

materialize async

materialize(
    workspace: Workspace,
    mirror: Path,
    ref: str | None = None,
) -> Path

Fork mirror into workspace, and answer the tree.

The one place a workspace is made, whichever kind it is: a project's checkout and a chat thread's empty tree are the same fork of the same shape, differing only in what they were forked from. Both land on the thread's own branch, and neither carries the mirror's origin unless the mirror has a real one to inherit.

A workspace that already exists is answered as it stands — a project thread's live tree, uncommitted work included, where forking again would throw that work away, and for a chat thread the tree an overlapping run of the same conversation is already in. Serialized per thread, so two turns arriving together materialize once.

The fork is made beside the workspace and moved into place, so a workspace only ever exists whole: a rename within one directory is atomic, where a copy the machine dies in the middle of would leave half a tree that every later turn reads as this thread's own. That is what makes "the directory is there" mean "the workspace is there", here and for a caller that asks the same question. A fork that merely fails is removed the same way, before the error travels on.

It is installed before it is moved into place, so a workspace is ready to run the moment it exists. A copy inherits the mirror's installed trees and that costs nothing; a clone carries no untracked file, so an ext4 host builds its own out of the store the mirror already warmed.

Source code in octomate/managers/workspaces/base.py
async def materialize(
    self,
    workspace: Workspace,
    mirror: Path,
    ref: str | None = None,
) -> Path:
    """Fork `mirror` into `workspace`, and answer the tree.

    The one place a workspace is made, whichever kind it is: a project's
    checkout and a chat thread's empty tree are the same fork of the same
    shape, differing only in what they were forked from. Both land on the
    thread's own branch, and neither carries the mirror's `origin` unless the
    mirror has a real one to inherit.

    A workspace that already exists is answered as it stands — a project
    thread's live tree, uncommitted work included, where forking again would
    throw that work away, and for a chat thread the tree an overlapping run of
    the same conversation is already in. Serialized per thread, so two turns
    arriving together materialize once.

    The fork is made beside the workspace and moved into place, so a workspace
    only ever exists whole: a rename within one directory is atomic, where a
    copy the machine dies in the middle of would leave half a tree that every
    later turn reads as this thread's own. That is what makes "the directory is
    there" mean "the workspace is there", here and for a caller that asks the
    same question. A fork that merely fails is removed the same way, before the
    error travels on.

    It is installed before it is moved into place, so a workspace is ready to
    run the moment it exists. A copy inherits the mirror's installed trees and
    that costs nothing; a clone carries no untracked file, so an ext4 host
    builds its own out of the store the mirror already warmed.
    """
    path = workspace.path
    # Outside the lock, so a turn waiting on another turn's fork is time this
    # span accounts for. From the far end the wait and a slow fork look the
    # same, and they want opposite fixes.
    with workspace_logfire.span(
        "workspace.materialize",
        thread_id=str(workspace.thread_id),
        kind=type(workspace).__name__,
        path=str(path),
    ) as span:
        async with self.lock(workspace.thread_id):
            if path.is_dir():
                # Where every turn after a thread's first arrives. Saying so is
                # what leaves the forks findable among them.
                span.set_attribute("forked", False)
                return path
            span.set_attribute("forked", True)
            path.parent.mkdir(parents=True, exist_ok=True)
            flag = await self.detect()
            # The one thing about a fork that another host would do differently,
            # so a trace of one says it before anything else.
            span.set_attribute("mechanism", f"cp -a {flag}" if flag else "clone")
            # Hidden, and named after the thread, so it is never mistaken for a
            # workspace and the next fork of this thread reclaims whatever a
            # killed one left — the case the cleanup below cannot reach.
            staging = path.with_name(f".{workspace.thread_id}.forking")
            if staging.exists():
                shutil.rmtree(staging)
            try:
                if flag is not None:
                    await copy(mirror, staging, flag)
                else:
                    await run_git("clone", str(mirror), str(staging))
                await self.inherit_remotes(mirror, staging)
                await self.discard_octomate_refs(staging)
                await self.checkout(workspace.thread_id, mirror, staging, ref)
                await install(staging)
                staging.rename(path)
                # `cp -a` preserves the mirror's timestamps and a rename does not
                # refresh them, so without this a fork inherits an mtime that can
                # already be past the idle window — a workspace made seconds ago,
                # born ready for the sweep.
                os.utime(path)
            except BaseException:
                shutil.rmtree(staging, ignore_errors=True)
                raise
        return path

inherit_remotes async

inherit_remotes(mirror: Path, workspace: Path) -> None

Give the fork the mirror's own origin, or no remote where the mirror has none.

Otherwise where a workspace points depends on how it was forked: a copy inherits the mirror's remotes, and a clone gets an origin pointing at the mirror itself. An agent asked to push its branch would find a different answer on a copy-on-write host than on one that clones, which is not something the host's filesystem gets to decide.

The mirror's origin is the project's real upstream, and is what an agent means by pushing — a branch it named for the job, on the repository people will read it in. Nothing Octomate does travels that way: a turn is saved by pushing to the mirror's path, never through a remote name.

A folder project's mirror has no upstream to inherit, and neither does its fork. There is nowhere to push a directory, and saying so by having no remote is clearer to an agent than an origin that fails when it is used.

Source code in octomate/managers/workspaces/base.py
async def inherit_remotes(self, mirror: Path, workspace: Path) -> None:
    """Give the fork the mirror's own `origin`, or no remote where the mirror
    has none.

    Otherwise where a workspace points depends on how it was forked: a copy
    inherits the mirror's remotes, and a clone gets an `origin` pointing at the
    mirror itself. An agent asked to push its branch would find a different
    answer on a copy-on-write host than on one that clones, which is not
    something the host's filesystem gets to decide.

    The mirror's `origin` is the project's real upstream, and is what an agent
    means by pushing — a branch it named for the job, on the repository people
    will read it in. Nothing Octomate does travels that way: a turn is saved by
    pushing to the mirror's path, never through a remote name.

    A folder project's mirror has no upstream to inherit, and neither does its
    fork. There is nowhere to push a directory, and saying so by having no
    remote is clearer to an agent than an `origin` that fails when it is used.
    """
    if "origin" in (await run_git("remote", cwd=workspace)).split():
        await run_git("remote", "remove", "origin", cwd=workspace)
    if "origin" not in (await run_git("remote", cwd=mirror)).split():
        return
    url = await run_git("remote", "get-url", "origin", cwd=mirror)
    await run_git("remote", "add", "origin", url.strip(), cwd=workspace)

discard_octomate_refs async

discard_octomate_refs(workspace: Path) -> None

Take Octomate's own refs out of a fresh fork, however it was made.

git clone asks for refs/heads/* and tags, so a cloned fork never had them. cp does not know what git is: it brings .git across whole, and with it every other thread's refs/octomate/threads/* — their snapshots sitting in a workspace that was never told about them, out of git branch but not out of git log --all. Which of the two a host does is detect's answer about its filesystem, and what one thread can read of another is not something the filesystem gets to decide.

The whole namespace rather than the foreign entries: a mirror that somehow carried a refs/octomate/saved would hand a fresh fork a workspace that reads as already saved, which the sweep answers by reclaiming work that was never pushed. Dropping this thread's own along with the rest costs nothing — the mirror is where it lives, and checkout fetches it back next.

Source code in octomate/managers/workspaces/base.py
async def discard_octomate_refs(self, workspace: Path) -> None:
    """Take Octomate's own refs out of a fresh fork, however it was made.

    `git clone` asks for `refs/heads/*` and tags, so a cloned fork never had
    them. `cp` does not know what git is: it brings `.git` across whole, and
    with it every other thread's `refs/octomate/threads/*` — their snapshots
    sitting in a workspace that was never told about them, out of `git branch`
    but not out of `git log --all`. Which of the two a host does is `detect`'s
    answer about its filesystem, and what one thread can read of another is not
    something the filesystem gets to decide.

    The whole namespace rather than the foreign entries: a mirror that somehow
    carried a `refs/octomate/saved` would hand a fresh fork a workspace that
    reads as already saved, which the sweep answers by reclaiming work that was
    never pushed. Dropping this thread's own along with the rest costs nothing —
    the mirror is where it lives, and `checkout` fetches it back next.
    """
    listing = await run_git(
        "for-each-ref", "--format=%(refname)", "refs/octomate/", cwd=workspace
    )
    for ref in listing.split():
        await run_git("update-ref", "-d", ref, cwd=workspace)

checkout async

checkout(
    thread_id: UUID7,
    mirror: Path,
    workspace: Path,
    ref: str | None = None,
) -> None

Put a freshly forked workspace back the way this thread's last turn left it, when mirror is keeping a snapshot of it.

A thread whose workspace was pruned resumes here, and resumes into the state it was interrupted in rather than a tidied version of it: the branch goes back to where the agent's HEAD was, which is the snapshot's parent, and the tree it could see is laid down over that from the snapshot itself. The index then drops back to HEAD, so work the agent had not committed reads as uncommitted rather than as staged.

A thread forking for the first time starts on its own branch, where the mirror's branch is — or where ref names, when the bind asked for one. The default branch is the wrong answer often enough: continuing someone's feature branch, reproducing against a tag, working from a PR head.

ref is only consulted by a first fork. A thread whose workspace was pruned resumes into its own snapshot, which is where its work is, and no starting point competes with that.

Source code in octomate/managers/workspaces/base.py
async def checkout(
    self,
    thread_id: UUID7,
    mirror: Path,
    workspace: Path,
    ref: str | None = None,
) -> None:
    """Put a freshly forked `workspace` back the way this thread's last turn left
    it, when `mirror` is keeping a snapshot of it.

    A thread whose workspace was pruned resumes here, and resumes into the
    state it was interrupted in rather than a tidied version of it: the branch
    goes back to where the agent's HEAD was, which is the snapshot's parent,
    and the tree it could see is laid down over that from the snapshot itself.
    The index then drops back to HEAD, so work the agent had not committed
    reads as uncommitted rather than as staged.

    A thread forking for the first time starts on its own branch, where the
    mirror's branch is — or where `ref` names, when the bind asked for one.
    The default branch is the wrong answer often enough: continuing someone's
    feature branch, reproducing against a tag, working from a PR head.

    `ref` is only consulted by a first fork. A thread whose workspace was
    pruned resumes into its own snapshot, which is where its work is, and no
    starting point competes with that.
    """
    saved = thread_ref(thread_id)
    # The fork's two endings, and the only place that knows which this is: a
    # thread starting, or one coming back to work a sweep reclaimed the tree
    # of. `resumed` is what separates them in a trace, and the snapshot it
    # came back from is what makes the tree it landed on checkable.
    with workspace_logfire.span(
        "workspace.checkout", thread_id=str(thread_id), ref=ref
    ) as span:
        if not await run_git("ls-remote", str(mirror), saved):
            span.set_attribute("resumed", False)
            branch = thread_branch(thread_id)
            if ref is None:
                await run_git("checkout", "-b", branch, cwd=workspace)
                return
            # Fetched from the mirror rather than named directly: a clone's
            # remote-tracking refs are removed with the `origin` `inherit_remotes`
            # replaces, so the only commit a fork is certain to resolve is the one
            # it was left on. A ref the mirror does not have fails here, which is
            # what the caller has to be able to report.
            await run_git("fetch", str(mirror), ref, cwd=workspace)
            await run_git("checkout", "-b", branch, "FETCH_HEAD", cwd=workspace)
            return
        span.set_attribute("resumed", True)
        # Every fork arrives without it, whichever way it was made, so this is how
        # the workspace comes to have it at all.
        await run_git("fetch", str(mirror), f"+{saved}:{saved}", cwd=workspace)
        subject = await run_git("show", "-s", "--format=%s", saved, cwd=workspace)
        branch = subject.strip().removeprefix(SNAPSHOT_PREFIX)
        span.set_attribute("branch", branch)
        span.set_attribute(
            "snapshot",
            (await run_git("rev-parse", saved, cwd=workspace)).strip(),
        )
        head = (
            ("--detach", f"{saved}^")
            if branch == "HEAD"
            else ("-B", branch, f"{saved}^")
        )
        await run_git("checkout", *head, cwd=workspace)
        await run_git("read-tree", "-u", "--reset", saved, cwd=workspace)
        await run_git("reset", cwd=workspace)
        # A restored workspace is the mirror's snapshot by construction, and says
        # so: the ref that records it was local to the workspace this one replaces,
        # and without this a resume that never reached its next save would look
        # like unsaved work forever, which is a workspace that cannot be reclaimed.
        await run_git("update-ref", SAVED_REF, saved, cwd=workspace)

snapshot async

snapshot(path: Path) -> str

A commit holding this workspace exactly as it stands — staged, unstaged and untracked alike — made without disturbing it.

Built rather than committed, so restoring a workspace is all this costs the repository inside it: HEAD stays where the agent left it, its branch carries the agent's commits and no others, and whatever the agent was in the middle of is not finished on its behalf. Resolving that history is the agent's job; putting the tree back is this one.

The parent is HEAD, so the branch's own history comes along, and the snapshot belongs to no branch — reachable only from the thread's ref, the way a stash is reachable only from refs/stash. Its message carries the branch, which is the one thing a tree and a parent cannot say.

The index it is written through is a copy of the workspace's, which keeps the real one untouched and carries git's stat cache with it, so staging the tree compares timestamps instead of rehashing every file.

Source code in octomate/managers/workspaces/base.py
async def snapshot(self, path: Path) -> str:
    """A commit holding this workspace exactly as it stands — staged, unstaged
    and untracked alike — made without disturbing it.

    Built rather than committed, so restoring a workspace is all this costs
    the repository inside it: HEAD stays where the agent left it, its branch
    carries the agent's commits and no others, and whatever the agent was in
    the middle of is not finished on its behalf. Resolving that history is the
    agent's job; putting the tree back is this one.

    The parent is HEAD, so the branch's own history comes along, and the
    snapshot belongs to no branch — reachable only from the thread's ref, the
    way a stash is reachable only from `refs/stash`. Its message carries the
    branch, which is the one thing a tree and a parent cannot say.

    The index it is written through is a copy of the workspace's, which keeps
    the real one untouched and carries git's stat cache with it, so staging
    the tree compares timestamps instead of rehashing every file.
    """
    index = path / ".git" / "octomate-snapshot-index"
    await asyncio.to_thread(shutil.copyfile, path / ".git" / "index", index)
    env = {"GIT_INDEX_FILE": str(index)}
    await run_git("add", "-A", cwd=path, env=env)
    tree = (await run_git("write-tree", cwd=path, env=env)).strip()
    branch = (await run_git("rev-parse", "--abbrev-ref", "HEAD", cwd=path)).strip()
    commit = await run_git(
        *self.identity.commit_flags,
        "commit-tree",
        tree,
        "-p",
        "HEAD",
        "-m",
        f"{SNAPSHOT_PREFIX}{branch}",
        cwd=path,
    )
    return commit.strip()

save async

save(thread: Thread) -> None

Leave this thread's finished turn where losing its workspace cannot cost it: a snapshot of the whole tree, pushed to the project's mirror as the thread's own ref.

Snapshot and push. A fork owns its objects outright — nothing in a workspace survives its removal — so without the push the workspace is the only copy of what the turn did, and reclaiming it would be losing work rather than evicting a cache. That is the whole of what makes a workspace disposable, and why the sweep is a disk decision.

A thread with no workspace — in no project, or one whose turn never forked one — has nothing to leave. Nothing the agent asks for or is told about either: it knows the directory it was given, not that the directory is a fork, and not that the sweep will take it back.

Forced, because the ref records what the workspace holds rather than a history to protect: an amend or a reset inside the workspace moves the ref with it. The mirror is the only place this ever pushes; nothing here reaches the upstream, which sees a thread's work when a person asks for a pull request and not before.

A failure is logged rather than raised. The turn has already happened and already answered; failing it now would report a delivered answer as an error, and the next turn saves the same work again.

Source code in octomate/managers/workspaces/base.py
async def save(self, thread: Thread) -> None:
    """Leave this thread's finished turn where losing its workspace cannot cost
    it: a snapshot of the whole tree, pushed to the project's mirror as the
    thread's own ref.

    Snapshot *and* push. A fork owns its objects outright — nothing in a
    workspace survives its removal — so without the push the workspace is the
    only copy of what the turn did, and reclaiming it would be losing work
    rather than evicting a cache. That is the whole of what makes a workspace
    disposable, and why the sweep is a disk decision.

    A thread with no workspace — in no project, or one whose turn never forked
    one — has nothing to leave. Nothing the agent asks for or is told about
    either: it knows the directory it was given, not that the directory is a
    fork, and not that the sweep will take it back.

    Forced, because the ref records what the workspace holds rather than a
    history to protect: an amend or a reset inside the workspace moves the ref
    with it. The mirror is the only place this ever pushes; nothing here
    reaches the upstream, which sees a thread's work when a person asks for a
    pull request and not before.

    A failure is logged rather than raised. The turn has already happened and
    already answered; failing it now would report a delivered answer as an
    error, and the next turn saves the same work again.
    """
    # The turn's work leaving the workspace. Until this succeeds the fork is
    # the only copy, so `saved` — which is what the sweep reads — is the
    # attribute to look at when a workspace nobody can reclaim turns up.
    with workspace_logfire.span(
        "workspace.save", thread_id=str(thread.id), saved=False
    ) as span:
        project = await self.projects.of(thread)
        path = self.path(thread.id)
        span.set_attribute("project", project.name if project else None)
        if project is None or not path.is_dir():
            return
        try:
            async with self.lock(thread.id):
                snapshot = await self.snapshot(path)
                await run_git(
                    "push",
                    "--force",
                    str(self.mirrors.path(project)),
                    f"{snapshot}:{thread_ref(thread.id)}",
                    cwd=path,
                )
                # Only now, and only here: this is what tells the sweep that
                # letting this workspace go costs a resume rather than the work.
                await run_git("update-ref", SAVED_REF, snapshot, cwd=path)
            span.set_attribute("snapshot", snapshot)
            span.set_attribute("saved", True)
        except (GitCommandError, OSError):
            logger.exception(
                "the workspace for thread %s could not be saved; its work is only "
                "in %s until a later turn saves it",
                thread.id,
                path,
            )

saved async

saved(path: Path) -> bool

Whether the mirror already holds everything in this workspace: the same tree, on the same commit, as the last push carried.

The question the sweep has to answer before reclaiming anything, and one git status cannot answer any more — nothing here commits, so a saved workspace is a dirty workspace and being dirty says nothing. Snapshotting again and comparing does say it. A push that failed leaves a workspace that looks saved from the outside, which is why what it is compared against is a ref the push itself writes.

Source code in octomate/managers/workspaces/base.py
async def saved(self, path: Path) -> bool:
    """Whether the mirror already holds everything in this workspace: the same
    tree, on the same commit, as the last push carried.

    The question the sweep has to answer before reclaiming anything, and one
    `git status` cannot answer any more — nothing here commits, so a saved
    workspace is a dirty workspace and being dirty says nothing. Snapshotting
    again and comparing does say it. A push that failed leaves a workspace
    that looks saved from the outside, which is why what it is compared
    against is a ref the push itself writes.
    """
    pushed = await run_git(
        "for-each-ref", "--format=%(objectname)", SAVED_REF, cwd=path
    )
    if not pushed.strip():
        return False
    state = ("show", "-s", "--format=%T %P")
    return await run_git(*state, pushed.strip(), cwd=path) == await run_git(
        *state, await self.snapshot(path), cwd=path
    )

prune async

prune(idle: float) -> list[UUID7]

Release every workspace nothing has used for idle seconds, and answer with the threads that lost theirs.

Eviction, not a decision about whether a thread is over: abandonment is unobservable — a thread parked on a question and one silently dropped look identical — so rather than guessing, being wrong here costs the next turn a fork instead of costing anyone their work.

Which is only true of a workspace the mirror already has, so one holding anything unsaved is kept and said out loud. That covers a turn still running, too: it has changed something since its last turn was saved, almost by definition.

Source code in octomate/managers/workspaces/base.py
async def prune(self, idle: float) -> list[UUID7]:
    """Release every workspace nothing has used for `idle` seconds, and answer
    with the threads that lost theirs.

    Eviction, not a decision about whether a thread is over: abandonment is
    unobservable — a thread parked on a question and one silently dropped look
    identical — so rather than guessing, being wrong here costs the next turn a
    fork instead of costing anyone their work.

    Which is only true of a workspace the mirror already has, so one holding
    anything unsaved is kept and said out loud. That covers a turn still
    running, too: it has changed something since its last turn was saved,
    almost by definition.
    """
    released: list[UUID7] = []
    # Three counts, because "why is that workspace still there" has three
    # answers and the disk cannot tell them apart: nothing has been idle long
    # enough, or something is idle and holding work the mirror never got.
    kept_busy = kept_unsaved = 0
    with workspace_logfire.span("workspace.prune", idle=idle) as span:
        if not self.workspaces_dir.is_dir():
            return released
        for path in sorted(self.workspaces_dir.iterdir()):
            try:
                thread_id = uuid.UUID(path.name)
            except ValueError:
                # A staging directory, a probe, or `chat/`: none of them is a
                # workspace this may reclaim. A chat workspace is its run's to
                # throw away, and is not kept anywhere the sweep could restore it
                # from.
                continue
            if time.time() - path.stat().st_mtime < idle:
                kept_busy += 1
                continue
            if not await self.saved(path):
                kept_unsaved += 1
                logger.warning(
                    "the workspace for thread %s is idle but holds work the "
                    "mirror does not have; keeping it",
                    thread_id,
                )
                continue
            await self.release(thread_id)
            released.append(thread_id)
        span.set_attribute("released", len(released))
        span.set_attribute("kept_busy", kept_busy)
        span.set_attribute("kept_unsaved", kept_unsaved)
        if released:
            logger.info("released %d idle workspace(s)", len(released))
        return released

sweep async

sweep() -> None

Prune on the interval, for as long as the host runs.

A sweep that fails is logged and the next one still happens: reclaiming disk is maintenance, and a host that stops serving because it could not tidy up has the priority backwards.

Source code in octomate/managers/workspaces/base.py
async def sweep(self) -> None:
    """Prune on the interval, for as long as the host runs.

    A sweep that fails is logged and the next one still happens: reclaiming
    disk is maintenance, and a host that stops serving because it could not
    tidy up has the priority backwards.
    """
    while True:
        await asyncio.sleep(self.config.sweep_interval)
        try:
            await self.prune(self.config.idle_window)
        except Exception:
            logger.exception("the workspace sweep failed")

carry async

carry(source: Thread, thread: Thread) -> None

Give thread, landed in source's project, a workspace holding the work as source left it — what a conversation moving to another thread takes of its work — and fork it.

Saved first: the source's last turn may not have been, and what it left uncommitted has to travel as uncommitted. The snapshot is filed under thread too, so its first fork resumes into it the way a pruned workspace does; on a branch of its own when the source was on its own, since two threads pushing one branch would overwrite each other. A source that never forked a workspace has no snapshot, and thread forks the project fresh.

Ignored files are copied across, an .env among them, since a snapshot never holds them. Ignored directories are not: they are dependencies and build output, which the fork's install and the next build make again.

Source code in octomate/managers/workspaces/base.py
async def carry(self, source: Thread, thread: Thread) -> None:
    """Give `thread`, landed in `source`'s project, a workspace holding the work
    as `source` left it — what a conversation moving to another thread takes of
    its work — and fork it.

    Saved first: the source's last turn may not have been, and what it left
    uncommitted has to travel as uncommitted. The snapshot is filed under
    `thread` too, so its first fork resumes into it the way a pruned workspace
    does; on a branch of its own when the source was on its own, since two
    threads pushing one branch would overwrite each other. A source that never
    forked a workspace has no snapshot, and `thread` forks the project fresh.

    Ignored files are copied across, an `.env` among them, since a snapshot
    never holds them. Ignored directories are not: they are dependencies and
    build output, which the fork's install and the next build make again.
    """
    project = await self.projects.of(source)
    if project is None:
        return
    await self.save(source)
    mirror = self.mirrors.path(project)
    saved = thread_ref(source.id)
    if await run_git("ls-remote", str(mirror), saved):
        subject = await run_git("show", "-s", "--format=%s", saved, cwd=mirror)
        filed = saved
        if subject.strip() == f"{SNAPSHOT_PREFIX}{thread_branch(source.id)}":
            filed = await run_git(
                *self.identity.commit_flags,
                "commit-tree",
                f"{saved}^{{tree}}",
                "-p",
                f"{saved}^",
                "-m",
                f"{SNAPSHOT_PREFIX}{thread_branch(thread.id)}",
                cwd=mirror,
            )
        await run_git(
            "update-ref", thread_ref(thread.id), filed.strip(), cwd=mirror
        )
    landed = await self.open(thread.id, project).prepare()
    origin = self.existing(source.id)
    if origin is None:
        return
    ignored = await run_git(
        "ls-files",
        "--others",
        "--ignored",
        "--exclude-standard",
        "--directory",
        "-z",
        cwd=origin,
    )
    for name in ignored.split("\0"):
        if not name or name.endswith("/") or (landed / name).exists():
            continue
        (landed / name).parent.mkdir(parents=True, exist_ok=True)
        await asyncio.to_thread(
            shutil.copy2, origin / name, landed / name, follow_symlinks=False
        )

dismiss async

dismiss(thread: Thread) -> bool

Release this thread's workspace on its agent's word that the work in it is done — the one release that is asked for rather than swept up — and answer whether it went.

Saved first, and released only when the save took: the sweep's rule, that a workspace holding work the mirror has not seen is never thrown away, does not bend for being asked. So what this costs is what a prune costs, a fork on the thread's next turn with the ref laid back over it.

Source code in octomate/managers/workspaces/base.py
async def dismiss(self, thread: Thread) -> bool:
    """Release this thread's workspace on its agent's word that the work in it
    is done — the one release that is asked for rather than swept up — and
    answer whether it went.

    Saved first, and released only when the save took: the sweep's rule, that
    a workspace holding work the mirror has not seen is never thrown away,
    does not bend for being asked. So what this costs is what a prune costs,
    a fork on the thread's next turn with the ref laid back over it.
    """
    with workspace_logfire.span(
        "workspace.dismiss", thread_id=str(thread.id), released=False
    ) as span:
        await self.save(thread)
        path = self.path(thread.id)
        if not path.is_dir():
            return False
        if not await self.saved(path):
            logger.warning(
                "the workspace for thread %s was dismissed but holds work the "
                "mirror does not have; keeping it",
                thread.id,
            )
            return False
        await self.release(thread.id)
        span.set_attribute("released", True)
        return True

release async

release(thread_id: UUID7) -> None

Give this thread's workspace back to the disk, or nothing when it is already gone — pruning and releasing a thread that never had one both arrive here, and a workspace is a cache either way.

The mirror is untouched: a copied fork owns its objects outright, and a cloned one holds hard links, where unlinking a link leaves the file. In a thread, off the loop: a released tree is as big as the project.

Source code in octomate/managers/workspaces/base.py
async def release(self, thread_id: UUID7) -> None:
    """Give this thread's workspace back to the disk, or nothing when it is
    already gone — pruning and releasing a thread that never had one both
    arrive here, and a workspace is a cache either way.

    The mirror is untouched: a copied fork owns its objects outright, and a
    cloned one holds hard links, where unlinking a link leaves the file. In
    a thread, off the loop: a released tree is as big as the project.
    """
    path = self.path(thread_id)
    # The end of a tree, and the span a resume is read against: a `materialize`
    # with `resumed` true is this one's other half, however long after.
    with workspace_logfire.span(
        "workspace.release", thread_id=str(thread_id)
    ) as span:
        async with self.lock(thread_id):
            # False where a thread never had one, or a sweep and a caller both
            # reached for the same tree — neither is a failure, and both would
            # otherwise read as one released workspace too many.
            span.set_attribute("removed", path.is_dir())
            if path.is_dir():
                await asyncio.to_thread(shutil.rmtree, path)

thread_ref

thread_ref(thread_id: UUID7) -> str

Where a thread's work is kept in its project's mirror.

Outside refs/heads/, so the mirror does not grow a branch per thread and git branch in a fresh workspace shows the project's branches rather than everyone's threads. That a fork holds none of them at all is discard_octomate_refs' doing and not this namespace's — a copied fork brings across whatever the mirror has.

Source code in octomate/managers/workspaces/base.py
def thread_ref(thread_id: UUID7) -> str:
    """Where a thread's work is kept in its project's mirror.

    Outside `refs/heads/`, so the mirror does not grow a branch per thread and
    `git branch` in a fresh workspace shows the project's branches rather than
    everyone's threads. That a fork holds none of them at all is
    `discard_octomate_refs`' doing and not this namespace's — a copied fork brings
    across whatever the mirror has.
    """
    return f"refs/octomate/threads/{thread_id}"

thread_branch

thread_branch(thread_id: UUID) -> str

The branch a thread's first fork starts on, its own.

Source code in octomate/managers/workspaces/base.py
def thread_branch(thread_id: uuid.UUID) -> str:
    """The branch a thread's first fork starts on, its own."""
    return f"octomate/thread-{thread_id}"

copy async

copy(source: Path, target: Path, flag: str) -> None

Copy source to target with flag, raising CopyError with the stderr when cp exits nonzero.

-a because a fork has to be the tree it was made from: a symlink stays a symlink, and preserved timestamps keep the copied git index valid, so the workspace's first git status compares stat data rather than rehashing every file in the tree.

Source code in octomate/managers/workspaces/base.py
async def copy(source: Path, target: Path, flag: str) -> None:
    """Copy `source` to `target` with `flag`, raising `CopyError` with the stderr
    when `cp` exits nonzero.

    `-a` because a fork has to be the tree it was made from: a symlink stays a
    symlink, and preserved timestamps keep the copied git index valid, so the
    workspace's first `git status` compares stat data rather than rehashing
    every file in the tree.
    """
    process = await asyncio.create_subprocess_exec(
        "cp",
        "-a",
        flag,
        str(source),
        str(target),
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    _, stderr = await process.communicate()
    if process.returncode != 0:
        raise CopyError(
            f"cp -a {flag} {source} {target} failed: "
            f"{stderr.decode(errors='replace').strip()}"
        )

octomate.managers.workspaces.mirrors

Project mirrors: the pristine checkouts workspaces fork from, and the git plumbing that syncs them.

logger module-attribute

logger = logging.getLogger(__name__)

BATCH_GIT_ENV module-attribute

BATCH_GIT_ENV = {
    "GIT_TERMINAL_PROMPT": "0",
    "GIT_SSH_COMMAND": "ssh -o BatchMode=yes",
}

BLANK_MIRROR module-attribute

BLANK_MIRROR = '.blank'

GitCommandError

Bases: RuntimeError

A git command exited nonzero; the message carries its stderr.

Source code in octomate/managers/workspaces/mirrors.py
class GitCommandError(RuntimeError):
    """A git command exited nonzero; the message carries its stderr."""

MirrorManager

Bases: Manager, Locks[str]

Every project's mirror: a pristine git checkout at mirrors_dir/<project>, made from the project's upstream and kept current by syncing from it.

A remote upstream is cloned and freshened by fetching; a directory upstream is git init'd and freshened by staging the folder's state and committing when anything changed — the same job with a directory in place of a URL, so the default branch becomes a history of the folder. The folder stays authoritative: carrying reviewed work back out to it is a deliberate step, not part of syncing.

A mirror stays on its default branch and no run ever writes to it; workspaces fork from it and are disposable.

Source code in octomate/managers/workspaces/mirrors.py
class MirrorManager(Manager, Locks[str]):
    """Every project's mirror: a pristine git checkout at ``mirrors_dir/<project>``,
    made from the project's upstream and kept current by syncing from it.

    A remote upstream is cloned and freshened by fetching; a directory upstream is
    `git init`'d and freshened by staging the folder's state and committing when
    anything changed — the same job with a directory in place of a URL, so the
    default branch becomes a history of the folder. The folder stays authoritative:
    carrying reviewed work back out to it is a deliberate step, not part of syncing.

    A mirror stays on its default branch and no run ever writes to it; workspaces
    fork from it and are disposable.
    """

    def __init__(
        self,
        config: MirrorsConfig | None = None,
        mirrors_dir: Path = Path(".octomate/mirrors"),
    ) -> None:
        self.config = config if config is not None else MirrorsConfig()
        self.mirrors_dir = mirrors_dir
        self.synced: dict[str, float] = {}

    def path(self, project: Project | None = None) -> Path:
        """Where `project`'s mirror lives — or, with no project, where the blank one
        does: the mirror of nothing, which every thread in no project forks.

        Absolute, because directory sync points `GIT_DIR` here while running in the
        upstream folder, where a relative path would resolve against the wrong tree.
        """
        return (self.mirrors_dir / self.name(project)).resolve()

    def name(self, project: Project | None) -> str:
        """What this mirror is filed under, which is also what its lock and its
        freshness stamp are keyed by. The blank mirror is dot-prefixed, to keep it
        out of the way of the mirrors it sits among — those are named after
        projects, and so after directories people actually have."""
        return project.name if project is not None else BLANK_MIRROR

    async def sync(self, project: Project) -> Path:
        """The mirror, current enough to fork from: created from the upstream when
        missing, freshened from it when the freshness window has lapsed.

        Serialized per mirror, so a burst of materializations costs one sync: the
        making of it waits inside `create`, and the freshening of it waits here, on
        the same lock. Which is why `create` is called before this takes it —
        `asyncio.Lock` is not reentrant, and both halves are the same mirror's turn.

        A freshen that fails degrades to the stale mirror with a warning rather than
        failing the caller — work continues on what the machine already has, and
        the stale stamp is not renewed, so the next sync tries again. A mirror
        that cannot be created is a hard failure: there is nothing to fall back to.

        A mirror whose tree just changed is installed as well, which is what makes
        forking it cheap: the machine's package store is warm and the installed
        trees themselves are copied. Not on the window's short path, and not on the
        stale one — nothing moved there, so there is nothing to install.
        """
        # Asked before `create`, which is where the waiting happens: a mirror this
        # call had to make is one nothing can be behind, so it is not fetched again
        # for the sync that made it.
        missing = not self.path(project).is_dir()
        path = await self.create(project)
        async with self.lock(project.name):
            if missing:
                self.synced[project.name] = time.monotonic()
                await install(path)
                return path
            last = self.synced.get(project.name)
            window = self.config.freshness_window
            if last is not None and time.monotonic() - last < window:
                return path
            try:
                await self.freshen(project, path)
            except (GitCommandError, OSError) as error:
                logger.warning("mirror for %s is stale: %s", project.name, error)
                return path
            self.synced[project.name] = time.monotonic()
            await install(path)
        return path

    async def create(self, project: Project | None = None) -> Path:
        """The mirror, made where there is none, and where it is.

        Three shapes, and a project's upstream picks between the first two: a remote
        is cloned onto its default branch, and a directory is `git init`'d with the
        folder's contents committed. With no project at all it is the blank mirror,
        which is a `git init` and one empty commit — that commit being what makes it
        forkable rather than special, since a repository with no commit clones onto
        an unborn branch, where git refuses the ordinary things a run does with one.
        Nothing binds to it and it takes no registry row: a thread that forks it is
        exactly a thread that is in no project.

        Serialized per mirror and idempotent, which is what the blank one needs —
        two threads in no project starting at once both ask for it, and a mirror
        half-made is a mirror something would fork. `sync` holds the same lock for
        the freshening half, and takes it after this rather than around it.

        A partial mirror is worse than none — it would read as existing and turn
        every later sync into a stale-mirror warning — so a creation that fails or
        is cancelled removes what it left before the error travels on.
        """
        path = self.path(project)
        async with self.lock(self.name(project)):
            if path.is_dir():
                return path
            self.mirrors_dir.mkdir(parents=True, exist_ok=True)
            upstream = project.upstream if project is not None else None
            try:
                if isinstance(upstream, RemoteUpstream):
                    await run_git("clone", upstream.url, str(path), env=BATCH_GIT_ENV)
                    return path
                await run_git("init", "-b", "main", str(path))
                if upstream is None:
                    await run_git(
                        *self.config.identity.commit_flags,
                        "commit",
                        "--allow-empty",
                        "-m",
                        "empty",
                        cwd=path,
                    )
                else:
                    await self.commit_folder(upstream.path, path)
            except BaseException:
                shutil.rmtree(path, ignore_errors=True)
                raise
        return path

    async def freshen(self, project: Project, path: Path) -> None:
        """Bring an existing mirror up to its upstream: fetch and move the default
        branch for a remote, stage-and-commit the folder for a directory.

        `--prune` follows upstream branch deletions; thread refs live outside the
        fetched refspec, so pruning never touches them. The hard reset is what a
        mirror allows that a working checkout would not: nothing else writes
        here, so the default branch simply follows upstream, force-pushes included.
        """
        upstream = project.upstream
        if isinstance(upstream, RemoteUpstream):
            await run_git("fetch", "--prune", "origin", cwd=path, env=BATCH_GIT_ENV)
            await run_git("reset", "--hard", "@{upstream}", cwd=path)
        else:
            await self.commit_folder(upstream.path, path)

    async def commit_folder(self, folder: Path, path: Path) -> None:
        """One sync of a directory upstream: stage exactly the folder's state into
        the mirror at `path`, and commit only when anything changed.

        Git's own index does the comparison. With `GIT_WORK_TREE` pointing at the
        folder, `add -A` stages additions, edits, and deletions in one pass while
        honoring the folder's own `.gitignore` — which is what keeps a served
        checkout's `.octomate/` out of its mirror — and no `.git` is ever created
        in the folder itself. The final reset materializes the commit into the
        mirror's tree; it never `git clean`s, so untracked state the mirror keeps
        (a warm `node_modules`, say) survives every sync.
        """
        against = {
            "GIT_DIR": str(path / ".git"),
            "GIT_WORK_TREE": str(folder),
            # A scratch index, so the mirror's own index keeps tracking its HEAD.
            # Staging a deletion in the shared index would untrack the file, and
            # the final reset — which deletes what is tracked-but-gone and spares
            # what was never tracked — would then leave it behind in the mirror.
            "GIT_INDEX_FILE": str(path / ".git" / "sync-index"),
        }
        await run_git("add", "-A", cwd=folder, env=against)
        # `status` rather than `diff HEAD`, because a just-init'd mirror has no
        # HEAD to diff against, and an unchanged folder has to read as clean.
        if not await run_git("status", "--porcelain", cwd=folder, env=against):
            return
        await run_git(
            *self.config.identity.commit_flags,
            "commit",
            "-m",
            f"sync {folder}",
            cwd=folder,
            env=against,
        )
        await run_git("reset", "--hard", cwd=path)

    async def reconcile(self, projects: list[Project]) -> None:
        """Sync every enabled project's mirror, as startup runs it over the
        registry: registering a project is what grants it a mirror, so the
        mirrors directory agrees with the registry before the first
        materialization asks.

        One unreachable upstream must not stop the rest — each failure is logged
        and the sweep continues, the same isolation tentacle startup gets.
        """
        for project in projects:
            if not project.enabled:
                continue
            try:
                await self.sync(project)
            except Exception:
                logger.exception("mirror for %s could not be made", project.name)

config instance-attribute

config = config if config is not None else MirrorsConfig()

mirrors_dir instance-attribute

mirrors_dir = mirrors_dir

synced instance-attribute

synced: dict[str, float] = {}

path

path(project: Project | None = None) -> Path

Where project's mirror lives — or, with no project, where the blank one does: the mirror of nothing, which every thread in no project forks.

Absolute, because directory sync points GIT_DIR here while running in the upstream folder, where a relative path would resolve against the wrong tree.

Source code in octomate/managers/workspaces/mirrors.py
def path(self, project: Project | None = None) -> Path:
    """Where `project`'s mirror lives — or, with no project, where the blank one
    does: the mirror of nothing, which every thread in no project forks.

    Absolute, because directory sync points `GIT_DIR` here while running in the
    upstream folder, where a relative path would resolve against the wrong tree.
    """
    return (self.mirrors_dir / self.name(project)).resolve()

name

name(project: Project | None) -> str

What this mirror is filed under, which is also what its lock and its freshness stamp are keyed by. The blank mirror is dot-prefixed, to keep it out of the way of the mirrors it sits among — those are named after projects, and so after directories people actually have.

Source code in octomate/managers/workspaces/mirrors.py
def name(self, project: Project | None) -> str:
    """What this mirror is filed under, which is also what its lock and its
    freshness stamp are keyed by. The blank mirror is dot-prefixed, to keep it
    out of the way of the mirrors it sits among — those are named after
    projects, and so after directories people actually have."""
    return project.name if project is not None else BLANK_MIRROR

sync async

sync(project: Project) -> Path

The mirror, current enough to fork from: created from the upstream when missing, freshened from it when the freshness window has lapsed.

Serialized per mirror, so a burst of materializations costs one sync: the making of it waits inside create, and the freshening of it waits here, on the same lock. Which is why create is called before this takes it — asyncio.Lock is not reentrant, and both halves are the same mirror's turn.

A freshen that fails degrades to the stale mirror with a warning rather than failing the caller — work continues on what the machine already has, and the stale stamp is not renewed, so the next sync tries again. A mirror that cannot be created is a hard failure: there is nothing to fall back to.

A mirror whose tree just changed is installed as well, which is what makes forking it cheap: the machine's package store is warm and the installed trees themselves are copied. Not on the window's short path, and not on the stale one — nothing moved there, so there is nothing to install.

Source code in octomate/managers/workspaces/mirrors.py
async def sync(self, project: Project) -> Path:
    """The mirror, current enough to fork from: created from the upstream when
    missing, freshened from it when the freshness window has lapsed.

    Serialized per mirror, so a burst of materializations costs one sync: the
    making of it waits inside `create`, and the freshening of it waits here, on
    the same lock. Which is why `create` is called before this takes it —
    `asyncio.Lock` is not reentrant, and both halves are the same mirror's turn.

    A freshen that fails degrades to the stale mirror with a warning rather than
    failing the caller — work continues on what the machine already has, and
    the stale stamp is not renewed, so the next sync tries again. A mirror
    that cannot be created is a hard failure: there is nothing to fall back to.

    A mirror whose tree just changed is installed as well, which is what makes
    forking it cheap: the machine's package store is warm and the installed
    trees themselves are copied. Not on the window's short path, and not on the
    stale one — nothing moved there, so there is nothing to install.
    """
    # Asked before `create`, which is where the waiting happens: a mirror this
    # call had to make is one nothing can be behind, so it is not fetched again
    # for the sync that made it.
    missing = not self.path(project).is_dir()
    path = await self.create(project)
    async with self.lock(project.name):
        if missing:
            self.synced[project.name] = time.monotonic()
            await install(path)
            return path
        last = self.synced.get(project.name)
        window = self.config.freshness_window
        if last is not None and time.monotonic() - last < window:
            return path
        try:
            await self.freshen(project, path)
        except (GitCommandError, OSError) as error:
            logger.warning("mirror for %s is stale: %s", project.name, error)
            return path
        self.synced[project.name] = time.monotonic()
        await install(path)
    return path

create async

create(project: Project | None = None) -> Path

The mirror, made where there is none, and where it is.

Three shapes, and a project's upstream picks between the first two: a remote is cloned onto its default branch, and a directory is git init'd with the folder's contents committed. With no project at all it is the blank mirror, which is a git init and one empty commit — that commit being what makes it forkable rather than special, since a repository with no commit clones onto an unborn branch, where git refuses the ordinary things a run does with one. Nothing binds to it and it takes no registry row: a thread that forks it is exactly a thread that is in no project.

Serialized per mirror and idempotent, which is what the blank one needs — two threads in no project starting at once both ask for it, and a mirror half-made is a mirror something would fork. sync holds the same lock for the freshening half, and takes it after this rather than around it.

A partial mirror is worse than none — it would read as existing and turn every later sync into a stale-mirror warning — so a creation that fails or is cancelled removes what it left before the error travels on.

Source code in octomate/managers/workspaces/mirrors.py
async def create(self, project: Project | None = None) -> Path:
    """The mirror, made where there is none, and where it is.

    Three shapes, and a project's upstream picks between the first two: a remote
    is cloned onto its default branch, and a directory is `git init`'d with the
    folder's contents committed. With no project at all it is the blank mirror,
    which is a `git init` and one empty commit — that commit being what makes it
    forkable rather than special, since a repository with no commit clones onto
    an unborn branch, where git refuses the ordinary things a run does with one.
    Nothing binds to it and it takes no registry row: a thread that forks it is
    exactly a thread that is in no project.

    Serialized per mirror and idempotent, which is what the blank one needs —
    two threads in no project starting at once both ask for it, and a mirror
    half-made is a mirror something would fork. `sync` holds the same lock for
    the freshening half, and takes it after this rather than around it.

    A partial mirror is worse than none — it would read as existing and turn
    every later sync into a stale-mirror warning — so a creation that fails or
    is cancelled removes what it left before the error travels on.
    """
    path = self.path(project)
    async with self.lock(self.name(project)):
        if path.is_dir():
            return path
        self.mirrors_dir.mkdir(parents=True, exist_ok=True)
        upstream = project.upstream if project is not None else None
        try:
            if isinstance(upstream, RemoteUpstream):
                await run_git("clone", upstream.url, str(path), env=BATCH_GIT_ENV)
                return path
            await run_git("init", "-b", "main", str(path))
            if upstream is None:
                await run_git(
                    *self.config.identity.commit_flags,
                    "commit",
                    "--allow-empty",
                    "-m",
                    "empty",
                    cwd=path,
                )
            else:
                await self.commit_folder(upstream.path, path)
        except BaseException:
            shutil.rmtree(path, ignore_errors=True)
            raise
    return path

freshen async

freshen(project: Project, path: Path) -> None

Bring an existing mirror up to its upstream: fetch and move the default branch for a remote, stage-and-commit the folder for a directory.

--prune follows upstream branch deletions; thread refs live outside the fetched refspec, so pruning never touches them. The hard reset is what a mirror allows that a working checkout would not: nothing else writes here, so the default branch simply follows upstream, force-pushes included.

Source code in octomate/managers/workspaces/mirrors.py
async def freshen(self, project: Project, path: Path) -> None:
    """Bring an existing mirror up to its upstream: fetch and move the default
    branch for a remote, stage-and-commit the folder for a directory.

    `--prune` follows upstream branch deletions; thread refs live outside the
    fetched refspec, so pruning never touches them. The hard reset is what a
    mirror allows that a working checkout would not: nothing else writes
    here, so the default branch simply follows upstream, force-pushes included.
    """
    upstream = project.upstream
    if isinstance(upstream, RemoteUpstream):
        await run_git("fetch", "--prune", "origin", cwd=path, env=BATCH_GIT_ENV)
        await run_git("reset", "--hard", "@{upstream}", cwd=path)
    else:
        await self.commit_folder(upstream.path, path)

commit_folder async

commit_folder(folder: Path, path: Path) -> None

One sync of a directory upstream: stage exactly the folder's state into the mirror at path, and commit only when anything changed.

Git's own index does the comparison. With GIT_WORK_TREE pointing at the folder, add -A stages additions, edits, and deletions in one pass while honoring the folder's own .gitignore — which is what keeps a served checkout's .octomate/ out of its mirror — and no .git is ever created in the folder itself. The final reset materializes the commit into the mirror's tree; it never git cleans, so untracked state the mirror keeps (a warm node_modules, say) survives every sync.

Source code in octomate/managers/workspaces/mirrors.py
async def commit_folder(self, folder: Path, path: Path) -> None:
    """One sync of a directory upstream: stage exactly the folder's state into
    the mirror at `path`, and commit only when anything changed.

    Git's own index does the comparison. With `GIT_WORK_TREE` pointing at the
    folder, `add -A` stages additions, edits, and deletions in one pass while
    honoring the folder's own `.gitignore` — which is what keeps a served
    checkout's `.octomate/` out of its mirror — and no `.git` is ever created
    in the folder itself. The final reset materializes the commit into the
    mirror's tree; it never `git clean`s, so untracked state the mirror keeps
    (a warm `node_modules`, say) survives every sync.
    """
    against = {
        "GIT_DIR": str(path / ".git"),
        "GIT_WORK_TREE": str(folder),
        # A scratch index, so the mirror's own index keeps tracking its HEAD.
        # Staging a deletion in the shared index would untrack the file, and
        # the final reset — which deletes what is tracked-but-gone and spares
        # what was never tracked — would then leave it behind in the mirror.
        "GIT_INDEX_FILE": str(path / ".git" / "sync-index"),
    }
    await run_git("add", "-A", cwd=folder, env=against)
    # `status` rather than `diff HEAD`, because a just-init'd mirror has no
    # HEAD to diff against, and an unchanged folder has to read as clean.
    if not await run_git("status", "--porcelain", cwd=folder, env=against):
        return
    await run_git(
        *self.config.identity.commit_flags,
        "commit",
        "-m",
        f"sync {folder}",
        cwd=folder,
        env=against,
    )
    await run_git("reset", "--hard", cwd=path)

reconcile async

reconcile(projects: list[Project]) -> None

Sync every enabled project's mirror, as startup runs it over the registry: registering a project is what grants it a mirror, so the mirrors directory agrees with the registry before the first materialization asks.

One unreachable upstream must not stop the rest — each failure is logged and the sweep continues, the same isolation tentacle startup gets.

Source code in octomate/managers/workspaces/mirrors.py
async def reconcile(self, projects: list[Project]) -> None:
    """Sync every enabled project's mirror, as startup runs it over the
    registry: registering a project is what grants it a mirror, so the
    mirrors directory agrees with the registry before the first
    materialization asks.

    One unreachable upstream must not stop the rest — each failure is logged
    and the sweep continues, the same isolation tentacle startup gets.
    """
    for project in projects:
        if not project.enabled:
            continue
        try:
            await self.sync(project)
        except Exception:
            logger.exception("mirror for %s could not be made", project.name)

run_git async

run_git(
    *args: str,
    cwd: Path | None = None,
    env: dict[str, str] | None = None,
) -> str

Run one git command and answer its stdout, raising GitCommandError with the command and its stderr when it exits nonzero.

Source code in octomate/managers/workspaces/mirrors.py
async def run_git(
    *args: str, cwd: Path | None = None, env: dict[str, str] | None = None
) -> str:
    """Run one git command and answer its stdout, raising `GitCommandError` with
    the command and its stderr when it exits nonzero."""
    process = await asyncio.create_subprocess_exec(
        "git",
        *args,
        cwd=cwd,
        env={**os.environ, **env} if env else None,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    stdout, stderr = await process.communicate()
    if process.returncode != 0:
        raise GitCommandError(
            f"git {' '.join(args)} failed: {stderr.decode(errors='replace').strip()}"
        )
    return stdout.decode(errors="replace")

octomate.managers.workspaces.dependencies

A tree's dependencies, installed once and inherited by every fork of it.

uv, pnpm and npm each keep one content-addressed store per machine and hardlink out of it, so the second install of a package fetches nothing and costs almost no disk. Nothing here isolates a tree from those stores and nothing should: leaving them where the tools put them is the whole of why a fresh workspace installs offline, and it is the constraint on anything that ever puts a run behind a filesystem boundary.

The rest is one install per ecosystem, run where a tree just changed — in a mirror, so a copied fork arrives ready to run, and in a fork, because a cloned one carries no untracked file and so arrives with nothing installed at all.

Which ecosystem a tree belongs to is its lockfile's answer, and each manager below owns the commands that follow from it. They differ in more than the binary's name: uv needs two commands and a flag to produce an environment that survives being copied, where the node managers need one and would be broken by the flag their own documentation recommends.

logger module-attribute

logger = logging.getLogger(__name__)

STAMP module-attribute

STAMP = 'octomate-installed'

MANAGERS module-attribute

MANAGERS: tuple[PackageManager, ...] = (UV(), Pnpm(), Npm())

PackageManager

Bases: Manager, ABC

One ecosystem's answer to "put this tree's dependencies in place".

A manager is stateless and claims a tree by its lockfile, which is also what says whether the tree is still current. Everything a manager decides is in its install; the machinery around it — whether to install at all, and what to record afterwards — belongs to the module and is the same for all of them.

Source code in octomate/managers/workspaces/dependencies.py
class PackageManager(Manager, ABC):
    """One ecosystem's answer to "put this tree's dependencies in place".

    A manager is stateless and claims a tree by its lockfile, which is also what
    says whether the tree is still current. Everything a manager decides is in its
    `install`; the machinery around it — whether to install at all, and what to
    record afterwards — belongs to the module and is the same for all of them.
    """

    # The file whose presence makes a tree this manager's, and whose contents
    # decide whether what is installed is still what the tree asks for.
    lockfile: ClassVar[str]

    @abstractmethod
    async def install(self, tree: Path) -> bool:
        """Install `tree`'s dependencies, answering whether they are now there.

        The answer is what the stamp turns on, so a manager that reports success
        is promising the tree can be forked and used as it stands.
        """

    async def run(self, tree: Path, *argv: str) -> bool:
        """One command of an install, in `tree`, answering whether it worked.

        A failure is logged rather than raised. The tree is a working checkout
        either way and whatever runs there installs for itself, the way every run
        did before any of this existed — refusing someone a workspace because the
        host has no `pnpm` would turn a slow first turn into no turn at all.
        """
        try:
            process = await asyncio.create_subprocess_exec(
                *argv,
                cwd=tree,
                stdout=asyncio.subprocess.DEVNULL,
                stderr=asyncio.subprocess.PIPE,
            )
            _, stderr = await process.communicate()
            if process.returncode == 0:
                return True
            reason = stderr.decode(errors="replace").strip()
        except OSError as error:
            reason = str(error)
        logger.warning(
            "`%s` in %s failed; the tree is left uninstalled and whatever runs "
            "there installs it: %s",
            " ".join(argv),
            tree,
            reason,
        )
        return False

lockfile class-attribute

lockfile: str

install abstractmethod async

install(tree: Path) -> bool

Install tree's dependencies, answering whether they are now there.

The answer is what the stamp turns on, so a manager that reports success is promising the tree can be forked and used as it stands.

Source code in octomate/managers/workspaces/dependencies.py
@abstractmethod
async def install(self, tree: Path) -> bool:
    """Install `tree`'s dependencies, answering whether they are now there.

    The answer is what the stamp turns on, so a manager that reports success
    is promising the tree can be forked and used as it stands.
    """

run async

run(tree: Path, *argv: str) -> bool

One command of an install, in tree, answering whether it worked.

A failure is logged rather than raised. The tree is a working checkout either way and whatever runs there installs for itself, the way every run did before any of this existed — refusing someone a workspace because the host has no pnpm would turn a slow first turn into no turn at all.

Source code in octomate/managers/workspaces/dependencies.py
async def run(self, tree: Path, *argv: str) -> bool:
    """One command of an install, in `tree`, answering whether it worked.

    A failure is logged rather than raised. The tree is a working checkout
    either way and whatever runs there installs for itself, the way every run
    did before any of this existed — refusing someone a workspace because the
    host has no `pnpm` would turn a slow first turn into no turn at all.
    """
    try:
        process = await asyncio.create_subprocess_exec(
            *argv,
            cwd=tree,
            stdout=asyncio.subprocess.DEVNULL,
            stderr=asyncio.subprocess.PIPE,
        )
        _, stderr = await process.communicate()
        if process.returncode == 0:
            return True
        reason = stderr.decode(errors="replace").strip()
    except OSError as error:
        reason = str(error)
    logger.warning(
        "`%s` in %s failed; the tree is left uninstalled and whatever runs "
        "there installs it: %s",
        " ".join(argv),
        tree,
        reason,
    )
    return False

UV

Bases: PackageManager

Python, through uv.

Source code in octomate/managers/workspaces/dependencies.py
class UV(PackageManager):
    """Python, through uv."""

    lockfile: ClassVar[str] = "uv.lock"

    async def install(self, tree: Path) -> bool:
        """Create a relocatable environment once, then sync its dependencies.

        The venv is otherwise the one thing a fork cannot inherit: uv writes an
        absolute interpreter path into every console script, so a copied
        `.venv/bin/<tool>` keeps running the mirror's python against the mirror's
        site-packages. Nothing errors — the workspace simply is not the
        environment in use — and re-syncing does not repair it, since the copy
        already satisfies the lockfile and uv has nothing to do.

        Made relocatable, the scripts resolve the interpreter beside themselves
        and the copy is correct. uv records the choice in `pyvenv.cfg` and later
        syncs keep it. Reuse that environment when the lockfile changes. An
        existing environment without the marker is left untouched and reported:
        rewriting its configuration would not repair installed console scripts.
        """
        venv = tree / ".venv"
        if venv.exists():
            config = venv / "pyvenv.cfg"
            if not config.is_file() or not re.search(
                r"(?m)^relocatable\s*=\s*true\s*$", config.read_text()
            ):
                logger.warning(
                    "Dependencies in %s were not refreshed: .venv is not relocatable. "
                    "Move it aside and recreate it with `uv venv --relocatable` "
                    "before retrying.",
                    tree,
                )
                return False
        elif not await self.run(tree, "uv", "venv", "--relocatable"):
            return False
        return await self.run(tree, "uv", "sync")

lockfile class-attribute

lockfile: str = 'uv.lock'

install async

install(tree: Path) -> bool

Create a relocatable environment once, then sync its dependencies.

The venv is otherwise the one thing a fork cannot inherit: uv writes an absolute interpreter path into every console script, so a copied .venv/bin/<tool> keeps running the mirror's python against the mirror's site-packages. Nothing errors — the workspace simply is not the environment in use — and re-syncing does not repair it, since the copy already satisfies the lockfile and uv has nothing to do.

Made relocatable, the scripts resolve the interpreter beside themselves and the copy is correct. uv records the choice in pyvenv.cfg and later syncs keep it. Reuse that environment when the lockfile changes. An existing environment without the marker is left untouched and reported: rewriting its configuration would not repair installed console scripts.

Source code in octomate/managers/workspaces/dependencies.py
async def install(self, tree: Path) -> bool:
    """Create a relocatable environment once, then sync its dependencies.

    The venv is otherwise the one thing a fork cannot inherit: uv writes an
    absolute interpreter path into every console script, so a copied
    `.venv/bin/<tool>` keeps running the mirror's python against the mirror's
    site-packages. Nothing errors — the workspace simply is not the
    environment in use — and re-syncing does not repair it, since the copy
    already satisfies the lockfile and uv has nothing to do.

    Made relocatable, the scripts resolve the interpreter beside themselves
    and the copy is correct. uv records the choice in `pyvenv.cfg` and later
    syncs keep it. Reuse that environment when the lockfile changes. An
    existing environment without the marker is left untouched and reported:
    rewriting its configuration would not repair installed console scripts.
    """
    venv = tree / ".venv"
    if venv.exists():
        config = venv / "pyvenv.cfg"
        if not config.is_file() or not re.search(
            r"(?m)^relocatable\s*=\s*true\s*$", config.read_text()
        ):
            logger.warning(
                "Dependencies in %s were not refreshed: .venv is not relocatable. "
                "Move it aside and recreate it with `uv venv --relocatable` "
                "before retrying.",
                tree,
            )
            return False
    elif not await self.run(tree, "uv", "venv", "--relocatable"):
        return False
    return await self.run(tree, "uv", "sync")

Pnpm

Bases: PackageManager

Node, through pnpm.

Source code in octomate/managers/workspaces/dependencies.py
class Pnpm(PackageManager):
    """Node, through pnpm."""

    lockfile: ClassVar[str] = "pnpm-lock.yaml"

    async def install(self, tree: Path) -> bool:
        """`node_modules` is a tree of links into the store and copies cleanly, so
        there is nothing here but the install."""
        return await self.run(tree, "pnpm", "install")

lockfile class-attribute

lockfile: str = 'pnpm-lock.yaml'

install async

install(tree: Path) -> bool

node_modules is a tree of links into the store and copies cleanly, so there is nothing here but the install.

Source code in octomate/managers/workspaces/dependencies.py
async def install(self, tree: Path) -> bool:
    """`node_modules` is a tree of links into the store and copies cleanly, so
    there is nothing here but the install."""
    return await self.run(tree, "pnpm", "install")

Npm

Bases: PackageManager

Node, through npm.

Source code in octomate/managers/workspaces/dependencies.py
class Npm(PackageManager):
    """Node, through npm."""

    lockfile: ClassVar[str] = "package-lock.json"

    async def install(self, tree: Path) -> bool:
        """`install` rather than the `ci` a lockfile would normally call for: `ci`
        begins by deleting `node_modules`, which is exactly the tree a warm mirror
        exists to keep."""
        return await self.run(tree, "npm", "install")

lockfile class-attribute

lockfile: str = 'package-lock.json'

install async

install(tree: Path) -> bool

install rather than the ci a lockfile would normally call for: ci begins by deleting node_modules, which is exactly the tree a warm mirror exists to keep.

Source code in octomate/managers/workspaces/dependencies.py
async def install(self, tree: Path) -> bool:
    """`install` rather than the `ci` a lockfile would normally call for: `ci`
    begins by deleting `node_modules`, which is exactly the tree a warm mirror
    exists to keep."""
    return await self.run(tree, "npm", "install")

manager

manager(tree: Path) -> PackageManager | None

Whose tree this is, or None where nobody claims it — a plain folder of documents, and the empty tree a thread in no project forks.

Source code in octomate/managers/workspaces/dependencies.py
def manager(tree: Path) -> PackageManager | None:
    """Whose tree this is, or None where nobody claims it — a plain folder of
    documents, and the empty tree a thread in no project forks."""
    for candidate in MANAGERS:
        if (tree / candidate.lockfile).is_file():
            return candidate
    return None

install async

install(tree: Path) -> None

Install tree's dependencies, unless its lockfile is still the one they were last installed from.

That stamp is what makes one call right for both trees this is asked about. A mirror installs when its lockfile moves, which is the only time it needs to. A copied fork inherits the stamp along with the installed tree and does nothing; a cloned fork has neither and installs from the store the mirror warmed; and a fork checked out on a ref whose lockfile differs installs whichever way it was made.

An install that failed leaves no stamp, so the next tree forked from this one tries again rather than inheriting the claim that it is done.

Source code in octomate/managers/workspaces/dependencies.py
async def install(tree: Path) -> None:
    """Install `tree`'s dependencies, unless its lockfile is still the one they
    were last installed from.

    That stamp is what makes one call right for both trees this is asked about. A
    mirror installs when its lockfile moves, which is the only time it needs to. A
    copied fork inherits the stamp along with the installed tree and does nothing;
    a cloned fork has neither and installs from the store the mirror warmed; and a
    fork checked out on a ref whose lockfile differs installs whichever way it was
    made.

    An install that failed leaves no stamp, so the next tree forked from this one
    tries again rather than inheriting the claim that it is done.
    """
    owner = manager(tree)
    if owner is None:
        return
    stamp = tree / ".git" / STAMP
    digest = hashlib.sha256((tree / owner.lockfile).read_bytes()).hexdigest()
    if stamp.is_file() and stamp.read_text() == digest:
        return
    if await owner.install(tree):
        stamp.write_text(digest)