#Runtime topology
On this page
ContainersChannelsOne command end to endInside jamdDeployment modes for an agentStartupShutdownFiles on diskJam runs as a long-lived local daemon, jamd, plus thin clients. jamd owns every Band connection, every coding-agent process it starts, the encrypted store, and all routing. The desktop app, the band/jam CLI, and the hooks and skills of attached coding agents are clients that send requests to jamd over a private local socket.
#Containers
Every process on one machine and the channel between each pair:
| Process | Binary | Who starts it | Who stops it | Owns |
|---|---|---|---|---|
| Desktop | band-desktop inside Band.app |
The user | The user; quitting does not stop jamd |
Windows, tray, notifications, updater, OAuth sign-in window. No runtime state. |
| Daemon | jamd |
The OS login service (jam daemon install: launchd on macOS, systemd user unit on Linux, Task Scheduler or Run key on Windows), the desktop at launch, or jam daemon run |
/v1/shutdown, Ctrl-C, or the service manager |
All Band connections, all owned provider processes, the store, all routing and durable state |
| CLI | band (canonical) and jam (compatibility alias), both from the jam package in bins/jam |
The user, scripts, and the hooks of attached coding agents | Exits after each command, except long waits such as jam inbox --wait |
Nothing durable |
| Owned provider | Stock codex, copilot, claude, opencode, or an ACP agent command |
jamd, through a jam-host adapter |
jamd |
Its own conversation state on disk |
| Docker Sandbox microVM | Docker's sbx |
jamd, through crates/jam-host/src/sandbox.rs |
jamd parks it with sbx stop and removes it only on reset or cleanup |
Guest filesystem and processes |
| Attached coding agent | The user's own Claude Code or Copilot CLI | The user | The user | Its own session. Jam watches host_pid only. |
The desktop never starts a provider process: AGENTS.md states that "Tauri never owns runtime processes." And the desktop never links the daemon crates: just no-daemon fails if jam-desktop's dependency tree contains jam-manager, jam-core, jam-store, jam-transport, jam-host, jam-usage, jam-discovery, ccusage, or mdns-sd. See the generated crate graphs for the exact closure of each binary.
#Channels
| Channel | Between | Format | Bounds |
|---|---|---|---|
| Tauri IPC | WebView and Tauri core | Specta-generated commands (apps/desktop/src/lib/bindings.ts) |
Every command must be in both isolation allowlists (src-isolation/index.js, src/lib/isolation.ts) and in collect_commands!, or the isolation layer rejects it before Rust sees it |
| Local control socket | Desktop core or CLI and jamd |
HTTP/1.1 + JSON; routes are constants in crates/jam-wire |
<app>/jam.sock with mode 0600 on Unix; a current-user-only named pipe on Windows (crates/jam-windows-ipc). At most 16 concurrent unary requests per jam_client::Client (MAX_UNARY_IN_FLIGHT). Default unary timeout 45 s on the daemon (DEFAULT_UNARY_TIMEOUT_SECS), with longer per-route budgets |
| Event stream | jamd to clients |
GET /v1/events, newline-delimited JSON, starting with a peer snapshot |
Held open; not under the unary timeout; closes on daemon shutdown |
| Band REST | jamd and Band |
HTTPS JSON | One shared BandHttpClient for the whole process: at most 16 in-flight REST requests and 8 idle sockets per host |
| Band agent WebSocket | jamd and Band, one per running agent |
Phoenix channels over WSS | Not bounded by the REST budget; every agent keeps its own listener |
| Band human WebSocket | jamd and Band, one per signed-in account |
Phoenix channels over WSS | Feeds live room changes to the desktop |
| Provider stdio | jamd and an owned provider process |
Codex app-server JSON-RPC, ACP JSON-RPC, Claude Code stream-json, or the Copilot SDK protocol | Per-adapter shutdown allowance (Codex app-server TEARDOWN_TIMEOUT: 3 s) |
| Sandbox exec | jamd and a microVM |
sbx exec -i <name> -- <runtime> wrapping the same stdio protocol |
Read-only sbx version, diagnose, and ls: 12 s (SBX_INTERACTIVE_TIMEOUT). Ordinary lifecycle commands: 300 s (SBX_COMMAND_TIMEOUT, from DOCKER_SANDBOX_LIFECYCLE_TIMEOUT_SECS). sbx create: 600 s (SBX_CREATE_TIMEOUT), because first use can download the agent image |
| Attached delivery | jamd and a user-run agent |
Claude Code teammate mailbox file, pull leases read through the CLI, or Copilot extension bridge jobs | Mailbox writes use a lock with a bounded retry budget |
#One command end to end
One ensure_peer call from a click in the desktop to a state change in jamd. The crosswalk lists the same path for every method.
The same Control trait appears three times on this path, which is what makes the layering hold together:
jam_manager::ManagerimplementsControlfor real (impl Control for Managerincrates/jam-manager/src/manager.rs).jam_daemon::routerexposes anyArc<dyn Control>over HTTP.jam_client::ClientimplementsControlagain by sending HTTP requests (impl Control for Clientincrates/jam-client/src/lib.rs).
Clients depend on the trait and the wire crate only. Test doubles implement Control directly: crates/jam-daemon/tests/roundtrip.rs has several.
Errors travel back through the same layers. jam-daemon's err_response maps each ControlError variant to an HTTP status and an ErrorResp body. jam-client maps it back to the same variant. jam-ipc converts ControlError to IpcError, whose Display strings are the user-facing messages.
#Inside jamd
The daemon's internal composition. jamd.rs injects each port into the manager through jam_manager::Deps.
jamd.rs is the composition root and selects the principal injected implementations. It builds a jam_manager::Deps with the store and queue backend, a Band transport factory, the host factory, a process-liveness probe, the work-item source factory, the room board backend, the Copilot bridge registry, and the discovery provider. It then adds further ports through builder methods such as with_oauth and with_sandbox_inventory. Manager construction still installs some concrete services itself, such as HostRuntimeSandboxRemover, and recovery code constructs WorkspaceManager directly.
The host factory is the seam that selects an adapter for each host session. build_host in jamd.rs is an ordered if chain:
- Legacy owned PTY: rejected with an error.
- Owned Claude Code CLI:
ClaudeCodeOwned. - Owned ACP or OpenCode:
AcpHost. - Owned Codex app-server:
CodexAppServerRegistry::build. A daemon-scoped registry makes every Managed Shared Codex session on one runtime host share one provider process. - Owned Copilot SDK:
CopilotSdkRegistry::build. - Attached Copilot (
session.provider == COPILOT_PROVIDER):CopilotCli, which queues bridge jobs for the Copilot CLI extension. - Attached Claude Code (
peer.host == "claudecode"):ClaudeCode, which writes the per-session teammate mailbox. - Anything else:
Generic, a pull-only adapter with a no-opdeliver.
The order matters. Owned Claude Code must come before the peer.host == "claudecode" fallback, or an owned session would be delivered through the attached mailbox. A comment at that branch records the reason. The providers chapter covers each adapter.
#Deployment modes for an agent
An agent runs in one of three arrangements. They differ in who owns the process, where the files live, and what isolates the agent from the host.
The processes and directories involved in each mode:
| Attached | Owned, host-native | Owned, Docker Sandbox | |
|---|---|---|---|
| Process owner | The user | jamd |
jamd, through sbx |
| Executable | User's install | User's installed stock CLI, resolved on the terminal PATH by crates/jam-host/src/runtime_env.rs |
Docker's stock agent image inside the microVM |
| Delivery | Push to a mailbox or bridge, or pull by the agent | Structured protocol over stdio | The same protocol through sbx exec -i |
| Workspace | Wherever the user runs the agent | The configured directory, or a Jam-allocated directory | Managed (Jam-owned, repository-free by default), private clone, or direct host mount (SandboxWorkspaceSource) |
| Outer isolation | None | None | The microVM: filesystem, process, and network policy |
| Credentials | The agent's own login | The provider's host login, or an explicit API key | Docker service secrets through sbx secret; never copied into the guest |
| Placement | Not applicable | Dedicated | Shared per agent or Dedicated per session (RuntimeHostPlacement) |
Docker is optional. Host-native execution is the default, and jamd must work when sbx is not installed. Capability is re-probed on demand, so installing Docker later requires only a re-check. The sandbox chapter covers the managed-runtime lifecycle.
#Startup
Startup order. The note marks the point from which requests are served.
The socket is served before rebuild finishes. A client can therefore reach a healthy daemon while peers are still being restored, so "the daemon responds" does not mean "every runtime has recovered." The startup task in bins/jam/src/jamd.rs spells out the order, and the consistency and recovery chapter lists what Manager::rebuild replays before it starts any worker.
Independent of the startup task, main also starts a usage capture loop (at start, then every 6 hours; JAM_USAGE_ARCHIVE_INTERVAL_SECS) and a daily update check (after 30 seconds, then every 24 hours; JAM_UPDATE_CHECK_INTERVAL_SECS).
#Shutdown
/v1/shutdown, Ctrl-C, or a server error cancels one shared CancellationToken. From that edge, several things run concurrently:
- The local server stops accepting requests and drains open connections. The event and log streams observe the token and close.
- The manager cleanup task runs
Manager::close, analytics shutdown, and discovery withdrawal together, and joins the startup task. - A force-exit backstop waits 45 seconds (
FORCE_EXIT_GRACE) and then exits the process.
Manager::close cancels the manager's root token first, because an in-flight room bind may hold the bind gate while waiting on that token. It then drains attached working-state publishers, takes the room-bind gate, finishes CLI-only receivers, and stops every worker concurrently, with each peer retrying under its own lifecycle gate until its owned runtime confirms it stopped. Finally it retries pending host cleanups.
The time limits nest, but they are not total bounds on one worker. Worker::stop_bounded applies a 5-second timeout to each wait in turn: the supervisor, then each watcher. Owned-runtime finalization (quiesce, stop the provider, publish the final checkpoint, park Docker) has adapter-specific budgets; the Codex app-server allows 30 seconds. Manager::close retries a failed stop until it succeeds. The 45-second force-exit backstop is the only hard ceiling, so shortening it risks killing the process in the middle of a checkpoint and losing exact resume.
jamd holds <app>/locks/daemon.lock (jam_service::try_acquire_daemon_lock) until cleanup finishes, which is after the socket has disappeared. The desktop waits for that lock before spawning a new daemon, so an absent socket alone is not permission to start one. This prevents two daemons from running against the same store and provider state during the shutdown window.
#Files on disk
jamd resolves its app directory from --config-dir, then JAM_CONFIG_DIR, then ~/.jam (%LOCALAPPDATA%\jam on Windows). JAM_CONFIG_DIR is the only complete isolation between two local environments: each value gets its own daemon, socket, store, keychain entries, and logs. --profile selects an account inside one app directory and shares the daemon.
| Path under the app dir | Contents | Owner |
|---|---|---|
jam.sock |
Local control socket (Unix) | jam-daemon |
jamd.lock, locks/daemon.lock |
Single-instance lock and lifetime lock | jamd, jam-service |
cfg/jam.db |
Encrypted SQLite store (SQLCipher). The key is held by SecretStore, normally in the OS keychain. |
jam-store |
cfg/artifacts/ |
Content-addressed room artifacts, plus Docker Sandbox owner markers | jam-store, jam-host::sandbox |
logs/jamd.log, logs/jamd.N.log |
Size-rotated daemon log, 300 MiB total (LOG_MAX_TOTAL_BYTES) |
jamd |
logs/<profile>/<scope>.log |
One rotating event log per peer, tailed by jam logs |
jam-manager::logfile |
runtime-hosts/<RuntimeHostId>/ |
Managed runtime host root: workspace-root/sessions/<RuntimeSessionId>/, provider-state/, recovery/, operation-journal/, manifest.json |
jam-manager::workspace |
On macOS, the installed CLI and daemon binaries come from /Applications/Band.app, with ~/.local/bin/band as the canonical CLI link and ~/.local/bin/jam as the compatibility alias. The clients chapter covers installation, the updater, and version lockstep between the desktop and jamd.