Skip to main content

Repository virtualization

Each work unit branch gets a physically isolated working directory. Agents write files, build, and test in their own sandbox — there is no shared working directory and no file-lock contention between concurrent agents.

How it works

The workspace root is configured via Workspace:RootPath in appsettings.json. For every branch, Studio derives a directory path by sanitizing the branch ID — any character invalid in a filesystem path (including /, the conventional branch separator) is replaced with _:
Examples: To get the effective path for any branch programmatically:
  • REST: GET /studio/workspace/path?branchId=<branch>
  • MCP: nm_v1_workspace_path with { "branchId": "..." }
Response: { "branchId": "...", "workingDirectory": "...", "exists": true }

Branch seeding

When a new branch directory is initialized, Studio tries three strategies in order:
  1. Scoped CAS materialization — if the work unit declares FileScope glob patterns and a CAS snapshot exists, only the matching paths (plus project structure files such as .csproj and package.json) are extracted from the content-addressable store. This keeps branch directories small when agents only need a subset of the repository.
  2. Seed from another branch — if seedFromBranchId is specified on work-unit create, the source branch directory is copied as-is.
  3. Main-branch reconstruction — for the main branch specifically, Studio reconstructs from Workspace:SeedRepositoryPath via CAS snapshot or a full directory copy.

Keeping the CAS snapshot current

The CAS snapshot (RepositorySnapshot — a flat path→blobId map, plus an Add/Replace/Delete op-log) is a best-effort audit/reconstruction trail derived from the seed repository’s on-disk content — it is not the source of truth during a run; the per-branch working directory is. Two things advance it:
  • Bootstrap (RepositoryImportService.EnsureBootstrappedAsync) — walks every file once and records a Generation-0 snapshot. Fires automatically the first time a goal is created against a repository, then intentionally does nothing on any later GoalCreation/StartupRecovery call for that same repository in the same process — a one-time seed is all those need.
  • Forced resync (RepositoryImportService.ForceSyncAsync) — re-diffs the repository’s current disk content against the last snapshot and records a successor snapshot if anything changed, regardless of whether it was already bootstrapped. This is what keeps the snapshot from going permanently stale after the first goal. It fires:
    • Automatically, right after a merge’s changes are written back to the repository — scoped specifically to the global default repository (Workspace:SeedRepositoryPath); a multi-repo work unit’s own registered repository does not currently trigger an automatic resync this way.
    • On-demand via POST /studio/workspace/switch or the nm_v1_workspace_switch MCP tool, whether or not the path actually changed.
A resync never touches an already-materialized file in any branch directory — branch initialization no-ops the instant a branch directory is non-empty, and the only other snapshot-consuming read path (on-demand FileScope fallback fetch) only ever fires for a file that branch has never touched before. So a live resync cannot disturb a running agent’s own in-progress work; the one observable effect is that a scoped branch’s first-ever fetch of a not-yet-materialized file may see fresher content than it would have before the resync ran.

Scoped materialization

When a work unit’s FileScope property contains one or more glob patterns, the materializer extracts only files matching those patterns from the CAS blob store. Project-structure files (.csproj, package.json, Cargo.toml, go.mod, etc.) are always included regardless of scope so build tools can resolve the project graph. This is the primary mechanism for keeping short-lived worker branches lean — an agent working on src/Auth/** does not need to materialize the entire repository.

Concurrency model

Each work unit has its own branch and therefore its own directory. The CAS blob store (under Workspace:CasRootPath, defaulting to {SeedRepositoryPath}/.nodalmerge/cas) is the shared read-only layer. Per-branch directories are the write-isolated layer — parallel agents never compete for file locks. Workspace:MaterializerConcurrency controls the number of parallel I/O threads used during CAS reconstruction (default: 4). Increase this on machines with fast NVMe storage and many concurrent agents.

Branch directory cleanup (WorkspaceCacheManager)

Branch working directories are treated as ephemeral cache entries — any evicted directory can be reconstructed later from the latest repository snapshot + CAS. WorkspaceCacheManager runs a best-effort orphan sweep automatically at host startup (Completed/Merged/Cancelled work units only), and exposes REST endpoints for manual control: A Cancelled work unit is always safe to evict — its changes were never merged, so there’s nothing to preserve. A Completed/Merged work unit is only evicted once the repository’s own snapshot postdates the work unit’s last update — i.e., once a resync (above) has actually captured that work unit’s contribution, so reconstructing later won’t lose anything. For a multi-repo work unit, this check resolves the work unit’s own registered repository rather than always the global default, so eviction/rematerialization checks the right repository’s snapshot.
This is currently REST-only — no VS Code UI panel or MCP tool surfaces branch directory count, disk usage, or manual evict/materialize/gc today. It’s intentionally automatic-only for typical use.

Observability

If Workspace:SeedRepositoryPath, the CAS blob store, or the repository op-log service aren’t all configured together, the CAS dual-write (blob + op-log write, alongside every file write/delete in a branch directory) is silently skipped — this is a legitimate, common, intentional deployment choice, not a misconfiguration (plenty of setups don’t need the audit trail at all). The first time this happens, an Information-level log line is written naming which of the three pieces is missing; it is not repeated on every subsequent file operation, and stops appearing entirely once configuration is completed at runtime (e.g. via POST /studio/workspace/switch).

Repository binding and re-linking

Every workspace folder you open is bound to a registered repository — its repository room, where that repo’s work units, proposals, findings, and constraints live and replicate. Studio derives the repository’s identity deterministically from its root commit history, computed locally with no server round-trip: the same clone on two machines resolves to the same room, and a fresh checkout binds correctly even with the room server offline. Remotes are not part of the identity — they only break ties between forks. Binding usually just works, and Studio records how it resolved so it can be trusted later: Studio never silently re-resolves an identity it has already resolved. Once a folder is bound, it stays bound until you ask to change it. This is deliberate: re-resolving “helpfully” on every open is how a workspace silently drifts into the wrong room. The Artifact Explorer shows which repository room you’re currently in, so a mismatch is visible rather than hidden.

Re-linking

When the binding is wrong — the folder matched the wrong repository (a fork, a moved clone, an ambiguous case), or nothing is registered yet — run NodalMerge: Re-link Repository… (or the Re-link action in the Artifact Explorer). It offers whichever of these fits your situation:
Splitting a folder into a new repository orphans the content already in its old room. Studio’s shared graph has no delete primitive, so the old room’s work units and artifacts are not removed — they simply stop being reachable from this folder. Re-link surfaces a modal confirmation naming what will be left behind before it does this, so a split is always a deliberate choice, never an accident.
Bindings are per workspace folder. If you switch the folder VS Code has open, Studio notices and offers to restart the host so the new folder binds cleanly rather than running against the previous one’s room.

Git integration

Two WorkspaceOptions flags control whether Studio acts on the filesystem changes agents produce: Both are intended for headless CI/CD pipelines — see Guides → Headless peer for the typical configuration pattern.

Configuration reference

All keys live under Workspace in appsettings.json: