# `Pixir.ACP.Server`
[🔗](https://github.com/Ranvier-Technologies/pixir/blob/main/lib/pixir/acp/server.ex#L1)

The ACP agent (server side) over stdio (ADR 0009): a single `GenServer` that owns the
stdout writer and the `acp_session_id ↔ pixir_session_id` map, decodes ndjson JSON-RPC
from stdin, dispatches by method onto `Pixir.Conversation`, and runs each
`session/prompt` in a supervised Task.

## Channel discipline (ADR 0005)

**stdout carries only JSON-RPC.** Every write goes through this one process, so the
ndjson stream never interleaves. Prompt Tasks never touch stdout directly — they call
`emit/2`. Diagnostics go to stderr. The caller (`run/0`) redirects `Logger` to stderr
before starting so no log line corrupts the stream.

## stdin

A dedicated reader process blocks on `IO.read(io, :line)` and forwards `{:line, l}` /
`:eof` / `{:io_error, r}` to this server, so the server mailbox is never blocked on raw
stdin. `run/0` explicitly configures stdio as Unicode because GUI launchers can start
Pixir without a UTF-8 locale; ACP wire text must remain UTF-8 regardless of the parent
process environment. On EOF the server stops normally and `run/0` unblocks (exit 0).

## Scope

Implements `initialize`, `session/new`, `session/prompt`, `session/cancel`,
`authenticate` + `logout` (ACP handshake no-ops; Pixir advertises terminal
auth through `pixir login`, and owns Credential storage outside the stdio channel),
`session/set_mode` + `session/set_config_option` (modes, models, and reasoning effort,
D.2), `session/set_model` (legacy Pixir/T3 compatibility), and `session/load` + `session/resume`
(lifecycle, A.6); emits `session/update` (incl. `current_mode_update`, `plan`, and the
additive runtime-driven `config_option_update`, #520) and ORIGINATES `session/request_permission` (interactive permissions,
A.2 — correlating the client's response against `pending_requests`). Per-turn
knobs (model, reasoning effort, hosted Web Search, `permission_mode`) ride on
`session/prompt` `_meta`; sticky model, reasoning-effort, and Web Search
selections are exposed through `configOptions`; the legacy model catalog +
auth status ride on `initialize._meta.pixir`.
Other methods get `-32601`. JSON-RPC errors are reserved for protocol faults; a
failed Turn is reported as content with `stopReason:"end_turn"` (ADR 0009 §5), and
the prompt result additionally carries `_meta.pixir.turn_failure` exactly when a
`turn_failed` event was observed during the prompt. Availability is owned jointly by
ACP's prompt table and the underlying Session: a terminal update normally remains behind
a bounded cleanup wait before its PromptResponse, and any residual `:busy` race is an
explicit `-32602` refusal rather than an empty successful Turn. A stalled Session probe
or a successor Turn can never hold the ACP Server or the completed request id indefinitely.
Failure facts are allowlisted: `terminal_status` is one of the current producer
statuses (`configuration_error`, `provider_error`, `tool_error`, or `interrupted`),
while `error_kind` is a lower-case ASCII identifier of at most 64 UTF-8 bytes. A
cleanup `interrupted` classification does not replace earlier non-empty facts from the
same prompt. Malformed facts are omitted, though an observed `turn_failed` still
projects an empty facts map so evidence presence is preserved. A refused prompt or
silent stall claims no failure evidence.
Permission posture follows the session mode (`plan` → read-only) and
`_meta.permission_mode "ask"` (→ interactive approval via the ACP asker).

## TODO(presenter-session-id)

ACP clients already receive the Pixir Session id from `session/new`, but tool/model
projections can still make the parent id invisible to the assistant text layer. The
next Presenter slice should expose the parent `pixir_session_id` consistently in
Pixir-specific `_meta`, tool result raw output, or session/status updates so T3/Zed
prompts can report it without guessing from child ids. Keep this presentation-only:
the Log remains authoritative and stdout must remain JSON-RPC only.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `emit`

```elixir
@spec emit(GenServer.server(), map()) :: :ok
```

Emit a `session/update` notification (called by prompt Tasks; serializes writes).

# `emit_event`

```elixir
@spec emit_event(GenServer.server(), binary(), Pixir.Event.t()) :: :ok
```

Translate and emit a Pixir Event with server-owned presentation state.

# `feed`

```elixir
@spec feed(GenServer.server(), binary()) :: :ok
```

Feed one already-decoded JSON-RPC line into the Server. Test seam that drives the same
`handle_info({:line, _})` path the reader uses, without real stdio.

# `request_permission`

```elixir
@spec request_permission(GenServer.server(), map()) :: {:ok, map()} | {:error, term()}
```

Originate a `session/request_permission` request to the client and BLOCK until
the client responds (A.2). Returns the raw `RequestPermissionResponse` result
(a map) for `Translate.permission_outcome/1` to interpret, or `{:error, reason}`.

Called from inside the Executor's Task (the Turn's tool loop), so blocking here
blocks only that one Task — never the Server GenServer (which keeps writing and
reading lines, including the eventual response). The Server owns the timeout and
removes the pending request if a silent client never replies.

# `run`

```elixir
@spec run() :: :ok
```

Blocking entrypoint for `pixir acp`. Redirects `Logger` to stderr, starts a linked
Server reading `:stdio`, and blocks until the Server stops on EOF. Returns `:ok` so the
CLI router exits 0.

# `runtime_config_change`

```elixir
@spec runtime_config_change(GenServer.server(), binary(), map()) ::
  {:ok, :queued} | {:error, %{kind: :invalid_args, details: map()}}
```

Apply a runtime-owned config change for one ACP session and push it to the
client without any client round-trip (#520). After validating and storing the
new sticky value(s), the Server emits ONE `session/update` carrying
`sessionUpdate: "config_option_update"` with the COMPLETE `configOptions`
list reflecting the new values (ACP: an update replaces the advertised set).
A runtime mode change additionally emits the existing additive
`current_mode_update`, mirroring the client-driven `session/set_mode` path.

`changes` maps any of `"mode"`, `"model"`, or `"reasoning_effort"` to its new
id. Invalid entries are dropped with a stderr warning; an incompatible proposed
model/effort combination is rejected atomically with session state unchanged.
Unknown sessions are ignored (diagnostics to stderr; stdout stays JSON-RPC only). Returns
`{:ok, :queued}` when the change was handed to the Server, or a structured
`{:error, %{kind: :invalid_args}}` for malformed arguments. Presenter
plumbing only — the Log remains authoritative. The live plan→build producer
is `Pixir.ACP.RuntimeMode.leave_plan/1`, called from `update_plan`.

# `start_link`

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

Start the Server. Opts:

  * `:io` — the stdio device (default `:stdio`; inject a `StringIO`/pipe in tests).
  * `:provider`, `:provider_opts` — passed through to each Turn (test seam).
  * `:prompt_resolve_hook` — test callback after the bounded Session Turn cleanup wait
    and immediately before prompt resolution.
  * `:prompt_before_cleanup_hook` — test callback after terminal status and before the
    bounded Session cleanup wait.
  * `:compaction_complete` — test seam for the runtime-owned structured compaction
    completion; production uses `Pixir.Compaction.complete/2`.
  * `:name` — optional registered name.

---

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