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
- Stop issuing requests after close(); check a closed flag in caller code before invoking request().
- Reorder shutdown so all pending requests complete before calling close().
- Recreate a new transport instance if further requests are needed.
- 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
- Track transport lifecycle in one owner module
- Cancel in-flight request loops on close via AbortController
- Await all pending requests before calling close() in tests and shutdown hooks
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
- Chat SDK endpoint runtime was retired
- Chat SDK runtime is shutting down
- provider turn ended with status ${turn.status}
- Plugin UI is not available (status: ${plugin.status})
- Plugin is not ready (current status: ${plugin.status})
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/1b400bcc2e08a9d3.
Report an issue: GitHub.