Shared daemon
One Fuse daemon per repository holds the warm compilation as a shared asset, so multiple agent sessions and the ambient hooks read one resident workspace instead of each paying its own cold start and memory.
A Fuse daemon is one long-lived fuse host process per repository root. It owns the resident workspace (the live Roslyn compilation) and the index write path (open, reconcile, syntax-first cold start, and background semantic upgrade) so that every client reading the same repository shares one warm compilation and one writer on .fuse/fuse.db. fuse mcp serve enables the shared daemon by default; set FUSE_DAEMON=0 to hold an in-process workspace and index instead.
What runs
- One daemon per root.
fuse hostacquires a single-instance lock keyed by the repository root at startup; if another daemon already owns the root, the second process exits cleanly ("a daemon already serves ...; not starting a second"). A spawn race resolves to exactly one daemon. fuse mcp servedoes not hold its own resident workspace, warm refactor/doctor solution, pooled capture-check worker, or index writer by default. It ensures a daemon is running for the working-directory repo (spawning one on demand) and delegates resident-grade checks, capture-backed oracle checks, staged refactors, live doctor, and store-backed index writes to it over the pipe (fuse/checkOverlay,fuse/checkCapture,fuse/refactor,fuse/doctor,fuse/openIndexed,fuse/index). When the daemon cannot start, serve falls back to its in-process paths, so a one-shot CLI and a no-daemon environment still work. SetFUSE_DAEMON=0to skip daemon delegation entirely.- The ambient-verification hooks (
fuse check --delta,fuse gate) already connect to the same per-root pipe, so they read the daemon's workspace too.
The payoff is measured on NodaTime: one daemon holds the resident workspace at about 109 MB; a second daemon on the same root exits rather than duplicating it, so two sessions cost one workspace, not two.
Index writes (explicit fuse index, syntax-first cold start, reconcile, and background semantic upgrade) run in the shared daemon when FUSE_DAEMON is on (the default for fuse mcp serve). Non-owner MCP clients call fuse/openIndexed over the pipe, then open .fuse/fuse.db read-only locally for queries. Daemon-less CLI commands (fuse find, fuse index) and FUSE_DAEMON=0 serve use the in-process IndexCoordinator: one writer queue per root plus a cross-process fuse-index-writer-{hash} mutex. When multiple processes contend for the same root without a daemon, the loser gets index_busy: and the agent retries; warm reads use OpenForReadAsync and do not take the writer mutex.
How to see it
Ask a connected agent to call fuse_workspace with action=status. The response names the
daemon serving the root, when one is:
daemon: PID 48612, uptime 45s, RSS 109 MB (fuse host 4.1.0)When no daemon serves the root, the line reads daemon: none (this process serves the workspace directly).
How to stop it
- Idle shutdown. A daemon spawned on demand stops itself after
FUSE_DAEMON_IDLE_MINUTESwith no connected clients (the idle clock resets while a client is connected), so a daemon does not linger holding a workspace. A manually runfuse hostdefaults to no idle shutdown (window 0). - Version handshake. A client whose protocol version does not match the daemon's treats it as no daemon and refuses cleanly, so a stale client after an upgrade triggers a clean restart rather than a malformed exchange.
- Manual stop. Stop the
fuse hostprocess by its PID (from the status line above).
Always-on warm service (opt-in)
R38 and R39 warm and keep a repo fresh within an agent session and, with the idle window, across quick successive sessions. The remaining tier is warm before a session even opens, across reboots: an opt-in background OS service installed only by an explicit fuse warm --service install (a Windows service, launchd agent, or systemd user unit), never by fuse mcp install. It keeps a store-backed daemon alive and re-warms a bounded LRU of recently-used repos. Guardrails: store-backed only (never a resident Roslyn compilation, which stays opt-in per repo), a hard LRU cap (the last few repos), battery- and load-aware (pause pre-warming on battery or under load), idle-evict, and a clean fuse warm --service uninstall. It stays opt-in for the agent-first default; a promotion to opt-out requires measuring idle RSS, CPU, and battery impact on a laptop and showing them light.
Eager warm-on-start
The index warms itself the moment a repo is served, before any tool call (R38): at fuse host (or in-process fuse mcp serve) start the daemon kicks off a background syntax-first index for the served root, so the first read hits a warm or a bounded-building index (R27) rather than triggering the cold build. It runs through the shared cold-start coordinator, so the eager start and a first read that races it share one build, never two. It is best-effort (a contended or unavailable store falls back to the first read's bounded build) and default-on; opt out with FUSE_EAGER_INDEX=0, or warm explicitly with fuse warm <path>. The persistent store keeps a repo warm across a session (a reopened daemon is open-plus-reconcile, not a rebuild), and the idle-shutdown window holds the session daemon warm for quick successive sessions. Focus-first priority ordering and per-scope semantic readiness (warm the queried and git-active files first) are delivered by the selective semantic load in the faster-indexing work.
Warm Solution for refactor and doctor
The compiler tools that need a design-time MSBuild workspace - fuse_refactor and fuse_workspace action=doctor's live load - reuse one held Roslyn Solution per root instead of re-opening MSBuildWorkspace on every call (R42). That fresh open is essentially the whole latency of those tools (measured 4-26s), and it was re-paid from scratch each call. Because Roslyn solutions are immutable snapshots, a held solution is forked per refactor for free, and a doctor diagnosis reads its per-project outcome directly. The first call in a session loads and caches; the second and later calls skip the load (measured on the Fuse repo: refactor first ~23.7s, warm ~0.45s; doctor ~13.9s, warm ~0.87s; environment-dependent).
When the daemon or an in-process resident server owns a watcher, a settled change to a tracked C# source file evicts the held solution (R54). A clean warm call then returns without the fallback directory scan. Build output and generated trees such as bin and obj do not evict it because they are outside that signature's source set. The one-shot and no-watcher paths keep the pruned .cs scan, so a changed source tree still reloads before a refactor or live doctor result.
The held states have separate owners. A resident daemon holds its live workspace for overlay checks. A store-backed fuse_check that has no resident overlay can start the pooled capture worker in the MCP server process. Those paths are mutually exclusive for one root, so there is no single daemon process that holds the resident workspace, warm solution, and pooled worker together. The R55 RSS probes recorded 234.41 MB for one Fuse host and 576.80 MB across separate Fuse and eShopOnWeb host processes; the environment-dependent measurements are in the roadmap progress log. The existing per-cache LRU caps remain the memory bounds for their owning processes.
Correctness comes from a cheap freshness signature - the count, newest write time, and total size of the tracked .cs files under the target's directory, excluding build and vendored trees. Any edit, addition, or deletion changes it and forces a fresh load, so a reused solution always yields the diff a cold load would, and a changed tree pays the same cost as before (never worse). Memory is bounded because a held solution is a full compilation set (hundreds of MB on a large repo): a hard LRU cap (FUSE_WARM_SOLUTION_CAP, default 3) and an idle window (FUSE_WARM_SOLUTION_IDLE_MINUTES, default 30) each evict and dispose a workspace, and an evicted root falls back to a fresh per-call load. The held-solution RSS ceiling was about 594 MB at cap 3 on the Fuse repo. This is a read/compile cache: it never writes the working tree (the D2 apply path is the only writer) and never writes the index (the D13/R19 single-writer path is untouched).
For fuse_workspace action=doctor there is a faster path still: the per-project load diagnosis (the achieved tier, the selected solution, and each project's load outcome) is stamped into index_meta at index time (R43), so doctor reports it straight from the warm index in sub-second time without any MSBuild load - a single stamped row read, independent of repo size. Because the stamp is written by the same code the live diagnosis uses, it matches what a live load would report, and it reflects exactly what was indexed. A live load runs only when the caller passes refresh=true (--refresh on the CLI) or no stamp is present yet (an older index, or a store still building); the warm-Solution cache then keeps that live load fast on the second call.
MSBuild toolchain warmup
The first MSBuild-touching call per process pays a fixed multi-second warmup: registering the MSBuild locator, then the first MSBuildWorkspace open (JIT, MSBuild assembly load, SDK resolution). In a long-lived daemon that cost otherwise lands on the first fuse_refactor or full fuse_check of a session. R44 warms it at startup: in the background, the daemon (and in-process fuse mcp serve when not delegating) discovers the served root's solution and primes it into the warm-Solution cache, which registers the locator and loads the solution in one step. So the warm load also populates the cache, and the first real refactor or doctor hits a warm solution rather than a cold load (measured on the Fuse repo: the ~5.9s background warmup moved the load off the first refactor, which then reused the primed cache with no reload). It is fire-and-forget and best-effort - a failure is swallowed so startup is never blocked - deduped with the first real load by the cache, and default-on; opt out with FUSE_MSBUILD_WARMUP=0.
Pooled build-capture check worker
Without a resident workspace (the store-backed default), an oracle fuse_check answers from a captured compiler log by spawning a worker that rehydrates the compilation from the log, checks one file, and exits. In an agent loop that runs a check after every edit, that rehydrate-and-exit is repeated per check, several seconds each. R48 pools the worker: one long-lived worker per captured compiler log, kept alive across checks. R53 first reads only the source ownership metadata for the captured C# compiler calls, then rehydrates the owning project on the first request for one of its files. A later request for another project rehydrates that project only. Each project's captured reference closure remains part of its compilation, so a cross-project call is checked against the same references as a full-log rehydration. The worker then answers many check requests over its stdio - each speculative edit forks the in-memory document against the held compilation - so later checks for the same project skip rehydration.
Check honesty is preserved exactly: the pooled worker runs the identical fork-and-diagnostics code the spawn-per-call path runs, so a pooled verdict equals a spawn-per-call verdict (false-green 0 unchanged). The pool is bounded by a hard LRU (FUSE_CHECK_WORKER_CAP, default 2) and an idle window (FUSE_CHECK_WORKER_IDLE_MINUTES, default 30), stopping the worker on eviction; a cold, absent, or failed pooled worker falls back to the spawn-per-call path, so it is never worse than today. This targets the store-backed default; the resident path (R42) already holds a live compilation.
Live incremental index
The daemon keeps the index fresh as the tree changes (R39). A debounced file-system watcher over the served root (which also fires on .git/HEAD and .git/index, so branch switches and pulls are caught) reconciles the changed files into the store through the single-writer coordinator, so a read finds the index already fresh and pays no per-read reconcile cost. The reconcile is overlap-guarded (one at a time) and best-effort (a failure never tears down the daemon); a periodic safety reconcile catches events a watcher dropped (network drives), and on-read reconcile stays the backstop, so freshness never depends on the watcher being perfect. Default-on with the daemon; opt out with FUSE_WATCH=0.
Bounded cold start
The daemon builds the index once per session and keeps it warm; it never repeats a cold build. The first read on a cold repo does not block for the full syntax build (tens of seconds on a large repo). Instead the syntax-first build runs in the background, deduplicated per root, and the read waits only up to FUSE_COLD_READ_DEADLINE_MS (default 2500 ms). If the build finishes within the deadline (a small repo) the read serves results; if it outruns the deadline the read returns a bounded index_state: building_syntax header with files_indexed, and the build continues so a second read in the same session serves the warming index. This keeps the first read fast and visible rather than a silent multi-second block, and the persistent store means a reopened session is open-plus-reconcile, never a full rebuild.
Daemon inventory and lifecycle
Exactly one daemon serves a root: the single-instance lock (a per-user named mutex keyed by the root hash) resolves a spawn race to one owner, and a redundant process exits cleanly. A daemon spawned on demand shuts itself down after FUSE_DAEMON_IDLE_MINUTES with no connected clients (idle shutdown), so servers do not accumulate across respawns.
Running daemons are visible. Each fuse host writes a small descriptor (served root, process id, version, start time) under {user-data}/daemons/ on start and removes it on shutdown; fuse_workspace action=doctor lists them (running daemons: N, one line per daemon with its PID, version, and served root), pruning any descriptor whose process is no longer alive. So a stale or version-mismatched daemon is visible rather than an invisible orphan.
In-session upgrade is staged with a health check and rollback (R33). The detached updater records the previously installed version, runs dotnet tool update, then health-checks the new binary (it starts and reports a version); on failure it rolls back to the previous version and warns, so an unattended upgrade never bricks the tool. A healthy switch logs reindex scheduled on next open (the extraction-contract check rebuilds the index if the contract changed, R22/R23).
In-session upgrade is a clean handoff, not a kill race. Auto-update (default-on for mcp serve) launches a detached updater that waits for the current process to exit before running dotnet tool update, so the running server releases its own locked binary on exit rather than being killed mid-flight; sibling sessions are left running. After the client reconnects it finds one daemon at the new version (the single-instance lock) serving a working index (rebuilt if the extraction contract changed, R22/R23), with no manual .fuse wipe.
Why one daemon
The .NET ecosystem already normalizes a per-repository server that amortizes cold start (the compiler server, MSBuild node reuse), including its failure modes: orphaned processes, spawn races, and stale versions after an upgrade. Fuse's daemon addresses each by construction: the single-instance lock (one owner, crash-recoverable via an abandoned-mutex takeover), idle shutdown (no lingering process), and the protocol-version handshake (a stale client refuses). The warm compilation becomes a shared asset, so a second agent session on a repository is warm immediately.
Resident workspace
How Fuse holds a live compilation in memory so an edited file is reflected in answers within a second, without a re-index or a build, and how it degrades to the store when no live compilation is available.
Capability and Plugin Model
How Fuse keeps language behavior in plugins and resolves it by file extension through capability registries.