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

#Domain and authority

On this pageThe objectsHow they relateAttached and owned agentsDesired and effective configurationAuthorityScope keysLegacy shapes still in the model

A Band agent, the local record that represents it, the room it works in, the configuration it runs with, the process that executes it, and the conversation thread inside that process are six different objects with six different lifetimes. Code that treats two of them as one routes messages, scopes cleanup, or attributes usage to the wrong object. AGENTS.md records several such defects on the owned-runtime surface.

#The objects

Core Jam identifiers are newtypes in crates/jam-domain/src/ids.rs (generated by the string_id! macro), so the compiler refuses to pass one where another is expected. The exceptions are the provider's own session IDs and pull receive leases, which are plain String fields on HostSession even though they are different namespaces.

Object Type What it identifies Minted by Lifetime
Band actor IdentityId A Band user or agent UUID. The key for every identity match: sender, participant, mention, per-agent cost. Band Permanent on the platform
Local peer PeerKey (profile/scope) One local record for one Band agent under one account profile. The worker key. jamd at onboard or ensure Until the peer is removed or archived
Room ChatId A Band chat room. Band Permanent on the platform
Host session alias SessionId An operator-facing name for one room binding (--host-session). Used for routing inside one peer, never as identity. Operator or jamd Until detached
Runtime template RuntimeTemplateId Durable desired configuration for one agent identity. jamd when an owned agent is created Until the agent is deleted
Runtime session RuntimeSessionId One local agent runtime context. Stable across renames and independent of room membership. jamd when a room-bound session is materialized Until archived and cleaned up
Runtime host RuntimeHostId One concrete execution boundary: a provider process and, for Docker, one microVM. jamd host allocator Until whole-host cleanup
Room binding RuntimeBindingId One runtime session bound to one room. jamd Until unbound
Managed workspace WorkspaceId Jam-owned workspace directory ownership. A path is a reference, never the identity. jamd workspace manager Until archived and removed
Provider thread String in HostSession::provider_session_id / agent_session_id The provider's own session or thread ID (Codex thread, Claude session UUID). The provider Provider-defined

Two names deserve a warning. SessionId sounds like runtime identity and is not: it is a display alias for a room binding. RuntimeSessionId is the runtime identity. And HostSession, the persisted record type, is neither of them: it is one room-to-runtime binding row that carries both IDs.

#How they relate

The objects that exist for one agent, with cardinalities:

The cardinalities that matter:

  • One Band agent has one local peer per account profile. Peer is keyed by (profile, scope) in the peers table and carries the Band agent_id and its API key (crates/jam-domain/src/peer.rs).
  • A peer has many host sessions, one per room. The module comment on crates/jam-domain/src/host_session.rs states the rule: exactly one room per host session, and inbound traffic is routed by chat_id to the session that owns that room. A room with no owning session is unrouted and its messages are queued rather than guessed.
  • A runtime template is a host session with no room. HostSession::is_parked is self.room.as_str().is_empty(). The worker builds no routing surface for a parked session, so a template never receives messages. A peer can hold more than one parked template. When the agent joins a room, the manager materializes a new room-bound HostSession from the template, copying the template's RuntimeTemplateId and minting a fresh RuntimeSessionId.
  • A runtime host holds zero or more runtime sessions. runtime_host_sessions maps each session to exactly one host. RuntimeHostAllocator persists the host before it attaches the first membership, so a crash between the two writes leaves a host with no members, which recovery repairs. Placement decides how many sessions share a host: RuntimeHostPlacement::SharedAgentHost keeps one live Shared host per agent identity and rejects a room whose configuration is incompatible with it, and DedicatedSessionHost allocates one host per session (crates/jam-domain/src/runtime_workspace.rs, crates/jam-manager/src/runtime_host.rs). A shared host still gives each room its own runtime session, working directory, and provider thread.
  • A process is not a session. One provider process can serve several runtime sessions on a shared host, and an attached coding-agent window (one PID) can back host sessions in several rooms. Code that assumes one process per session is wrong on both paths.

#Attached and owned agents

Every host session has a runtime mode, HostRuntime in host_session.rs:

  • AttachInbox (attached). The user runs the coding agent, for example Claude Code in a terminal. Jam delivers messages through a cooperating surface: the Claude Code teammate mailbox file, a pull lease that the agent reads with jam commands, or the Copilot CLI extension bridge. Jam does not own the process. Its liveness is observed through host_pid and the host monitor loop.
  • Owned. jamd starts and stops the provider process and talks to it over a structured protocol. HostTransport names the protocol: CodexAppServer, CopilotSdk, ClaudeCodeCli, Acp, Opencode, or the legacy Pty marker, which build_host in bins/jam/src/jamd.rs now rejects.

IdentityClass on the peer (Terminal or Owned) records the same split at identity level. It is stamped at onboard time because host session rows alone cannot tell a terminal identity with no window from an ordinary agent (crates/jam-domain/src/peer.rs, IdentityClass).

#Desired and effective configuration

An owned runtime's configuration has two generations on the HostSession record:

  • desired_config_generation is durable user intent. It advances on every accepted save, even if no runtime is running.
  • effective_config_generation is what a live runtime has confirmed. It moves only when the provider accepts the change.
  • pending_change records the change in flight and survives restarts, so a change that failed between save and apply is still visible afterward.

HostSession::has_outstanding_config_change is the single comparison. Status surfaces must read it rather than compare the two numbers themselves. crates/jam-manager/src/runtime_txn.rs owns the transaction that moves a change from desired to effective.

#Authority

"Manager" means jam_manager::Manager inside jamd.

Aggregate Authority Mutation coordinator Durable representation Projections Deletion authority
Account and credentials Band and FusionAuth issue; Jam stores Manager account lifecycle lock (lock_account_lifecycle) accounts table; tokens and keys through SecretStore (crates/jam-store/src/secret_store.rs), which prefers the OS keychain and falls back to permission-restricted files Desktop account list Sign-out joins the refresh loop, then deletes the token
Band agent identity Band Band API Band; peers.agent_id locally Participant lists Band agent delete through the Human API
Room and membership Band Band API, reached through Control methods Band; room_directory_cache, account_directories Desktop room list, EventKind::ChatAdded and related events Band; the agent owner may delete a room its agent owns
Room messages Band Band API Band; room_message_cache_* tables as a local cache Desktop conversation, fan-out events Band
Peer record Manager Per-peer worker lifecycle gate peers table PeerStatus, EventKind::PeerAdded Explicit stop or delete
Host session and template Manager Worker lifecycle gate, runtime room-bind gate host_sessions table (38 columns, primary key (profile, scope, id)) Sessions view, agent detail Explicit detach, unbind, or delete
Runtime host and membership Manager (crates/jam-manager/src/runtime_host.rs) Runtime host allocator runtime_hosts, runtime_host_sessions Runtime sessions table in the desktop Whole-host cleanup operation only
Managed workspace Manager (crates/jam-manager/src/workspace.rs) Workspace operation journal managed_workspaces, workspace_operations, workspace_operation_events; bytes under <app>/runtime-hosts/<host>/workspace-root/ Recovery and repository panels Archive, then export deletion, each separately confirmed
Provider thread state The provider Provider adapter in jam-host Provider files; checkpoints in provider_checkpoints plus payload under the runtime host root Continuity panel Continuity reset or payload deletion
Private task lane Manager (crates/jam-manager/src/task_lane.rs) TaskLaneCoordinator, one gate per RuntimeSessionId work_items, work_snapshots, task_* tables Work view Lane archive and retention sweep
Shared room board Local store or Band, chosen per room (crates/jam-manager/src/bandboard.rs) RoutedRoomBoardBackend room_tasks, room_task_operations, or Band Work board Board task operations
Usage Provider logs and live events; Jam archives Usage reconciliation outbox usage_* tables (12) Usage and cost views Never; the archive is grow-only
Desktop view state The daemon (server-derived) or the desktop (UI-only) useDaemonStore is the only writer of server-derived state Memory only React components Reset on reload

The table reduces to three rules.

  1. Band owns everything social. Rooms, membership, messages, and agent identities live on Band. Jam caches them and receives change events, but a cache disagreeing with Band is resolved by re-reading Band.
  2. jamd owns everything about local execution. The desktop and the CLI never write runtime, workspace, or task state directly. They call Control methods, and jamd performs the mutation. AGENTS.md states this as "the desktop and CLI expose that state without becoming a second runtime owner."
  3. Destructive actions have their own authority. Archive, force discard, export deletion, continuity reset, and whole-host cleanup each require a fresh inventory at the moment of mutation and their own confirmation. A successful inspection never grants reusable deletion authority. The sandbox and workspace chapter covers each one.

#Scope keys

Maps, locks, log lines, and caches each need a key. The correct key for common operations:

Operation Correct key Wrong key that has been used or is tempting
Route an inbound room message (PeerKey, ChatId) to find the owning host session A guessed runtime, or the first live session
Identify a runtime for lifecycle, logs, or cleanup RuntimeSessionId SessionId alias, room ID, provider thread ID
Decide host-wide scope (stop, restart, reset, cleanup) RuntimeHostId, then its durable membership set The clicked room row, or the live worker registry
Match a sender, participant, or mention IdentityId Handle, display name
Attribute usage Provider session ID joined through the session ledger Room ID
Name a sandbox and verify ownership Band agent UUID plus RuntimeSessionId, with a host-side plan and guest nonce Display name
Apply a runtime template to rooms RuntimeTemplateId (UUID) for compatibility; runtime_template_id (string) for the operator-facing template Room ID or binding alias

#Legacy shapes still in the model

These exist on main. A refactor must keep each one or migrate it explicitly:

  • Attach-inbox as the default. HostRuntime::default() and HostTransport::default() are both AttachInbox, so a record with no runtime columns decodes as a legacy attached session.
  • Optional placement and workspace source. HostRuntimeConfig::host_placement is None on records written before placement existed, which resolves to Dedicated. workspace_source is None on legacy records and maps from spawn.cwd plus sandbox.no_clone.
  • HostTransport::Unknown. A newer daemon can persist a transport string an older build does not know. The older build decodes it as Unknown, and jam-store writes the original string back on save so the value is not lost. The pairing is guarded by the test undecodable_enum_survives_a_load_and_save_round_trip.
  • agent_session_id versus provider_session_id. agent_session_id carries the provider's ID for attached sessions but a pull receive lease for pull sessions. provider_session_id was added so attribution always has the provider's ID. Both fields remain.
  • Two template identifiers. HostSession::runtime_template_id is a string naming the parked template for operators. HostRuntimeConfig::template_id is the immutable RuntimeTemplateId UUID used for host allocation and compatibility. A materialized room session must carry both.
Scroll to zoom, drag to pan.