{"record":{"id":"8cf6c11edb5ac423","repo":"paperclipai/paperclip","slug":"durable-prp-control-plane-is-not-listening","errorCode":null,"errorMessage":"Durable PRP control plane is not listening.","messagePattern":"Durable PRP control plane is not listening\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts","lineNumber":1532,"sourceCode":"\n  getCommand(commandId: string): DurableRecoveryCoreCommand | undefined {\n    return (\n      this.#store.state.commands.find(\n        (command) => command.commandId === commandId,\n      ) ??\n      (this.#store.state.warmTransition?.command.commandId === commandId\n        ? this.#store.state.warmTransition.command\n        : undefined) ??\n      (this.#store.state.completedWarmTransition?.command.commandId ===\n      commandId\n        ? this.#store.state.completedWarmTransition.command\n        : undefined)\n    );\n  }\n\n  get connectUrl(): string {\n    if (this.#port === null) {\n      throw new Error(\"Durable PRP control plane is not listening.\");\n    }\n    return `ws://127.0.0.1:${this.#port}/durableRecovery/connect`;\n  }\n\n  async start(port = 0): Promise<void> {\n    if (this.#server !== null) {\n      throw new Error(\"Durable PRP control plane is already running.\");\n    }\n    const server = createServer((_request, response) => {\n      response.writeHead(404).end();\n    });\n    this.#server = server;\n    server.on(\"upgrade\", (request, socket, head) =>\n      this.handleUpgrade(request, socket, \"/durableRecovery/connect\", head),\n    );\n    await new Promise<void>((resolveListen, rejectListen) => {\n      server.once(\"error\", rejectListen);\n      server.listen(port, \"127.0.0.1\", () => {","sourceCodeStart":1514,"sourceCodeEnd":1550,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts#L1514-L1550","documentation":"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.","triggerScenarios":"Reading `connectUrl` before calling start(), or after the server was stopped/failed to bind, while `#port` remains null.","commonSituations":"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.","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."],"exampleFix":"// before\nconst url = controlPlane.connectUrl;\nawait controlPlane.start();\n// after\nawait controlPlane.start();\nconst url = controlPlane.connectUrl;","handlingStrategy":"try-catch","validationCode":"if (!controlPlane.isListening) throw new Error('call start() before obtaining connectUrl');","typeGuard":null,"tryCatchPattern":"let url: string;\ntry {\n  url = controlPlane.connectUrl;\n} catch (e) {\n  if (e.message.includes('not listening')) {\n    await controlPlane.start();\n    url = controlPlane.connectUrl;\n  } else throw e;\n}","preventionTips":["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()."],"tags":["lifecycle","server","websocket"],"backgroundTag":"invalid-state-transition","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}