# `Raxol.AgentClientProtocol.Session`
[🔗](https://github.com/DROOdotFOO/raxol-agent/blob/raxol_agent_client_protocol-v0.1.0/packages/raxol_agent_client_protocol/lib/raxol/agent_client_protocol/session.ex#L53)

One GenServer per `session_id`: the ACP turn state machine.

It owns:

  * the turn state machine (`:idle | {:prompting, Turn} | {:cancelling, Turn}`,
    including the born-cancelled and stale-timer cases),
  * `rx_seq` latch consumption (cancel/prompt commute by wire order, not
    scheduling order),
  * register-before-respond by construction (the `:via` name is minted in
    `start_link/1` before `init/1` runs),
  * the prompt turn as a supervised `Task` with a task-group hold-open gate
    (the drain fires only when the monitor count hits zero),
  * `session/update` emission ordering (updates flow through the Connection on
    the same FIFO lane as the eventual prompt reply, so no update can serialize
    after the response),
  * cancellation (graceful `:acp_cancel` interrupt, then a non-blocking 30s
    `Process.exit/2` backstop; cancel-of-idle is a clean latch-only no-op;
    the disarm-on-response guard is the state machine itself),
  * the permission flow (`async_request` with a per-ask tag; fail-closed deny on
    every non-`{:selected, _}` row; a turn cancel aborts open asks),
  * the prompt reply via the Connection's delegated-reply mechanism (the Session
    is the adopter and sends the one reply),
  * the `mode_state` seam: `set_mode` validates against the injected
    `%SessionModeState{}`; a `nil` mode state makes `set_mode` invalid-params.

## Connection seam (dependency injection)

This module reaches the Connection through an injected module (`:conn_mod`,
default `Raxol.AgentClientProtocol.Connection`) so the session layer compiles
and tests independently of the sibling Connection module, and so the test
`FakeConnection` can double-check that the Connection surface the Session
consumes (`delegate_reply/3`, `reply/3`, `notify/3`, `async_request/6`,
`cancel_request/2`) is sufficient. The semantics are unchanged — the Session
still mints the reply, still owns the drain gate.

## Turn runner seam

`begin_prompt/4` has a fixed 4-arity and does not carry the turn
function, so the turn runner is injected at session start (`:turn_runner`, an
arity-2 fun `(session_pid, %PromptRequest{}) -> {:stop, StopReason} |
%PromptResponse{}`). It runs inside the supervised root `Task`; its return value
is folded into `turn.outcome` (write-once) when the task result arrives. A crash
folds to internal-error. The runner MAY `receive`/peek `:acp_cancel` between
steps to wind down gracefully (the 30s backstop kills it otherwise).

# `t`

```elixir
@type t() :: %Raxol.AgentClientProtocol.Session{
  config: %{
    permission_timeout: timeout(),
    cancel_backstop_ms: pos_integer(),
    idle_timeout: timeout()
  },
  conn: pid(),
  conn_mod: module(),
  emitter: module(),
  idle_epoch: non_neg_integer(),
  idle_timer: reference() | nil,
  journal: {module(), term()} | nil,
  last_cancel_seq: non_neg_integer(),
  mode_state:
    Raxol.AgentClientProtocol.Schema.AgentTypes.SessionModeState.t() | nil,
  session_id: String.t(),
  steer_adapter: module(),
  steer_state: term(),
  task_sup: pid() | atom(),
  turn:
    :idle
    | {:prompting, Raxol.AgentClientProtocol.Session.Turn.t()}
    | {:cancelling, Raxol.AgentClientProtocol.Session.Turn.t()},
  turn_runner: (pid(), struct() -&gt;
                  {:stop, atom()}
                  | Raxol.AgentClientProtocol.Schema.AgentTypes.PromptResponse.t()),
  turn_seq: non_neg_integer()
}
```

# `begin_prompt`

```elixir
@spec begin_prompt(GenServer.server(), struct(), reference(), non_neg_integer()) ::
  :ok | {:error, Raxol.AgentClientProtocol.Error.t()}
```

Begin a prompt turn (called by the `session/prompt` handler task).

Adopts the reply obligation via `delegate_reply/3`, then either starts the turn
(spawns the supervised root task) or, if the prompt was wire-ordered before a
latched cancel (`rx_seq < last_cancel_seq`), resolves it born-cancelled
immediately. Returns `:ok` on the deferred path (the handler must
then return `:deferred`) or `{:error, %Error{}}` when the session is busy — a
second prompt on a `:prompting`/`:cancelling` session; in the busy case
the handler replies with that error directly (no delegation happened).

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `post_update`

```elixir
@spec post_update(GenServer.server(), struct()) :: :ok | {:error, :turn_over}
```

Emit a `session/update` for the live turn (called by a turn task). Forwarded to
the Connection via `notify/3` (same FIFO lane as the eventual reply). A post
for a non-live turn returns `{:error, :turn_over}` and emits nothing (the
straggler-task guard).

# `registry`

```elixir
@spec registry() :: atom()
```

The package-level unique `Registry` name. Keys are `{conn_pid, session_id}`;
`session_id` stays a binary, never atomized.

# `request_permission`

```elixir
@spec request_permission(GenServer.server(), struct()) ::
  {:ok, {:selected, struct()}} | {:ok, :cancelled}
```

Request permission from the client (called by a turn task, blocks `:infinity`).
The Session stays fully responsive while the task is parked, which is what lets a
cancel abort the ask. Returns `{:ok, {:selected, %SelectedPermissionOutcome{}}}`
ONLY on a decoded selected outcome; every other terminal (timeout, client error,
decode failure, disconnect, session cancel) returns `{:ok, :cancelled}` — deny is
the zero value everywhere (fail-closed).

# `set_mode`

```elixir
@spec set_mode(GenServer.server(), String.t()) ::
  {:ok, Raxol.AgentClientProtocol.Schema.AgentTypes.SetSessionModeResponse.t()}
  | {:error, Raxol.AgentClientProtocol.Error.t()}
```

Set the active mode (called by the `session/set_mode` handler task). Validates
`mode_id` against the injected `%SessionModeState{}`; legal mid-turn. A `nil`
mode state or an unknown mode replies `-32602` invalid-params.

# `spawn_task`

```elixir
@spec spawn_task(GenServer.server(), (-&gt; any())) ::
  {:ok, reference()} | {:error, :turn_over}
```

Spawn a subagent task inside the current turn group (called by a turn task). The
task is monitored and its ref joins `monitors` — the hold-open gate: the prompt
reply is withheld until every group member is down. Returns
`{:ok, ref}`, or `{:error, :turn_over}` when there is no live prompting turn.

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

Start a Session. Registration happens via the `:via` name in `start_link/1`,
*before* `init/1` runs — this is the structural half of register-before-respond.
A duplicate live `{conn, session_id}` returns `{:error, {:already_started,
pid}}` (the `session/load`-of-a-live-session case).

Options:

  * `:session_id` (required, binary)
  * `:conn` (required, Connection pid — also the registry key half)
  * `:mode_state` — `%SessionModeState{}` or `nil`
  * `:task_sup` (required) — the per-connection `Task.Supervisor` (`ctx.task_sup`)
  * `:turn_runner` (required for prompting) — arity-2 fun (see moduledoc)
  * `:conn_mod` — Connection module (default `Raxol.AgentClientProtocol.Connection`)
  * `:config` — `%{permission_timeout: _, cancel_backstop_ms: _}`
  * `:name` — override the `:via` name (tests)

# `steer`

```elixir
@spec steer(
  GenServer.server(),
  Raxol.AgentClientProtocol.Session.SteerAdapter.request()
) ::
  Raxol.AgentClientProtocol.Session.SteerAdapter.result()
```

Resolve a steer request against the live turn.

Redirects the running turn with new user input WITHOUT killing it. Runs the
injected `SteerAdapter`'s `expected_turn_id` compare-and-swap decision from the
Session's OWN mailbox — the single-writer serialization
a CAS decision requires, satisfied by construction (only this process
reads-modifies-writes the CAS state). Returns the honest CAS outcome
synchronously:

  * `{:ok, {:accepted, ref}}` — the steer landed: the steering text was
    forwarded to the running turn (the `{:acp_steer, text}` runner seam) and
    one durable steer record was appended via the emitter.
  * `{:ok, {:duplicate, ref}}` — idempotent re-delivery of an already-accepted
    `client_msg_id`; `ref` is the ORIGINAL accept, nothing new happens.
  * `{:error, {:stale_turn, expected, actual}}` — the CAS lost (the turn ended,
    or another steer won); nothing journaled, zero model effect.
  * `{:error, :no_live_turn}` — no turn is running.
  * `{:error, :client_msg_id_reuse}` — the idempotency key arrived carrying
    different content (a client bug/attack, never a retry).
  * `{:error, :no_steer_channel}` — no steer adapter is wired (the default).

`request` is a map `%{text: _, expected_turn_id: _, client_msg_id: _}` (see
`t:Raxol.AgentClientProtocol.Session.SteerAdapter.request/0`).

# `via`

```elixir
@spec via(pid(), String.t()) :: {:via, module(), {atom(), {pid(), String.t()}}}
```

The `:via` tuple that keys a Session on `{conn, session_id}`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
