# `Pixir.Session`
[🔗](https://github.com/Ranvier-Technologies/pixir/blob/main/lib/pixir/session.ex#L1)

The unit of agency (ADR 0001): a single GenServer that owns one conversation —
its `:role` (the Agent configuration), its monotonic `seq` counter, and the append
to its **Log**. There is exactly one Session process per `session_id`, registered in
`Pixir.Sessions.Registry`.

## Recording events

Canonical events go through `record/2`, which runs the load-bearing sequence inside
the GenServer (so it is serialized): **stamp `seq` → append to the Log → publish on
the bus**. The Log is the source of truth, so an event that fails to persist is not
published. Ephemeral events go through `emit/2` (publish only — no `seq`, no Log).

## Turns as supervised Tasks

A **Turn** runs in a Task under `Pixir.TurnSupervisor`, monitored (not linked) by the
Session. `interrupt/1` kills that Task — the load-bearing invariant that makes a Turn
cleanly cancellable without taking the Session down. The Turn body is a 1-arity
function given a context map (`%{session_id, workspace, role, fork_root_session_id}`);
the real tool-loop (build step 7) plugs in here.

## Resume

On `init/1` the Session folds its existing Log to seed `seq` (so new canonical events
continue the sequence). History itself is always re-derived from the Log on demand
(`history/1`), never held as authoritative state (ADR 0003). A Session also acquires
a filesystem-backed writer lease so a second OS process cannot become a competing Log
writer while this process is alive.

# `ctx`

```elixir
@type ctx() :: %{
  session_id: String.t(),
  workspace: String.t(),
  role: role(),
  fork_root_session_id: String.t()
}
```

# `role`

```elixir
@type role() :: atom()
```

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `emit`

```elixir
@spec emit(String.t(), Pixir.Event.t()) :: :ok | {:error, map()}
```

Publish an ephemeral Event (live display only; never persisted).

# `gen_id`

```elixir
@spec gen_id() :: String.t()
```

Generate a sortable, filename-safe Session id.

# `history`

```elixir
@spec history(String.t()) :: {:ok, Pixir.Log.history()} | {:error, map()}
```

Reconstruct History by folding the Log (the source of truth).

# `info`

```elixir
@spec info(String.t()) :: map() | {:error, map()}
```

Snapshot of Session metadata (id, workspace, role, next seq, turn state).

# `interrupt`

```elixir
@spec interrupt(String.t()) :: :ok | {:error, :no_turn} | {:error, map()}
```

Kill the currently running Turn's Task, if any.

# `record`

```elixir
@spec record(String.t(), Pixir.Event.t()) :: {:ok, Pixir.Event.t()} | {:error, map()}
```

Record a canonical Event: stamp `seq`, append to the Log, then publish. Returns the
stamped Event, or a structured error if it was not canonical / failed to persist.

# `register_pressure_warning`

```elixir
@spec register_pressure_warning(String.t(), integer() | nil, String.t()) ::
  {:ok, :warn | :already_warned}
```

Hysteresis gate for context-pressure warnings (ADR 0020): returns `{:ok, :warn}`
the first time a `(latest checkpoint to_seq, tier)` pair is seen and
`{:ok, :already_warned}` afterwards — no warning spam on consecutive turns for
the same pair. A new compaction checkpoint (different `to_seq`) or a different
tier re-arms the gate. State is ephemeral process state by design: it is never
logged, and re-warning after a Session restart is acceptable.

# `start_link`

# `start_turn`

```elixir
@spec start_turn(String.t(), (ctx() -&gt; any())) ::
  {:ok, reference()} | {:error, :busy} | {:error, map()}
```

Start a Turn: run `turn_fun.(ctx)` in a supervised Task. Returns `{:ok, ref}` or
`{:error, :busy}` if a Turn is already running. If the previous Turn left orphan
tool calls in the Log, Pixir first records fallback `tool_result` events so Provider
replay stays valid.

# `turn_running?`

```elixir
@spec turn_running?(String.t()) :: boolean() | {:error, map()}
```

Whether a Turn is currently running.

# `via`

`{:via, Registry, …}` name for a Session id.

---

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