{"record":{"id":"ebf3bd71c209befe","repo":"paperclipai/paperclip","slug":"no-available-loopback-port-found-at-or-above-pre","errorCode":null,"errorMessage":"No available loopback port found at or above ${preferredPort}.","messagePattern":"No available loopback port found at or above (.+?)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"cli/src/commands/test-drive.ts","lineNumber":157,"sourceCode":"  return fs.mkdtempSync(path.join(os.tmpdir(), \"paperclip-test-drive-\"));\n}\n\nasync function loopbackPortAvailable(port: number): Promise<boolean> {\n  return await new Promise<boolean>((resolve) => {\n    const server = createServer();\n    server.unref();\n    server.once(\"error\", () => resolve(false));\n    server.listen(port, \"127.0.0.1\", () => {\n      server.close(() => resolve(true));\n    });\n  });\n}\n\nexport async function resolveTestDriveServerPort(preferredPort = 3100): Promise<number> {\n  for (let port = preferredPort; port <= 65_535; port += 1) {\n    if (await loopbackPortAvailable(port)) return port;\n  }\n  throw new Error(`No available loopback port found at or above ${preferredPort}.`);\n}\n\n/**\n * Establish isolation before the CLI's normal config and .env loading hook.\n * The selected credential source is preserved in case its name happens to use\n * a PAPERCLIP_ prefix; all other Paperclip routing/configuration is discarded.\n */\nexport async function prepareTestDriveEnvironment(\n  options: Pick<TestDriveOptions, \"dataDir\" | \"apiKeyEnv\">,\n  cwd = process.cwd(),\n): Promise<{ dataDir: string; linkedWorktree: boolean }> {\n  const sourceEnvName = options.apiKeyEnv?.trim();\n  const preservedCredential = sourceEnvName ? process.env[sourceEnvName] : undefined;\n\n  for (const key of Object.keys(process.env)) {\n    if (key.startsWith(\"PAPERCLIP_\")) {\n      delete process.env[key];\n    }","sourceCodeStart":139,"sourceCodeEnd":175,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/cli/src/commands/test-drive.ts#L139-L175","documentation":"Like the other PRP rotation guards, this fires when the run rotation must prepare and rotate an EXTERNAL authority epoch but the required external state callbacks are absent. Here three callbacks are required: `readRunnerState`, `prepareExternalRunnerState`, and `archiveExternalRunnerState`; any missing one causes the throw after `prepareExternalRunnerState` could not even be invoked.","triggerScenarios":"A cold (non-exact-authority, non-warm-recovery) run rotation targeting an externally-owned state store where any of `readRunnerState`, `prepareExternalRunnerState`, or `archiveExternalRunnerState` is `undefined` in the transport options.","commonSituations":"Partial wiring of external-state plumbing when migrating between state owners; options objects built conditionally in tests or scripts; upgrading the transport and not noticing new required callbacks.","solutions":["Supply all three callbacks (`readRunnerState`, `prepareExternalRunnerState`, `archiveExternalRunnerState`) in the transport options.","Route the rotation through warm recovery when a recovery proof is available so external cold-start callbacks are not needed.","Audit the options construction site so no callback is dropped by conditional spread or defaults."],"exampleFix":"// before\nnew RunnerdCodexTransport({ readRunnerState, archiveExternalRunnerState });\n// after\nnew RunnerdCodexTransport({\n  readRunnerState,\n  prepareExternalRunnerState: async () => prepareState(),\n  archiveExternalRunnerState,\n});","handlingStrategy":"validation","validationCode":"const required = [\"readRunnerState\", \"prepareExternalRunnerState\", \"archiveExternalRunnerState\"] as const;\nfor (const k of required) {\n  if (typeof options[k] !== \"function\") throw new Error(`${k} callback required`);\n}","typeGuard":"function hasAllExternalCallbacks(o: unknown): boolean {\n  const x = o as Record<string, unknown>;\n  return [\"readRunnerState\",\"prepareExternalRunnerState\",\"archiveExternalRunnerState\"]\n    .every((k) => typeof x[k] === \"function\");\n}","tryCatchPattern":"try {\n  await transport.start(params);\n} catch (e) {\n  if (e.message === \"native_runner_prp_run_rotation_unavailable\") {\n    transport.reconfigure({ ...options, prepareExternalRunnerState, readRunnerState, archiveExternalRunnerState });\n  }\n}","preventionTips":["Wire external-state callbacks as a single cohesive group, never individually.","Validate the options object in tests with a schema (zod/ajv) at construction time.","Grep for the transport constructor to audit all construction sites when the callback contract changes."],"tags":["configuration","runner","state-rotation"],"backgroundTag":"missing-required-config-field","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"}