# Connection lifecycle

> Per-connection `initialize` handshake, `clientInfo` requirements, notification opt-out, thread subscribe/unsubscribe idle unload, and server-initiated requests vs client RPC.

- 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

- `README.md`
- `src/request_processors/initialize_processor.rs`
- `src/message_processor.rs`
- `src/transport.rs`
- `src/connection_rpc_gate.rs`
- `tests/suite/v2/client_metadata.rs`

---

---
title: "Connection lifecycle"
description: "Per-connection `initialize` handshake, `clientInfo` requirements, notification opt-out, thread subscribe/unsubscribe idle unload, and server-initiated requests vs client RPC."
---

Each transport connection to `codex app-server` is an isolated JSON-RPC session: one `initialize` handshake per connection, per-connection capability negotiation, thread event subscriptions scoped to that connection, and bidirectional RPC where the server can emit client-bound requests mid-turn. `MessageProcessor` and `ConnectionSessionState` in `src/message_processor.rs` gate client RPC until initialization completes; `src/transport.rs` applies per-connection notification filters on the outbound path.

## Per-connection state model

```mermaid
stateDiagram-v2
    [*] --> Open: Transport connects
    Open --> Initialized: initialize succeeds
    Initialized --> Initialized: Client RPC + server requests
    Initialized --> Draining: Connection closes
    Draining --> [*]: ConnectionRpcGate waits for in-flight handlers

    note right of Open
        RPC other than initialize
        returns "Not initialized"
    end note

    note right of Initialized
        outbound_initialized enables
        broadcast notifications
    end note
```

| Layer | Responsibility |
| --- | --- |
| `ConnectionSessionState` | Stores `initialize` outcome: experimental API flag, opt-out set, `clientInfo`, attestation capability |
| `ConnectionRpcGate` | Serializes handler shutdown: no new work after close; in-flight handlers finish |
| `ThreadStateManager` | Maps connections ↔ loaded threads (subscriptions) |
| `OutboundConnectionState` | Per-connection outbound queue, `initialized` flag, opt-out filter, experimental API flag |

Connections do not share request IDs. A websocket integration test confirms that `initialize` responses and later RPC responses route only to the connection that sent them.

## Initialize handshake

<Steps>
<Step title="Send `initialize` (once per connection)">

Send a single JSON-RPC request with method `initialize`. This must be the first client RPC on that connection.

<RequestExample>

```json
{
  "method": "initialize",
  "id": 0,
  "params": {
    "clientInfo": {
      "name": "my_integration",
      "title": "My Integration",
      "version": "1.0.0"
    },
    "capabilities": {
      "experimentalApi": true,
      "requestAttestation": false,
      "optOutNotificationMethods": ["thread/started", "item/agentMessage/delta"]
    }
  }
}
```

</RequestExample>

</Step>

<Step title="Read `initialize` response">

On success the server returns:

<ResponseField name="userAgent" type="string">
User agent string presented to upstream services (derived from `clientInfo.name` and `version` for originating clients).
</ResponseField>

<ResponseField name="codexHome" type="string">
Absolute path to the server’s Codex home directory.
</ResponseField>

<ResponseField name="platformFamily" type="string">
Runtime platform family (for example `unix`, `windows`).
</ResponseField>

<ResponseField name="platformOs" type="string">
Runtime OS (for example `macos`, `linux`, `windows`).
</ResponseField>

</Step>

<Step title="Emit `initialized` notification">

After receiving the `initialize` response, send a client notification (no `id` field):

```json
{
  "method": "initialized"
}
```

Official test clients and in-process embedders follow this ordering. The server’s RPC gate opens when `initialize` completes; the `initialized` notification is part of the documented client contract.

</Step>

<Step title="Receive post-init server notifications">

For socket transports, the server then enables outbound delivery on that connection and may send connection-scoped notifications such as `configWarning` (from config parse warnings) and `remoteControl/statusChanged`. Broadcast notifications are delivered only to connections whose outbound `initialized` flag is set.

</Step>
</Steps>

### Errors and constraints

| Condition | JSON-RPC error |
| --- | --- |
| Any client RPC before `initialize` succeeds | `"Not initialized"` (`-32600` invalid request) |
| Second `initialize` on the same connection | `"Already initialized"` |
| `clientInfo.name` is not a valid HTTP header value | `"Invalid clientInfo.name: '…'. Must be a valid HTTP header value."` |
| Experimental method without `capabilities.experimentalApi` | Experimental-required message from handler |

## `clientInfo` requirements

<ParamField body="clientInfo.name" type="string" required>
Stable client identifier. Must be a valid HTTP header value (validated before session state is committed). Used for upstream user-agent construction and compliance logging. Enterprise integrations should use a known, registered name.
</ParamField>

<ParamField body="clientInfo.title" type="string">
Human-readable client title (optional).
</ParamField>

<ParamField body="clientInfo.version" type="string" required>
Client version string; appended to the user-agent suffix as `{name}; {version}`.
</ParamField>

### Originator and user-agent behavior

| `clientInfo.name` | Process-global originator |
| --- | --- |
| Normal clients (for example `codex_vscode`, `xcode`) | Sets default originator and user-agent suffix |
| `codex_app_server_daemon` | Does **not** override originator (probe/daemon clients) |
| `codex-backend` | Does **not** override originator |

`CODEX_INTERNAL_ORIGINATOR_OVERRIDE` can pre-set the originator; a later `initialize` from a normal client does not replace an already-set originator.

<Note>
`clientInfo.name` is forwarded into turn notify payloads and upstream metadata. Pick a stable, unique name per product surface.
</Note>

## Capabilities negotiated at initialize

<ParamField body="capabilities.experimentalApi" type="boolean">
When `true`, experimental v2 RPCs and notification fields are allowed on this connection. When `false`, experimental server notifications are dropped on outbound and experimental RPCs are rejected.
</ParamField>

<ParamField body="capabilities.requestAttestation" type="boolean">
When `true`, this connection is eligible to receive `attestation/generate` server requests for upstream `x-oai-attestation`. The first attestation-capable subscriber on a thread is preferred.
</ParamField>

<ParamField body="capabilities.optOutNotificationMethods" type="string[]">
Exact notification method names to suppress for this connection only. Matching is exact (no wildcards or prefixes). Unknown names are accepted and ignored.
</ParamField>

### Notification opt-out

Opt-out is evaluated in `src/transport.rs` when enqueueing outbound messages:

- Suppressed if the method is listed in `optOutNotificationMethods`.
- Suppressed if the notification is experimental and `experimentalApi` is `false` on the connection.

Integration tests confirm that `thread/started` does not arrive when opted out, while the `thread/start` RPC response still returns normally.

<Warning>
Opt-out affects **notifications only**, not RPC responses. High-volume streams (`item/agentMessage/delta`, command output deltas) are typical opt-out candidates.
</Warning>

## Thread subscribe and unsubscribe

### Auto-subscribe

`thread/start`, `thread/resume`, and `thread/fork` **auto-subscribe** the calling connection to that thread’s turn/item notifications. The server attaches a per-thread listener via `ensure_conversation_listener` and records the connection in `ThreadStateManager`.

Thread-scoped events (including server-initiated approval requests) are sent only to **subscribed** connections for that `threadId`.

### `thread/unsubscribe`

Call `thread/unsubscribe` with `{ "threadId": "…" }` to stop receiving turn/item notifications on this connection without necessarily unloading the thread from memory.

| `status` | Meaning |
| --- | --- |
| `unsubscribed` | Connection was subscribed and is now removed |
| `notSubscribed` | Connection was not subscribed to that thread |
| `notLoaded` | Thread is not loaded in memory |

<Info>
`thread/unsubscribe` during an in-flight turn stops **notifications** to that connection but does not cancel the turn. Server-initiated requests for in-flight work can still require a response on the same connection if the handler already issued them.
</Info>

### Idle unload (`thread/closed`)

After the last subscriber unsubscribes, the thread **stays loaded** until **both**:

1. No connections are subscribed (`has_subscribers == false`), and  
2. Thread status is not `active` (no running turn),

for a continuous **`THREAD_UNLOADING_DELAY` of 30 minutes** (`src/request_processors/thread_lifecycle.rs`).

Then the server shuts down the in-memory thread, cancels pending server→client requests for that thread, and emits `thread/closed` with `{ "threadId": "…" }`. Tests verify that `thread/closed` does **not** fire immediately after `thread/unsubscribe`; `thread/loaded/list` still lists the thread until idle timeout.

```text
thread/start ──► auto-subscribe connection
       │
       ▼
thread/unsubscribe ──► status: unsubscribed (thread still loaded)
       │
       ▼
30 min idle (no subscribers + not active)
       │
       ▼
thread/closed + memory released
```

## Connection teardown

When a transport connection closes:

1. `ConnectionRpcGate::shutdown` stops accepting new handler work and waits for in-flight RPC handlers.
2. `OutgoingMessageSender::connection_closed` clears pending server-request callbacks for that connection.
3. `ThreadStateManager::remove_connection` unsubscribes the connection from all threads.
4. FS watches, command exec sessions, and process exec state for that connection are cleared.

If a connection drops while a server request is outstanding, the client should treat the request as abandoned; the server cancels thread-scoped pending requests on unload.

## Client RPC vs server-initiated requests

Codex app-server uses **bidirectional JSON-RPC 2.0** (JSONL on stdio, one message per websocket frame on socket transports).

| Direction | Initiator | Wire shape | Examples |
| --- | --- | --- | --- |
| Client → server | Client | Request with client-chosen `id` | `initialize`, `thread/start`, `turn/start`, `config/read` |
| Server → client | Server | Request with server-chosen `id` | `item/commandExecution/requestApproval`, `item/tool/call`, `mcpServer/elicitation/request` |
| Either | Peer | Response matching `id` | Initialize result; approval `{ "decision": "accept" }` |
| Server → client | Server | Notification (no `id`) | `turn/started`, `item/started`, `serverRequest/resolved` |

### Server request catalog (v2)

| Method | Purpose |
| --- | --- |
| `item/commandExecution/requestApproval` | Shell/command approval during a turn |
| `item/fileChange/requestApproval` | Patch/file-change approval |
| `item/tool/requestUserInput` | Structured user input for a tool (experimental) |
| `item/permissions/requestApproval` | Permission profile selection |
| `item/tool/call` | Dynamic tool execution on the client |
| `mcpServer/elicitation/request` | MCP elicitation (form or URL) |
| `account/chatgptAuthTokens/refresh` | ChatGPT auth token refresh |
| `attestation/generate` | Upstream attestation token (requires `requestAttestation`) |

### Handling server requests in your client

<Steps>
<Step title="Read JSON-RPC requests on the same stream as notifications">

While waiting for `turn/completed`, your reader must handle interleaved **server requests**. Do not assume the stream is notification-only.

</Step>
<Step title="Correlate with `threadId` / `turnId`">

Approval and tool requests include `threadId` and usually `turnId`. Scope UI to the active conversation.

</Step>
<Step title="Respond with a JSON-RPC response">

Reply with the same `id` and a result object matching the request’s response schema (for example `{ "decision": "accept" }` for command approval).

</Step>
<Step title="Observe `serverRequest/resolved`">

After your response—or if the turn ends or is interrupted before you answer—the server emits `serverRequest/resolved` with `{ "threadId", "requestId" }` so clients can clear pending UI state.

</Step>
</Steps>

Server requests for a thread are targeted at **subscribed** connections. On `thread/resume`, the server may **replay** still-pending server requests to the resuming connection after the resume response.

### `ConnectionRpcGate` and in-flight RPC

After `initialize`, each client RPC runs through `ConnectionRpcGate::run`. On connection close the gate rejects new handlers while allowing started handlers to finish, avoiding torn state during teardown.

Some RPCs use serialization queues (per connection or per thread) so related operations do not overlap; most handlers are spawned concurrently behind the gate.

## Transport-specific notes

| Transport | Handshake notes |
| --- | --- |
| stdio (default) | One process-wide stream; `outbound_initialized` flips when `initialize` completes in `lib.rs` |
| unix control socket / websocket | Per-connection `ConnectionState`; initialize response is connection-scoped |
| In-process (`in_process`) | `start()` sends `initialize` + `initialized` and sets `outbound_initialized` inside the shared handler |

Websocket clients must call `initialize` per websocket connection before any other RPC on that socket.

## Verification checklist

| Check | Expected signal |
| --- | --- |
| Pre-init RPC | Error message `"Not initialized"` |
| Successful init | `userAgent`, `codexHome`, `platformFamily`, `platformOs` in response |
| Opt-out | Listed notification methods never arrive on that connection |
| `thread/unsubscribe` | `status: "unsubscribed"`; no immediate `thread/closed` |
| Idle unload | `thread/closed` after 30 minutes with no subscribers and idle thread |
| Server approval | Incoming request with `id`; reply; then `serverRequest/resolved` |

## Related pages

<CardGroup>
<Card title="Quickstart" href="/quickstart">
End-to-end first connection from `initialize` through `turn/completed`.
</Card>
<Card title="Protocol and transport" href="/protocol-and-transport">
JSON-RPC framing, transports, overload `-32001`, and queue backpressure.
</Card>
<Card title="Build a JSON-RPC client" href="/build-jsonrpc-client">
Framing, request IDs, `initialized` ordering, and mid-turn server requests.
</Card>
<Card title="Threads, turns, and items" href="/threads-turns-items">
Thread subscription model and thread status states.
</Card>
<Card title="Approvals and server requests" href="/approvals-and-server-requests">
Approval flows, MCP elicitations, and `serverRequest/resolved` lifecycle.
</Card>
<Card title="Stream turns and events" href="/stream-turns-and-events">
Turn/item notifications and opt-out for high-volume streams.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
`Not initialized`, overload retries, and connection errors.
</Card>
</CardGroup>
