paperclipai/paperclip · error
Durable PRP control plane is not listening.
Error message
Durable PRP control plane is not listening.
What it means
The connectUrl getter returns the ws://127.0.0.1:<port>/durableRecovery/connect URL for the control plane. It throws when the internal HTTP server has not been started (port is null), because there is no endpoint to connect to yet.
Solutions
- Call `await controlPlane.start()` (or start(port)) before reading connectUrl.
- Use the value returned by start()/the port it bound rather than caching connectUrl from before startup.
- Check start() errors: a bind failure (EADDRINUSE) leaves the plane not listening — pick a free port.
- Re-start the server after a stop() before handing the URL to recovery clients.
Example fix
// before const url = controlPlane.connectUrl; await controlPlane.start(); // after await controlPlane.start(); const url = controlPlane.connectUrl;
Defensive patterns
Strategy: try-catch
Validate before calling
if (!controlPlane.isListening) throw new Error('call start() before obtaining connectUrl'); Try / catch
let url: string;
try {
url = controlPlane.connectUrl;
} catch (e) {
if (e.message.includes('not listening')) {
await controlPlane.start();
url = controlPlane.connectUrl;
} else throw e;
} Prevention
- Always await start() before exposing connectUrl to recovery clients.
- Handle start() bind failures (choose a free port or detect EADDRINUSE).
- Invalidate cached URLs after stop().
When it happens
Trigger: Reading `connectUrl` before calling start(), or after the server was stopped/failed to bind, while `#port` remains null.
Common situations: Client code constructs the control plane and reads connectUrl immediately instead of awaiting start(); start() threw on bind failure (e.g. port in use) so port was never set; server was stopped then connectUrl read.
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
- Capability live session is not connected
- ACPX provider ownership admission is already active
- ACPX provider ownership admission is closed
- ACPX runtime host is closing
- action_task_closed
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-10).
Data as JSON: /api/errors/8cf6c11edb5ac423.
Report an issue: GitHub.
Appendix: source
Thrown at packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts:1533
getCommand(commandId: string): DurableRecoveryCoreCommand | undefined {
return (
this.#store.state.commands.find(
(command) => command.commandId === commandId,
) ??
(this.#store.state.warmTransition?.command.commandId === commandId
? this.#store.state.warmTransition.command
: undefined) ??
(this.#store.state.completedWarmTransition?.command.commandId ===
commandId
? this.#store.state.completedWarmTransition.command
: undefined)
);
}
get connectUrl(): string {
if (this.#port === null) {
throw new Error("Durable PRP control plane is not listening.");
}
return `ws://127.0.0.1:${this.#port}/durableRecovery/connect`;
}
async start(port = 0): Promise<void> {
if (this.#server !== null) {
throw new Error("Durable PRP control plane is already running.");
}
const server = createServer((_request, response) => {
response.writeHead(404).end();
});
this.#server = server;
server.on("upgrade", (request, socket, head) =>
this.handleUpgrade(request, socket, "/durableRecovery/connect", head),
);
await new Promise<void>((resolveListen, rejectListen) => {
server.once("error", rejectListen);
server.listen(port, "127.0.0.1", () => {View on GitHub (pinned to 3f1d897a7c)