Jam architecture main at 59f67f5d0 · 2026-09-29
View source

#Runtime topology

On this pageContainersChannelsOne command end to endInside jamdDeployment modes for an agentStartupShutdownFiles on disk

Jam 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::Manager implements Control for real (impl Control for Manager in crates/jam-manager/src/manager.rs).
  • jam_daemon::router exposes any Arc<dyn Control> over HTTP.
  • jam_client::Client implements Control again by sending HTTP requests (impl Control for Client in crates/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:

  1. Legacy owned PTY: rejected with an error.
  2. Owned Claude Code CLI: ClaudeCodeOwned.
  3. Owned ACP or OpenCode: AcpHost.
  4. Owned Codex app-server: CodexAppServerRegistry::build. A daemon-scoped registry makes every Managed Shared Codex session on one runtime host share one provider process.
  5. Owned Copilot SDK: CopilotSdkRegistry::build.
  6. Attached Copilot (session.provider == COPILOT_PROVIDER): CopilotCli, which queues bridge jobs for the Copilot CLI extension.
  7. Attached Claude Code (peer.host == "claudecode"): ClaudeCode, which writes the per-session teammate mailbox.
  8. Anything else: Generic, a pull-only adapter with a no-op deliver.

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.

Scroll to zoom, drag to pan.