# Codex Plugin Error Handling and Retry Logic with the App Server

> Learn how the Codex plugin handles errors and implements retry logic with the app server using ProtocolError, automatic fallback, exponential back-off, and graceful cleanup.

- Repository: [OpenAI/codex-plugin-cc](https://github.com/openai/codex-plugin-cc)
- Tags: how-to-guide
- Published: 2026-07-29

---

**The Codex plugin wraps all app server communication failures in a unified `ProtocolError` class and implements resilient retry logic through automatic broker-to-spawn fallback, exponential back-off for busy RPC codes, and graceful process cleanup.**

The `openai/codex-plugin-cc` repository implements a robust JSON-RPC client layer that manages communication between the Codex plugin and the Codex App Server. All network and process errors are normalized through a centralized error handling strategy in `plugins/codex/scripts/lib/app-server.mjs`, while retry logic operates at both the transport connection layer and the application request layer.

## Core Error Handling Strategy

The `CodexAppServerClient` class encapsulates error handling through two concrete transport implementations: `SpawnedCodexAppServerClient` for direct process spawning and `BrokerCodexAppServerClient` for broker socket communication. All errors are normalized into **`ProtocolError`** instances that carry RPC codes and optional diagnostic data.

### JSON Parsing Failures

When processing incoming data streams, the client buffers chunks in `handleChunk` and parses individual lines via `handleLine`. If `JSON.parse` throws due to malformed protocol data, the client immediately invokes `handleExit` with a `ProtocolError` containing the offending line text. This prevents invalid JSON from crashing the client and provides debuggable context for protocol mismatches.

### Server Request Errors

When `handleLine` detects a JSON-RPC response containing an `error` field, it rejects the pending promise using `createProtocolError`. This factory method constructs a `ProtocolError` with the RPC error code, message, and any additional data from the server response. Callers can inspect `error.rpcCode` to implement conditional recovery logic.

### Unsupported Server Requests

If the app server sends a request method that the client does not implement (via `handleServerRequest`), the client automatically replies with JSON-RPC error code **`-32601`** (Method not found). This prevents hanging requests when the server protocol version exceeds client capabilities.

### Transport-Level Failures

**Process termination** (in `SpawnedCodexAppServerClient`) is detected via `proc.on('exit')`. Non-zero exit codes or termination signals trigger `handleExit`, which rejects all outstanding promises with the exit error.

**Socket errors** (in `BrokerCodexAppServerClient`) are captured through `socket.on('error')` and `socket.on('close')` events, propagating to `handleExit` for uniform cleanup.

**Standard error accumulation** occurs via a `proc.stderr` listener that aggregates data into `this.stderr`, ensuring diagnostic information is available in exit error messages.

## Transport Layer Retry and Reconnection Logic

The plugin’s retry mechanism operates primarily at the connection layer rather than inside individual JSON-RPC requests, with specific handling for broker congestion at the application layer.

### Broker Endpoint Discovery and Fallback Spawning

The `CodexAppServerClient.connect` method attempts to obtain a broker endpoint from three sources in priority order:

```javascript
options.brokerEndpoint ?? options.env?.[BROKER_ENDPOINT_ENV] ?? process.env[BROKER_ENDPOINT_ENV]

```

If no endpoint is available and `options.reuseExistingBroker` is false, the client calls `ensureBrokerSession` to create a new broker session. When broker connection fails entirely, the client automatically falls back to `SpawnedCodexAppServerClient`, effectively retrying the connection via direct process spawn without exposing the transport failure to the caller.

### Busy-RPC Handling with Exponential Back-Off

The constant **`BROKER_BUSY_RPC_CODE = -32001`** indicates broker-side resource exhaustion. When a request receives this RPC code, calling code implements retry logic with exponential back-off:

```javascript
async function safeRequest(client, method, params) {
  const MAX_ATTEMPTS = 3;
  let attempt = 0;
  let backoff = 100; // ms

  while (true) {
    try {
      return await client.request(method, params);
    } catch (e) {
      if (e.rpcCode === -32001 && ++attempt < MAX_ATTEMPTS) {
        await new Promise(r => setTimeout(r, backoff));
        backoff *= 2;
        continue;
      }
      throw e;
    }
  }
}

```

Command implementations in `plugins/codex/commands/*.md` follow this pattern when interacting with broker resources.

### Graceful Shutdown and Process Cleanup

The `close()` method ensures clean termination of both transport types. For spawned processes on Windows, the client uses `terminateProcessTree` from `plugins/codex/scripts/lib/process.mjs` to kill the entire process tree, preventing orphaned processes that could block subsequent retry attempts. This cleanup runs before resolving the close promise, ensuring transport resources are fully released.

## Implementation Examples

### Connecting with Automatic Fallback

```javascript
import { CodexAppServerClient } from "./plugins/codex/scripts/lib/app-server.mjs";

async function connectCodex(cwd) {
  // Attempts broker first, falls back to direct spawn automatically
  const client = await CodexAppServerClient.connect(cwd, {
    disableBroker: false  // default behavior
  });
  return client;
}

```

### Handling Busy Broker Retries

```javascript
const BROKER_BUSY_RPC_CODE = -32001;

async function resilientRequest(client, method, params) {
  try {
    return await client.request(method, params);
  } catch (e) {
    if (e.rpcCode === BROKER_BUSY_RPC_CODE) {
      await new Promise(r => setTimeout(r, 200));
      return client.request(method, params); // Retry once
    }
    throw e;
  }
}

```

### Clean Shutdown

```javascript
async function shutdown(client) {
  await client.close();  // Handles both spawned processes and broker sockets
}

```

## Key Source Files and Architecture

| File | Responsibility |
|------|--------------|
| `plugins/codex/scripts/lib/app-server.mjs` | Core `CodexAppServerClient` class, `ProtocolError` implementation, transport selection, and `handleExit` orchestration |
| `plugins/codex/scripts/lib/broker-lifecycle.mjs` | Broker session creation via `ensureBrokerSession` and broker reuse logic |
| `plugins/codex/scripts/lib/broker-endpoint.mjs` | UNIX socket endpoint parsing for broker transport configuration |
| `plugins/codex/scripts/lib/process.mjs` | `terminateProcessTree` implementation for Windows process cleanup |
| `plugins/codex/scripts/codex-companion.mjs` | High-level wrapper implementing busy-RPC retry loops for user-facing commands |

## Summary

- **Unified error model**: All transport and protocol failures normalize to `ProtocolError` with inspectable `rpcCode` properties.
- **Transport fallback**: The client automatically retries failed broker connections by spawning a direct app server process.
- **Busy-RPC recovery**: Callers handle `-32001` errors through exponential back-off retry loops implemented at the command layer.
- **Cross-platform cleanup**: Windows-specific `terminateProcessTree` ensures no orphaned processes interfere with subsequent connection attempts.
- **Graceful degradation**: Protocol errors like `-32601` for unsupported methods prevent client hangs while maintaining JSON-RPC compliance.

## Frequently Asked Questions

### How does the Codex plugin detect malformed JSON from the app server?

The `handleLine` method in `plugins/codex/scripts/lib/app-server.mjs` wraps `JSON.parse` in a try-catch block. When parsing fails, it invokes `handleExit` with a `ProtocolError` containing the raw line text, allowing developers to diagnose protocol mismatches without crashing the client process.

### What happens when the broker is too busy to handle a request?

The broker returns RPC error code `-32001` (`BROKER_BUSY_RPC_CODE`). The plugin does not automatically retry these at the transport layer; instead, calling code in the command implementations catches this specific code and implements exponential back-off retry logic before reissuing the request.

### Can the plugin recover if the app server process crashes during operation?

Yes. `SpawnedCodexAppServerClient` listens for `proc.on('exit')` events and triggers `handleExit`, which rejects all pending promises with the exit error. While this terminates the current session, the client can be reinstantiated via `CodexAppServerClient.connect`, which will attempt to establish a fresh broker connection or spawn a new process.

### How does the plugin prevent zombie processes on Windows?

When `close()` is called, the Windows implementation uses `terminateProcessTree` from `plugins/codex/scripts/lib/process.mjs` to forcefully terminate the entire process tree of the spawned app server. This prevents orphaned child processes that could otherwise exhaust system resources or block port reuse during subsequent connection retries.