Skip to content
Developer Preview

Sessions and turns

A session is one durable conversation with the agent, identified by a UUIDv7 sessionId that is simultaneously the wire identity, the runtime stream id, and the on-disk directory name. A turn is one foreground run of the agent inside a session. This page covers how a client gets a host, opens a connection, and drives turns.

The process model: one host, one lease, one connection

Section titled “The process model: one host, one lease, one connection”

A host is one muse serve process holding zero or more loaded sessions; every loaded session is exclusively owned by exactly one host at a time, enforced by an on-disk writer lease that the OS releases if the host dies. A v1 host accepts exactly one client connection, and a second connection attempt is refused.

In v1 the only out-of-process transport is stdio: the client spawns the host, and the connection lifetime is the process lifetime. Closing stdin triggers an orderly drain — any active run is cancelled with a durable cancelled terminal, a durable session-end record is written, leases are released, and the process exits; killing the process instead writes nothing, by design, and the next load of the log backstops the gap. Because the spawner is the client, the spawn boundary is also the security boundary: a host must not accept a connection from a party that did not spawn it.

Two separately spawned hosts can still contend for the same session on disk; the loser gets the typed sessionInUse error rather than a generic failure.

Every connection must complete the initialize handshake before any other method is accepted; a request before that fails with a not-initialized error. The handshake fixes per-connection state: client identity, capabilities, experimental gating, and notification opt-outs.

After the handshake, the connection’s subscription set decides which sessions stream view events to it. Starting (session/start), resuming (session/resume), or forking (session/fork) a session auto-subscribes the calling connection; nothing else subscribes you. The server announces newly loaded sessions to all connections, and announces unloads with a final cursor you can later resume from.

A loaded session unloads when it has had zero subscribers, no active or pending run, and no pending approval or user-input prompt for the idle window. A session parked on an unanswered approval is never idle-unloaded.

Every lifecycle method returns or lists one shape: the Session object. Its concept-level fields:

  • sessionId and path — identity and the absolute path of the durable log; under the ephemeral profile the path is the empty string, the one value a client must never hand to a filesystem call.
  • status — not loaded, idle, or running; a host only knows its own load state, so foreign ownership is detected by the sessionInUse error, never by status.
  • activeTurnId — the current foreground turn when running, so a rejoining client can steer or interrupt precisely without folding history first.
  • approvalMode — the folded effective approval mode, the read path for the mode so a client never needs a mode-change command to learn it.
  • Unknown fields must be ignored: the object evolves additively.

The command plane is deliberately thin: a command’s response tells you only what the runtime decided at admission, and everything that actually happened arrives on the session view as cursor-bearing events. Submitting user input is turn/start; when the session is busy, an ifBusy disposition chooses queueing (the default), steering the running turn, or replacing it. The ack’s disposition field tells you which happened without folding history.

A turn actually begins when turn/started folds — fresh submits immediately, queued submits at their launch boundary, and steered submits never, because a steer joins the running turn instead of minting a new one.

A turn is over when its turn/completed view event folds, carrying one of three terminals:

  • completed — the run finished normally.
  • failed — the run ended in a run-fatal error; the event carries a turnError object with a kind, a human message, and the server’s judgment on whether resubmitting the same input may succeed.
  • cancelled — the terminal for both stop lanes (turn/interrupt and turn/cancel) and for host-shutdown cancellation.

Acceptance of a stop command means the stop was admitted, not that the turn is already stopped: the turn is over when you fold its cancelled terminal. Mid-turn failures reach the client as this terminal event, never as a JSON-RPC error.

Two edge exits matter for anyone awaiting a turn. A queued turn can be reclaimed before it ever runs, in which case turn/unqueued is its terminal event and no turn/completed will ever arrive — a turn-wait that folds only turn/completed hangs forever. And a queued or replace follow-up whose launch fails at the terminal boundary terminates with a failed turn/completed and no preceding turn/started.

The stop gesture is scoped to the active turn only: interrupting does not consume an already-queued successor, which may still launch once the interrupted turn folds its terminal; un-sending a queued turn is turn/unqueue.

turn/interrupt is the user’s stop gesture: it takes the runtime’s priority lane and can pair a retract intent that durably restores the prompt to the composer when no assistant output committed. turn/cancel is plain programmatic cancellation with no priority intent and no retract pairing. Both fold the same cancelled terminal. Background tasks started by the turn are not killed by either lane; they have their own stop verbs.

How the streamed item events between turn/started and turn/completed fold into client state is covered by the fold model.