{"record":{"id":"29ae17cbb22562d2","repo":"paperclipai/paperclip","slug":"durable-prp-run-identity-rotation-is-invalid","errorCode":null,"errorMessage":"Durable PRP run identity rotation is invalid.","messagePattern":"Durable PRP run identity rotation is invalid\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts","lineNumber":1684,"sourceCode":"      this.#store.state.commands.some(\n        (command) => command.type === \"run.attach\",\n      )\n    ) {\n      throw new Error(\n        \"Warm run identity rotation requires a durable transition receipt.\",\n      );\n    }\n    if (\n      !Object.values(identity).every(\n        (value) => typeof value === \"string\" && stableIdPattern.test(value),\n      ) ||\n      identity.runnerInstanceId !== this.#identity.runnerInstanceId ||\n      identity.environmentLeaseId !== this.#identity.environmentLeaseId ||\n      identity.normalizedSessionId !== this.#identity.normalizedSessionId ||\n      identity.runId === this.#identity.runId ||\n      this.#store.state.commands.some((command) => command.status === \"pending\")\n    ) {\n      throw new Error(\"Durable PRP run identity rotation is invalid.\");\n    }\n    this.disconnectActiveRunner();\n    const leases = Object.fromEntries(\n      Object.entries(this.#store.state.leases).map(([key, lease]) => [\n        key,\n        { ...lease, identity: structuredClone(identity) },\n      ]),\n    );\n    Object.assign(this.#store.state, initialCoreState(identity), {\n      leases,\n      runAttachTemplate:\n        runAttachTemplate === undefined\n          ? null\n          : structuredClone(runAttachTemplate),\n    });\n    this.#identity = structuredClone(identity);\n    this.#store.save();\n  }","sourceCodeStart":1666,"sourceCodeEnd":1702,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts#L1666-L1702","documentation":"The proposed identity in rotateRunIdentity() failed structural validation: every field must be a string matching stableIdPattern, runnerInstanceId/environmentLeaseId/normalizedSessionId must equal the current identity's values (only runId rotates), the new runId must differ from the current one, and no command in the store may be status \"pending\". Any violated condition produces this single fail-closed error.","triggerScenarios":"Passing an identity with a non-string or malformed field; changing runnerInstanceId, environmentLeaseId, or normalizedSessionId; reusing the current runId; or calling while any durable command is still pending (in-flight attach, semantic tool call, etc.).","commonSituations":"Caller builds the identity object with a typo'd or missing field; generated runId collides with the old one; rotation attempted while a command is still executing after a crash; ids generated in a format not matching stableIdPattern (e.g. with invalid characters).","solutions":["Ensure all in-flight commands complete (none with status \"pending\") before rotating; drain or settle the command log first.","Copy runnerInstanceId, environmentLeaseId, and normalizedSessionId verbatim from the current identity and only change runId.","Validate each field against stableIdPattern and confirm the new runId differs from the current runId before calling.","If a pending command is stuck, resolve it via its own recovery/timeout path rather than forcing rotation."],"exampleFix":"// before\ncontrolPlane.rotateRunIdentity({ runnerInstanceId: \"new\", environmentLeaseId: current.environmentLeaseId, normalizedSessionId: current.normalizedSessionId, runId: current.runId }); // changed instance, reused runId\n// after\nif (!store.state.commands.some((c) => c.status === \"pending\")) {\n  controlPlane.rotateRunIdentity({ ...structuredClone(current), runId: generateNewRunId() });\n}","handlingStrategy":"validation","validationCode":"function assertRotatable(identity: DurableRecoveryIdentity, current: DurableRecoveryIdentity, state: { commands: { status: string }[] }): void {\n  const okShape = Object.values(identity).every((v) => typeof v === \"string\" && stableIdPattern.test(v as string));\n  if (!okShape) throw new Error(\"identity fields must be stable-id strings\");\n  if (identity.runnerInstanceId !== current.runnerInstanceId) throw new Error(\"runnerInstanceId must not change\");\n  if (identity.environmentLeaseId !== current.environmentLeaseId) throw new Error(\"environmentLeaseId must not change\");\n  if (identity.normalizedSessionId !== current.normalizedSessionId) throw new Error(\"normalizedSessionId must not change\");\n  if (identity.runId === current.runId) throw new Error(\"runId must change\");\n  if (state.commands.some((c) => c.status === \"pending\")) throw new Error(\"pending commands must settle first\");\n}","typeGuard":"function isStableIdentity(v: unknown): v is DurableRecoveryIdentity {\n  return typeof v === \"object\" && v !== null && Object.values(v).every((x) => typeof x === \"string\" && stableIdPattern.test(x));\n}","tryCatchPattern":"try {\n  controlPlane.rotateRunIdentity(identity);\n} catch (err) {\n  if (err instanceof Error && err.message === \"Durable PRP run identity rotation is invalid.\") {\n    await settlePendingCommands();\n    controlPlane.rotateRunIdentity({ ...structuredClone(current), runId: generateRunId() });\n  } else throw err;\n}","preventionTips":["Build rotation identities by spreading the current identity and overriding only runId.","Settle all pending commands before rotating; drain after crash recovery.","Unit-test generated ids against stableIdPattern before they reach the control plane."],"tags":["validation","identity-rotation","state-machine","fail-closed"],"backgroundTag":"invalid-argument-value","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"}