magus v0.4.2 is out. See what's new
¶ View markdown source · ✎ Suggest an edit
10 min read

Daemon and concurrency

Concurrency

Magus runs project builds in parallel up to a configurable limit.

magus run build --concurrency=4
magus config set key=concurrency,value=4
MAGUS_CONCURRENCY=4 magus run build

That limit is per process. The machine budget is the one that spans them: every magus run and magus affected takes its concurrency slots and its declared memory_mb from a budget the daemon owns, so runs in separate worktrees queue behind each other instead of each admitting a full machine's worth of work. A run starts the daemon if none is up, because nothing else can arbitrate it, and a daemon started that way stops itself after ten minutes in which nothing held a claim and no client asked it for anything. One you start with magus server start stays up until you stop it. See Concurrency and MGS3009.

The daemon arbitrates that budget; it does not run your work. A top-level magus run executes in your own process and prints to your own terminal. Nested magus invocations still adopt into their parent's pool.

magus status shows the live pool state, current slot usage, and the machine budget with every claim and waiter across worktrees.

Daemon

Every magus run is a short-lived process with its own concurrency limiter. The daemon holds workspace state in memory, hosts shared services, and owns the one budget that keeps those separate limiters from oversubscribing the machine between them.

magus server start          # detaches and returns once it is accepting
magus server start --foreground # blocking, for a supervisor
magus server stop           # graceful shutdown; waits for in-flight work
magus server stop --services # stop the daemon's hosted services, leave the daemon up
magus status                # live pool + MCP endpoint health (reports the reason when down)
magus status -W 15s         # poll and reprint every 15 seconds

magus server start detaches by default: it spawns the daemon, waits until it is accepting, prints the pid, and returns 0 - including when one is already running, so a script can chain on it. Pass --foreground to run it blocking in the current process, which is what a supervisor wants (see Keeping the daemon running).

Two transports

The daemon is a single process with two listeners, one per audience. A Unix domain socket is the local control plane (the proc RPC that dispatches jobs, answers status, and adopts nested calls into one concurrency pool); it is fast and private (0700), and the local CLI - including the --probe=liveness and --probe=readiness checks - uses it. An HTTP server on mcp.address serves the clients that cannot reach a Unix socket: agents over MCP at /mcp, and orchestrators or scripts at the /livez, /readyz, and /healthz probe routes.

How a local command reaches the daemonThe magus CLI dispatches over a private unix domain socket into the concurrency pool, the workspace registry, the run registry and the service registry. Sharing a graph spawns a separate short-lived loopback server.MAGUS DAEMONHow a local command reaches the daemonDISPATCHSPAWNSSHELLmagus CLIrun, status, queryIPCUnix socket0700, internal/procConcurrency poolWorkspace registrygraph, SCIP, cacheRun registrylive runsService registryhost servicesLoopback servergraph export --openSHARED DAEMON STATESPAWNED ON DEMAND
The HTTP surface, and the one route without a guardAgents and the console reach /mcp, the /api/v1 read views, the review and plan routes and ten typed Connect services through the DNS-rebind, bearer-token and CORS guard. POST /api/v1/diff/session carries the human half of a paired review and POST /api/v1/share opens the LAN listener; both are loopback only. The health endpoints answer probes directly, with no authentication.MAGUS DAEMONThe HTTP surface, and the one route without a guardUNGUARDEDCLIENTAI agentsClaude Code, IDECLIENTConsole PWAdashboard, reviewCLIENTkubeletscripts, probesGATEGuardrebind + token + CORS/mcpagent tools, SSE/api/v1 read viewsgraph, events, insight, outputs/api/v1 review + plansdiff, session, plan, ledgerConnect RPCten typed services/livez /readyzno authenticationAUTHENTICATEDOPEN TO ANY CALLER

The Unix socket is the source of truth for daemon state; the HTTP health routes are a thin wrapper that answers by querying that same socket. But only an actual HTTP request proves the agent-facing endpoint is bound and serving - a socket check cannot detect an HTTP bind failure - so the two listeners can diverge, which is why magus status reports each separately.

This page zooms in on the two transports; the HTTP server also carries the agent-facing MCP tools, the read-only console routes the browser apps use (all reading the same warm knowledge graph), and one bearer-gated job-control service for maintenance jobs - the daemon's only mutating HTTP surface. The full-system view - clients, guards, shared state, and the progressive web app - is the architecture diagram in the README.

Sharing the console to a phone

The console can serve a read-only live view to a phone on the same network. The "Share to phone" action in the console opens a small dialog with a QR code; a phone that scans it loads the console and gets a read-only view of the dashboard, status, activity, and logs. It is opt-in and off by default: no listener faces the network until you click it.

How it works, and where the guards sit:

  • Loopback-only trigger. The share button POSTs /api/v1/share on the daemon's loopback HTTP server. That route requires the local peer and the existing bearer token (the cli or a connector token), so only the already-authenticated console on your own machine can open a share. A network client cannot reach it.
  • Ephemeral, time-boxed LAN listener. On success the daemon picks the machine's private LAN IPv4, binds a NEW listener on an ephemeral port, and serves ONLY the read surface there: the console static assets, the read JSON routes (status, events, insight, outputs, output), and the read-only Connect services (activity, metrics). It does NOT serve /mcp, the share endpoint, or any mutating route. The listener closes automatically after 15 minutes, and on daemon shutdown.
  • Short-lived, read-scoped token. Each share mints a fresh token in the mgs_ family, hashed at rest and stamped with a read-only scope. The listener is bound 1:1 to that one token: it accepts nothing else. The cli token, connector tokens, an expired share token, and a share token from any previous share session are all rejected. Every share supersedes the last - a repeat click revokes the old token and closes its listener before returning a new one, so there is only ever one live share.
  • The daemon verifier never accepts a share token. The verifier that guards /mcp and every mutating console route matches only the cli and connector tiers, so a share token is refused there. The read scope cannot reach a mutating surface.
  • Same-origin, so CORS never engages. The phone loads the console FROM the share listener, then its data fetches go back to the same origin. No cross-origin request is made, so the daemon's CORS middleware is never involved and is untouched by this feature. The standing loopback server stays bound to 127.0.0.1 as before.

What a leaked QR or URL is worth: at most a few minutes of read-only visibility into one workspace's dashboard, status, activity, and logs, from a device already on your LAN, and only until the share expires or you open a new one (which kills the old token). It cannot run a build, mutate the cache, reach /mcp, or read anything the loopback console does not already show. There is no standing exposure: when the timer fires the listener is gone and the token validates nowhere.

Health: what magus status reports

magus status is the health surface. It reports two listeners separately, because they can diverge:

  • the daemon (daemon pid ... with the live pool, or daemon: off) - the socket the daemon dispatches build jobs on;
  • the MCP endpoint (mcp endpoint block with state: serving | not-ready | unreachable | disabled) - the HTTP endpoint agent hosts connect to. Nothing starts this on its own, so an unreachable MCP endpoint is the usual reason "the magus tools disappeared" from an agent. See MCP.

For scripts and Kubernetes probes, magus status --probe=<kind> exits 0 healthy / 1 unhealthy. The kinds are liveness (the daemon answers), readiness (a workspace is loaded), and mcp (the MCP endpoint is reachable), and they are comma-combinable - --probe=liveness,mcp fails if either the daemon or the endpoint is down. The daemon also serves /livez, /readyz, and /healthz over HTTP on the MCP port.

Kubernetes and container probes

The daemon exposes both HTTP endpoints (for httpGet probes) and an exec form (for exec probes), split along the standard liveness/readiness lines:

K8s probe endpoint passes when
liveness GET /livez (alias /healthz) the daemon answers - independent of warm-up
readiness GET /readyz the daemon answers AND a workspace is loaded
startup GET /livez the daemon has come up (reuse the liveness URL)

Liveness is deliberately independent of warm-up state: it returns 200 as soon as the daemon answers, even before any workspace is loaded, so a slow first index never crash-loops the pod. Readiness gates on a loaded workspace, and GET /readyz?workspace=<root> pins it to one specific workspace (returns 503 until that workspace is warm). Because these endpoints are served by the MCP HTTP server itself, a successful probe also proves the MCP endpoint is listening.

The status code is the signal - a kubelet reads only that. Because these routes are unguarded, their bodies carry nothing identifying: /livez and /healthz answer a bare ok or unavailable, and /readyz returns a JSON {"ready": ..., "components": [...]} where each component has a coarse status (ok, degraded, down, disabled) plus a generic, quantitative detail such as 1 loaded or 0 of 4 up to date - counts and state phrases only, never workspace roots, project or service names, filesystem paths, or the daemon PID. For the identifying per-subsystem view (workspace roots, per-project symbol-index freshness, named service state), read the bearer-authenticated magus.status.v1alpha1.StatusService/GetStatus Connect route instead (the typed replacement for the retired GET /api/v1/status; magus status reads it the same way).

The endpoints bind to 127.0.0.1 by default, which the kubelet cannot reach; set MAGUS_MCP_ADDRESS=0.0.0.0:7391 (or mcp.address) so probes can hit the pod IP.

# pod spec
env:
  - { name: MAGUS_MCP_ADDRESS, value: "0.0.0.0:7391" }
startupProbe:
  httpGet: { path: /livez, port: 7391 }
  failureThreshold: 30
  periodSeconds: 2
livenessProbe:
  httpGet: { path: /livez, port: 7391 }
  periodSeconds: 10
readinessProbe:
  httpGet: { path: /readyz, port: 7391 }
  periodSeconds: 10

The endpoint requires no auth token (health routes are exempt); keep the port on the pod network, not the public internet - see MCP security.

Keeping the daemon running

The daemon is a local process, and the MCP endpoint is only up while it runs. Pick one way to keep it alive:

A shell profile (simplest, good while iterating on magus itself). Ensure a daemon is up whenever you open a shell by adding this to ~/.zprofile, ~/.bashrc, or equivalent:

magus status --probe=liveness >/dev/null 2>&1 || (magus server start &)

It is a no-op when a daemon is already running. This is the least durable option - the daemon dies when the last shell that started it exits - but it needs no system integration and always runs whatever magus is on your PATH, which is convenient when you rebuild magus often.

A systemd user service (Linux). Supervised, survives logout and reboot:

# ~/.config/systemd/user/magus.service
[Unit]
Description=magus daemon

[Service]
ExecStart=%h/.local/bin/magus server start
Restart=on-failure

[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user enable --now magus
loginctl enable-linger "$USER"   # keep it running when you are not logged in

A launchd LaunchAgent (macOS). The launchd equivalent, loaded at login and kept alive:

<!-- ~/Library/LaunchAgents/com.magus.daemon.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key><string>com.magus.daemon</string>
    <key>ProgramArguments</key>
    <array>
      <string>/usr/local/bin/magus</string>
      <string>server</string>
      <string>start</string>
    </array>
    <key>RunAtLoad</key><true/>
    <key>KeepAlive</key><true/>
  </dict>
</plist>
launchctl load ~/Library/LaunchAgents/com.magus.daemon.plist

Point ProgramArguments at your actual magus path (which magus). With a supervised service, restart it after upgrading magus (systemctl --user restart magus / launchctl kickstart -k gui/$UID/com.magus.daemon) so it runs the new binary.

Hosting shared services

The daemon is also the host for shared services (see services). When a magus run needs a service op as a dependency and a daemon is running, the daemon starts that service once and keeps it warm across separate invocations, so a shared Postgres stays up between magus run test:a and a later magus run test:b instead of restarting each time. Services are keyed by a configuration fingerprint, so identical definitions in different projects resolve to one instance. A per-process daemon (the one an unattended magus run spawns for itself) does not host services; only the stable magus server daemon does, which is why cross-invocation sharing needs it running.

The daemon reaps a service after an idle window once its last dependent releases (a 30 minute default, overridable per service via Service.idle), and it records each hosted service's stop command so a new daemon reaps orphans a previous one left behind if it was killed uncleanly. magus server stop --services clears every hosted service on demand (to drop stale state or free held ports) without shutting the daemon down.

Configuring the socket address

The socket address is resolved in priority order:

  1. --daemon-address <unix://...> flag
  2. MAGUS_DAEMON_ADDRESS environment variable
  3. daemon.address in magus.yaml
  4. Stable per-workspace default (unix://<sock-dir>/magus-daemon.sock)

The default socket is named without a PID so server stop and server status can find it without discovery.

To pin a socket address in config:

magus config set key=daemon.address,value=unix:///run/user/1000/magus/daemon.sock
daemonconcurrencymagus servermagus statussocketpoolmagus.yamlshared serviceskeep-warmhealthlivenessreadinessprobeskubernetes
Last updated (cc3d21b1)
Earlier changes on this page (4)

Full history ↗ · Blame source ↗

Glossary

Workspace

The magus root directory that owns a set of projects and shared config; the unit magus operates over. See workspace.

Project

A directory magus recognizes as a unit of work (it has a magusfile); the unit of caching, scheduling, and dependency tracking. See workspace.

Target

A named operation (build, test, ...) you invoke with magus run <target>; it may compose a spell's tool-native operations and depend on other targets. See targets.

Op

A single tool-native command a target composes (long form: operation); the middle of the work hierarchy (Spell to Op to Target). See operations.

Cache

The content-addressed store magus consults before running a target, so unchanged work is skipped. See cache.

Affected

The set of projects touched by a change; magus affected <target> runs a target only over them. See affected.

Sandbox

The restricted filesystem and environment a target runs in, so builds stay reproducible and side-effect-free. See sandbox.

Service

A long-running or shared process magus manages across runs, distinct from a one-shot target. See services.

Daemon

The background magus host that owns shared state such as services and the warm knowledge graph. See daemon.

CI

An ordinary magusfile-defined target you compose yourself with magus\needs - magus does not hardcode its stages. Magus.RunCI treats it specially only in that it strips the rw charm, it is the anchor magus affected ci keys off, and a selected scope with no project declaring it is a load error rather than a silent no-op. See targets.

Span

OpenTelemetry's name for one unit of work under a trace - a target execution, whose sub-operations are child spans. An output reference points at a span's captured output. See telemetry.

Pool

The concurrency pool: the shared set of slots that caps how many targets run in parallel on one machine. Its capacity defaults to MAGUS_CONCURRENCY, then 4 on GitHub-hosted runners, then min(NumCPU, 8); magus status and the dashboard report it live. See daemon.

Slot

One unit of the pool's capacity. A target acquires the slots it needs to run (most take one) and releases them when it finishes; the pool tracks capacity (total slots), running (acquired), and queued (blocked). See daemon.

Concurrency

How many targets run at once. It is bounded by the pool's capacity and set with --concurrency, MAGUS_CONCURRENCY, or the concurrency config key. See daemon.

Health

The at-a-glance daemon state derived from the pool: healthy when the pool is reporting, degraded when it reports an error, down when there is no pool. The dashboard color-codes each state. See daemon.

Knowledge graph

The queryable graph of a workspace's spells, targets, docs, and code relationships; query it with magus query/explain/path. See knowledge.

Insight

The reports magus derives over the graph and history (hotspots, affinity, ownership, trend, volatility, unreferenced). See insight.

Session

One magus process's recorded facts - the targets it finished, their outcomes, and the lease it acted as - kept in a repo-scoped store every worktree shares. magus session lists them; the store prunes itself by last-fact age.

Lease

One row of the lease ledger: a piece of work an orchestrating agent handed out, with its goal, the checkpoint it was cut against, and the paths it owns or must not touch. The ledger records; the agent guard is what reads those facts back when grading a write. See doctrine.

Conventions

Placeholders

Angle brackets mark a value you replace with your own - never type the brackets:

magus run <target>
magus completion <shell>    # e.g. bash, zsh, fish

<target>, <path>, <shell>, <name> and the like are stand-ins, not literal text.