How Broker Endpoints Are Created and Socket Communication Is Managed in the Codex Plugin

The Codex plugin creates platform-specific broker endpoints (named pipes on Windows, Unix sockets on POSIX) stored in temporary session directories, then manages IPC through newline-delimited JSON-RPC messages over Node.js net connections with a single-active-request enforcement policy.

The openai/codex-plugin-cc repository implements a shared broker architecture that allows multiple Codex agents to communicate with a single app-server instance via local IPC sockets. Understanding how broker endpoints are created and socket communication is managed reveals the deterministic lifecycle that enables reliable cross-platform agent coordination.

Creating Platform-Specific Broker Endpoints

When initializing a new broker session, the ensureBrokerSession function in broker-lifecycle.mjs generates a unique session directory and delegates endpoint construction to createBrokerEndpoint in broker-endpoint.mjs.

The implementation selects the appropriate IPC mechanism based on the platform:

// src: broker-endpoint.mjs#L10-L16
export function createBrokerEndpoint(sessionDir, platform = process.platform) {
  if (platform === "win32") {
    const pipeName = sanitizePipeName(`${path.win32.basename(sessionDir)}-codex-app-server`);
    return `pipe:\\\\.\\pipe\\${pipeName}`;
  }
  return `unix:${path.join(sessionDir, "broker.sock")}`;
}

Windows systems receive a named-pipe URI formatted as pipe:\\.\pipe\<sanitized-name>, while POSIX systems receive a Unix-domain socket URI prefixed with unix:. The session directory itself is created using mkdtempSync inside the system temporary folder (os.tmpdir()), ensuring unique, isolated socket paths for each broker instance.

Parsing and Validating Endpoint Strings

All consumer code uses parseBrokerEndpoint to transform the URI string into a structured object consumable by Node.js networking APIs:

// src: broker-endpoint.mjs#L19-L40
export function parseBrokerEndpoint(endpoint) {
  if (typeof endpoint !== "string" || endpoint.length === 0) {
    throw new Error("Missing broker endpoint.");
  }
  if (endpoint.startsWith("pipe:")) {
    const pipePath = endpoint.slice("pipe:".length);
    if (!pipePath) throw new Error("Broker pipe endpoint is missing its path.");
    return { kind: "pipe", path: pipePath };
  }
  if (endpoint.startsWith("unix:")) {
    const socketPath = endpoint.slice("unix:".length);
    if (!socketPath) throw new Error("Broker Unix socket endpoint is missing its path.");
    return { kind: "unix", path: socketPath };
  }
  throw new Error(`Unsupported broker endpoint: ${endpoint}`);
}

This function returns a {kind, path} object that abstracts away platform differences, allowing the rest of the codebase to use a unified connection interface.

Establishing Socket Connections

The connectToEndpoint helper in broker-lifecycle.mjs bridges the parsed endpoint to Node.js's net module:

// src: broker-lifecycle.mjs#L19-L22
function connectToEndpoint(endpoint) {
  const target = parseBrokerEndpoint(endpoint);
  return net.createConnection({ path: target.path });
}

This wrapper is used by all higher-level operations, including request dispatching and lifecycle management, ensuring consistent connection semantics across named pipes and Unix sockets.

Synchronizing Broker Readiness

Before clients can transmit requests, they must verify the broker process has bound to its socket. The waitForBrokerEndpoint function implements an exponential polling strategy:

// src: broker-lifecycle.mjs#L24-L40
export async function waitForBrokerEndpoint(endpoint, timeoutMs = 2000) {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    const ready = await new Promise((resolve) => {
      const socket = connectToEndpoint(endpoint);
      socket.on("connect", () => { socket.end(); resolve(true); });
      socket.on("error", () => resolve(false));
    });
    if (ready) return true;
    await new Promise(r => setTimeout(r, 50));
  }
  return false;
}

This utility attempts a connection every 50 milliseconds until success or until the default 2000ms timeout expires, preventing race conditions during broker startup.

Graceful Shutdown Protocol

To terminate the broker without dropping active connections, the client sends a JSON-RPC broker/shutdown message via sendBrokerShutdown:

// src: broker-lifecycle.mjs#L43-L55
export async function sendBrokerShutdown(endpoint) {
  await new Promise((resolve) => {
    const socket = connectToEndpoint(endpoint);
    socket.setEncoding("utf8");
    socket.on("connect", () => {
      socket.write(`${JSON.stringify({ id: 1, method: "broker/shutdown", params: {} })}\n`);
    });
    socket.on("data", () => { socket.end(); resolve(); });
    socket.on("error", resolve);
    socket.on("close", resolve);
  });
}

The broker process (app-server-broker.mjs) listens for this method (lines 60-64) and initiates a controlled teardown, closing all client sockets and removing the session directory.

The Broker Server Loop and Request Routing

The app-server-broker.mjs entry point implements the actual socket server using Node.js's net.createServer. After parsing the endpoint from CLI arguments, it:

  1. Creates a listener that works for both Windows pipes and Unix sockets using the parsed endpoint path.
  2. Tracks active sockets in a Set named sockets to ensure clean shutdown and resource cleanup.
  3. Enforces exclusivity: Only one request socket may hold an active transaction at a time. If a second client attempts a request while the broker is busy, it receives a JSON-RPC error with code BROKER_BUSY_RPC_CODE (lines 73-82).
  4. Routes notifications: The routeNotification function (lines 84-100) forwards server-side events to the currently active request socket.
  5. Handles special methods: initialize performs the handshake, while interrupt requests are allowed to pre-empt active streams (lines 70-84).

All messages are newline-delimited JSON-RPC objects. The send helper at line 29 encodes messages and appends \n, matching the simple line-based protocol expected by connectToEndpoint.

Complete Broker Lifecycle

The orchestration flows through broker-lifecycle.mjs in this sequence:

  1. Session initialization: ensureBrokerSession creates a temporary directory, calls createBrokerEndpoint, spawns the broker process via spawnBrokerProcess, and blocks until waitForBrokerEndpoint confirms the socket is listening.
  2. Persistence: The endpoint is serialized to broker.json in the session state directory (provided by resolveStateDir from state.mjs), enabling subsequent commands to reuse the same broker via loadBrokerSession.
  3. Request handling: Clients open connections with connectToEndpoint, transmit newline-terminated JSON-RPC requests, and await responses routed through the broker to the underlying CodexAppServerClient.
  4. Teardown: On process exit or explicit shutdown, teardownBrokerSession removes the socket file and session metadata, while the broker process cleans up its sockets Set and exits.

Practical Implementation Examples

The following example demonstrates starting a broker and sending a request through the socket:

// Start a broker and obtain its endpoint
import { ensureBrokerSession } from "./lib/broker-lifecycle.mjs";
import { parseBrokerEndpoint } from "./lib/broker-endpoint.mjs";
import net from "node:net";

async function startBroker(cwd) {
  const session = await ensureBrokerSession(cwd);
  if (!session) throw new Error("Failed to start broker");
  console.log("Broker listening at:", session.endpoint);
  return session.endpoint;
}

function sendJsonRpc(endpoint, request) {
  const { path } = parseBrokerEndpoint(endpoint);
  const socket = net.createConnection({ path });
  return new Promise((resolve, reject) => {
    socket.setEncoding("utf8");
    socket.once("data", (data) => {
      try { resolve(JSON.parse(data)); }
      catch (e) { reject(e); }
    });
    socket.once("error", reject);
    socket.write(`${JSON.stringify(request)}\n`);
  });
}

// Usage
(async () => {
  const endpoint = await startBroker(process.cwd());
  const response = await sendJsonRpc(endpoint, { 
    id: 1, 
    method: "initialize", 
    params: {} 
  });
  console.log(response);
})();

Summary

  • Endpoint generation uses createBrokerEndpoint in broker-endpoint.mjs to produce pipe: URIs on Windows and unix: URIs on POSIX, stored in unique temporary directories.
  • Connection management relies on parseBrokerEndpoint and connectToEndpoint to abstract platform differences and create Node.js net connections.
  • Readiness detection employs waitForBrokerEndpoint with a 50ms polling loop to ensure the broker socket is bound before clients transmit data.
  • Message protocol consists of newline-delimited JSON-RPC messages sent over the socket, with special handling for broker/shutdown in app-server-broker.mjs.
  • Concurrency control enforces a single-active-request policy via BROKER_BUSY_RPC_CODE, preventing simultaneous client requests while allowing interruptions.
  • Lifecycle cleanup removes socket files and session metadata through teardownBrokerSession when the broker exits or receives a shutdown command.

Frequently Asked Questions

What happens if two clients try to use the broker simultaneously?

The broker enforces exclusivity through a busy-state check in app-server-broker.mjs (lines 73-82). If a second client attempts to send a request while another request is active, the broker immediately returns a JSON-RPC error with code BROKER_BUSY_RPC_CODE. Only interrupt requests are allowed to pre-empt an active stream.

How does the broker handle platform differences between Windows and Linux/macOS?

The createBrokerEndpoint function detects the platform at runtime. On Windows (win32), it generates a named-pipe path (pipe:\\.\pipe\<name>), while on POSIX systems it uses a Unix-domain socket path (unix:/tmp/.../broker.sock). The parseBrokerEndpoint function normalizes these into a consistent {kind, path} structure, allowing net.createConnection to handle both mechanisms transparently.

What is the format of messages sent over the broker socket?

All messages follow a newline-delimited JSON-RPC 2.0 protocol. Requests and responses are serialized as JSON objects terminated with a \n character. This line-based framing allows the broker to parse complete messages using standard readline semantics, as implemented in the send helper at line 29 of app-server-broker.mjs.

How long does the client wait for the broker to become ready?

By default, waitForBrokerEndpoint waits up to 2000 milliseconds (configurable via the timeoutMs parameter). It attempts a connection every 50 milliseconds until successful, returning true if the socket accepts connections or false if the timeout expires before the broker binds to its endpoint.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →