paperclipai/paperclip · error

PRP Codex transport is closed

Error message

PRP Codex transport is closed

What it means

request() is the JSON-RPC entry point of the PRP Codex transport; once close() has run (#closed is set), every request is rejected because the underlying transport can no longer service calls. This is a lifecycle guard, not a transient failure.

Source

Thrown at packages/paperclip-runner/src/live/runnerd-codex-transport.ts:3284

                ? createSanitizedAwsAgentCoreEnvironment(
                    options.environment,
                    resolve(this.#root, "codex-home"),
                  )
                : createSanitizedCodexEnvironment(options.environment),
      ).sort(),
      diagnostics: ["lab transport selected authenticated durable PRP"],
    };
  }

  evidence(): CapabilityRunnerdProcessEvidence {
    return structuredClone(this.#evidence);
  }

  async request(
    method: string,
    params: Record<string, unknown>,
  ): Promise<Record<string, unknown>> {
    if (this.#closed) throw new Error("PRP Codex transport is closed");
    this.#throwIfFailed();
    if (
      this.#pendingWarmRecoveryCompletion !== null &&
      !["thread/read", "initialize", "collaborationMode/list"].includes(method)
    ) {
      throw new Error("native_runner_warm_transition_completion_pending");
    }
    if (method === "initialize") return { user: {} };
    if (method === "thread/start") return this.#start(params);
    if (method === "collaborationMode/list") {
      // runnerd negotiates the real Codex preset or the provider-proxy-owned
      // planning contract during session.open. This transport-level mask
      // confirms that closed boundary; turn/start remains runner-managed and
      // never forwards this sentinel to the outer TypeScript driver.
      return this.options.provider === undefined ||
        this.options.provider === "codex" ||
        this.options.provider === "opencode" ||
        this.options.provider === "acpx"

View on GitHub (pinned to 01ad858492)

Solutions

  1. Stop issuing requests after close(); check a closed flag in caller code before invoking request().
  2. Reorder shutdown so all pending requests complete before calling close().
  3. Recreate a new transport instance if further requests are needed.
  4. Use a finally/abort signal to cancel request loops when the transport closes.

Example fix

// before
await transport.close();
await transport.request('thread/read', { threadId }); // throws
// after
if (!transport.isClosed()) await transport.request('thread/read', { threadId });
Defensive patterns

Strategy: try-catch

Validate before calling

if (transport.isClosed?.()) throw new Error('transport already closed; refusing request');

Try / catch

try {
  await transport.request(method, params);
} catch (err) {
  if ((err as Error).message === 'PRP Codex transport is closed') {
    transport = createTransport();
    await transport.request(method, params);
  } else throw err;
}

Prevention

When it happens

Trigger: Calling transport.request(method, params) after transport.close() (or process shutdown path) has marked the transport closed.

Common situations: In-flight async work issuing follow-up requests during shutdown; callers keeping a transport reference after close; retry logic racing teardown; tests not awaiting close before assertions.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/1b400bcc2e08a9d3. Report an issue: GitHub.