# In-process embedding

> Using `in_process` to run `MessageProcessor` without a socket, internal initialize handshake, `InProcessClientHandle` request/event channels, backpressure, and relationship to `codex-app-server-client`.

- Repository: openai/codex
- GitHub: https://github.com/openai/codex
- Human docs: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1
- Complete Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/llms-full.txt

## Source Files

- `src/in_process.rs`
- `src/lib.rs`
- `src/message_processor.rs`
- `src/error_code.rs`
- `src/transport.rs`
- `README.md`

---

---
title: "In-process embedding"
description: "Using `in_process` to run `MessageProcessor` without a socket, internal initialize handshake, `InProcessClientHandle` request/event channels, backpressure, and relationship to `codex-app-server-client`."
---

The `codex-app-server` crate exposes `in_process` (`src/in_process.rs`) to run the same `MessageProcessor` and outbound routing stack as socket transports, but over bounded Tokio `mpsc` channels inside the caller’s process. CLI surfaces (`codex-tui`, `codex-exec`) typically use the sibling `codex-app-server-client` facade; integration tests and advanced embedders can call `in_process::start` directly.

## When to use in-process vs a socket transport

| Approach | Process boundary | Wire format | Typical consumer |
| --- | --- | --- | --- |
| `in_process::start` | None | Typed `ClientRequest` / `ClientNotification` in; JSON-RPC result envelope out | Tests, custom embedders |
| `codex-app-server-client` | None | Same semantics + worker task + `AppServerEvent` | TUI, exec |
| `--listen stdio://` / unix / websocket | Separate process or stream | JSON-RPC JSONL or websocket frames | VS Code, proxies, remote clients |

<Note>
In-process is transport-local, not protocol-free: successful RPC results still arrive as JSON-RPC `result` values because `MessageProcessor` produces that shape internally. Callers deserialize with `serde_json` (or `request_typed` in `codex-app-server-client`).
</Note>

## Architecture

```mermaid
flowchart TB
  subgraph embedder["Embedder (TUI / exec / test)"]
    Facade["codex-app-server-client\n(optional)"]
    Handle["InProcessClientHandle"]
  end

  subgraph runtime["in_process Tokio runtime task"]
    ClientRx["client_rx\n(InProcessClientMessage)"]
    ProcTx["processor_tx\n(ProcessorCommand)"]
    MP["MessageProcessor"]
    OutRouter["route_outgoing_envelope"]
    WriterRx["writer_rx\n(QueuedOutgoingMessage)"]
    EventTx["event_tx\n(InProcessServerEvent)"]
  end

  Facade --> Handle
  Handle -->|request / notify / server reply| ClientRx
  ClientRx --> ProcTx --> MP
  MP --> OutRouter --> WriterRx
  WriterRx -->|responses| ClientRx
  WriterRx -->|ServerRequest / notifications| EventTx
  EventTx --> Handle
```

Single logical connection: `IN_PROCESS_CONNECTION_ID` (`ConnectionId(0)`). There is no stdio/socket accept loop; `remote_control_handle` is `None` and analytics records `AppServerRpcTransport::InProcess`.

## Startup: `InProcessStartArgs` and `start`

<Steps>
<Step title="Build runtime inputs">

Populate `InProcessStartArgs` with the same ambient state `run_main_with_transport_options` assembles before `MessageProcessor::new`: `Arg0DispatchPaths`, `Arc<Config>`, CLI TOML overrides, `LoaderOverrides`, `strict_config`, cloud requirements, `ThreadConfigLoader`, `CodexFeedback`, optional `LogDbLayer` / `StateDbHandle`, `EnvironmentManager`, config warnings, `SessionSource`, `enable_codex_api_key_env`, `InitializeParams`, and `channel_capacity`.

</Step>
<Step title="Call `in_process::start`">

`start(args)` calls `start_uninitialized`, spawns the runtime task, then performs the handshake before returning:

1. `ClientRequest::Initialize` with `args.initialize` (default request id `0`).
2. On success, `ClientNotification::Initialized`.
3. Returns `InProcessClientHandle` ready for RPCs.

If initialize returns a JSON-RPC error, the runtime shuts down and `start` returns `InvalidData`.

</Step>
<Step title="Use the handle">

- `request(ClientRequest)` → `IoResult<Result<serde_json::Value, JSONRPCErrorError>>`
- `notify(ClientNotification)` → `IoResult<()>`
- `next_event(&mut self)` → `Option<InProcessServerEvent>`
- `respond_to_server_request` / `fail_server_request` for pending `ServerRequest` events
- `shutdown(self)` → bounded graceful teardown (5s timeouts, abort fallback)

</Step>
</Steps>

### Initialize handshake vs socket clients

On socket transports, `initialize` may return before outbound notifications are enabled; the client must send `initialized`, and `lib.rs` then calls `send_initialize_notifications_to_connection` and sets `outbound_initialized`.

In-process passes `Some(outbound_initialized)` into `handle_client_request`, so a successful `initialize` RPC immediately marks the connection outbound-ready. `start()` still emits `ClientNotification::Initialized` for parity; `process_client_notification` currently logs typed notifications only—the session is already live after the initialize response.

Config warnings are delivered via `send_initialize_notifications()` when the processor observes the initialized transition (`!was_initialized && is_initialized` in the runtime loop).

### Channel capacity

```text
DEFAULT_IN_PROCESS_CHANNEL_CAPACITY = CHANNEL_CAPACITY  // 128 (app-server-transport)
```

`channel_capacity` in `InProcessStartArgs` is clamped with `.max(1)`. The same capacity sizes: client command queue, event queue, processor command queue, outgoing envelope queue, and per-connection writer queue.

## `InProcessClientHandle` API

| Method | Direction | Behavior |
| --- | --- | --- |
| `request` | Client → server | Async; unique `RequestId` required among concurrent calls |
| `notify` | Client → server | Fire-and-forget; `try_send` on client queue |
| `next_event` | Server → client | Async recv of `InProcessServerEvent` |
| `respond_to_server_request` | Client → server | Completes a prior `ServerRequest` event |
| `fail_server_request` | Client → server | Rejects with JSON-RPC error payload |
| `shutdown` | Control | Drains runtime; cancels in-flight server requests with internal error |
| `sender` | — | Cloneable `InProcessClientSender` for concurrent callers |

### `InProcessServerEvent` variants

<ParamField body="ServerRequest" type="ServerRequest">
Server-initiated JSON-RPC request (approvals, MCP elicitation, etc.). Must be answered with `respond_to_server_request` or `fail_server_request`; unanswered requests can stall turns.
</ParamField>

<ParamField body="ServerNotification" type="ServerNotification">
App-server notification (`turn/completed`, deltas, thread events, etc.).
</ParamField>

<ParamField body="Lagged" type="{ skipped: number }">
Transport health marker: the consumer fell behind and best-effort events were dropped. Not an application-level Codex event. Emitted by `codex-app-server-client` when forwarding saturates; the low-level runtime logs drops but does not emit `Lagged` itself.
</ParamField>

Duplicate in-flight request IDs produce `INVALID_REQUEST` (`-32600`) on the duplicate caller’s oneshot, without enqueueing a second processor command.

## Backpressure and overload

Bounded queues use `try_send` at hot paths so memory cannot grow without bound.

### Client → runtime (`InProcessClientSender`)

| Queue full | Effect |
| --- | --- |
| `request` / `notify` / server-reply messages | `IoError` with `ErrorKind::WouldBlock` and message `"in-process app-server client queue is full"` |
| Closed runtime | `BrokenPipe` |

Embedders should retry or slow producers on `WouldBlock`, especially with `channel_capacity: 0` clamped to `1` (see unit test `in_process_start_clamps_zero_channel_capacity`).

### Runtime → `MessageProcessor`

| Queue full | Effect |
| --- | --- |
| Incoming `ClientRequest` | JSON-RPC error code **`-32001`** (`OVERLOADED_ERROR_CODE`), message `"in-process app-server request queue is full"` |
| Incoming `ClientNotification` | Logged warning; notification **dropped** |

### Runtime → event consumer (`InProcessServerEvent`)

| Outbound type | Queue full behavior |
| --- | --- |
| `ServerRequest` | Fail back into `MessageProcessor` with **`-32001`** (`"in-process server request queue is full"`) so approvals do not hang |
| `TurnCompleted`, `ThreadSettingsUpdated` | Blocking `.send().await` (guaranteed delivery tier in `in_process`) |
| Other notifications | `try_send`; on full, warning + **drop** |

<Warning>
Server requests are never silently abandoned at the in-process layer. Saturated event queues surface overload errors back to the processor instead of leaving approval flows pending forever.
</Warning>

Socket transports use the same **`-32001`** / `"Server overloaded; retry later."` pattern at ingress (`app-server-transport`); in-process overload strings are more specific but share the code.

### JSON-RPC error codes (in-process path)

| Code | Constant / name | Typical cause |
| --- | --- | --- |
| `-32600` | invalid request | Duplicate request id, not initialized (via `MessageProcessor`) |
| `-32603` | internal error | Processor closed, shutdown in progress |
| `-32001` | overloaded | Processor or server-request event queue full |

Public re-exports from `codex-app-server` include `INVALID_PARAMS_ERROR_CODE` and `INPUT_TOO_LARGE_ERROR_CODE` for shared client handling; overload for embedding is primarily `-32001` on the request path.

## Relationship to `codex-app-server-client`

`codex-app-server-client` (workspace crate `codex-app-server-client`, consumed by `codex-tui` and `codex-exec`) wraps `codex_app_server::in_process` and is the recommended integration surface for product code.

```text
Caller (TUI / exec)
    │
    ▼
InProcessAppServerClient  ── mpsc ClientCommand / events
    │ worker task (select: commands + handle.next_event)
    ▼
InProcessClientHandle     ── in_process runtime (this page)
    │
    ▼
MessageProcessor
```

### What the facade adds

- **`InProcessClientStartArgs`**: builds `InitializeParams` from `client_name`, `client_version`, `experimental_api`, `opt_out_notification_methods`, plus maps into `InProcessStartArgs` (including thread config loader discovery).
- **Worker task**: concurrent request handling (spawned per request so events keep draining during blocking server-request waits).
- **`forward_in_process_event`**: second bounded queue with **lossless** vs **best-effort** notification tiers—stricter than `in_process` alone (`TurnCompleted`, `ThreadSettingsUpdated`, transcript deltas, `ItemCompleted`, etc. block; overload rejects pending `ServerRequest` with `-32001`).
- **`InProcessServerEvent::Lagged`**: aggregates skipped best-effort events for slow consumers.
- **Typed helpers**: `request_typed`, `AppServerEvent`, shared shutdown timeout (5s).
- **Policy**: auto-rejects unsupported `chatgptAuthTokensRefresh` server requests for in-process clients.

Re-exported symbols: `DEFAULT_IN_PROCESS_CHANNEL_CAPACITY`, `InProcessServerEvent`, `StateDbHandle`, `LogDbLayer`.

<Info>
Pass both `SessionSource` and initialize `client_info.name` explicitly at startup so thread metadata (`thread/list`, `thread/read` source kinds) matches the originating runtime without hard-coding TUI/exec policy in the shared client crate.
</Info>

## Testing and verification

Integration tests under `tests/suite/` construct `InProcessStartArgs` and call `in_process::start` directly (for example `thread_read.rs`, `thread_unarchive.rs`, `remote_thread_store.rs`). Typical pattern:

- Ephemeral temp `codex_home`, `ConfigBuilder`, optional `state_db`
- `InitializeParams` with a test `client_info.name`
- `session_source` aligned with the scenario (`SessionSource::Cli`, `Exec`, etc.)
- `channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY`
- `shutdown().await` after assertions

Unit tests in `src/in_process.rs` cover initialize + typed v2 RPC, session source propagation on `thread/start`, and zero capacity clamping.

Run focused crate tests:

```bash
cd codex-rs && just test -p codex-app-server
```

## Choosing an integration layer

| Goal | Use |
| --- | --- |
| Ship a CLI or UI in the Codex repo | `codex-app-server-client::InProcessAppServerClient::start` |
| Custom embedder, minimal dependencies | `codex_app_server::in_process::start` + `InProcessClientHandle` |
| External editor or daemon | Separate `codex-app-server` process over stdio/unix/websocket |
| Protocol compliance tests without subprocess | `in_process::start` in `tests/suite` |

## Related pages

<CardGroup>
<Card title="Connection lifecycle" href="/connection-lifecycle">
Per-connection `initialize` / `initialized`, notification opt-out, and server-initiated requests on socket transports—contrasted with in-process auto-handshake.
</Card>
<Card title="Protocol and transport" href="/protocol-and-transport">
JSON-RPC rules, bounded queues, and overload `-32001` on stdio and websocket paths.
</Card>
<Card title="Build a JSON-RPC client" href="/build-jsonrpc-client">
Framing, request ids, and `initialized` ordering for out-of-process clients.
</Card>
<Card title="Approvals and server requests" href="/approvals-and-server-requests">
Handling `ServerRequest` events from `next_event` and why overload rejection matters.
</Card>
<Card title="Stream turns and events" href="/stream-turns-and-events">
Consuming `ServerNotification` streams and lossless transcript tiers in the client facade.
</Card>
<Card title="Development and testing" href="/development-and-testing">
`test_app_server`, suite layout, and `just test -p codex-app-server`.
</Card>
</CardGroup>
