paperclipai/paperclip · error

Durable PRP run identity rotation is invalid.

Error message

Durable PRP run identity rotation is invalid.

What it means

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.

Source

Thrown at packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts:1684

      this.#store.state.commands.some(
        (command) => command.type === "run.attach",
      )
    ) {
      throw new Error(
        "Warm run identity rotation requires a durable transition receipt.",
      );
    }
    if (
      !Object.values(identity).every(
        (value) => typeof value === "string" && stableIdPattern.test(value),
      ) ||
      identity.runnerInstanceId !== this.#identity.runnerInstanceId ||
      identity.environmentLeaseId !== this.#identity.environmentLeaseId ||
      identity.normalizedSessionId !== this.#identity.normalizedSessionId ||
      identity.runId === this.#identity.runId ||
      this.#store.state.commands.some((command) => command.status === "pending")
    ) {
      throw new Error("Durable PRP run identity rotation is invalid.");
    }
    this.disconnectActiveRunner();
    const leases = Object.fromEntries(
      Object.entries(this.#store.state.leases).map(([key, lease]) => [
        key,
        { ...lease, identity: structuredClone(identity) },
      ]),
    );
    Object.assign(this.#store.state, initialCoreState(identity), {
      leases,
      runAttachTemplate:
        runAttachTemplate === undefined
          ? null
          : structuredClone(runAttachTemplate),
    });
    this.#identity = structuredClone(identity);
    this.#store.save();
  }

View on GitHub (pinned to 01ad858492)

Solutions

  1. Ensure all in-flight commands complete (none with status "pending") before rotating; drain or settle the command log first.
  2. Copy runnerInstanceId, environmentLeaseId, and normalizedSessionId verbatim from the current identity and only change runId.
  3. Validate each field against stableIdPattern and confirm the new runId differs from the current runId before calling.
  4. If a pending command is stuck, resolve it via its own recovery/timeout path rather than forcing rotation.

Example fix

// before
controlPlane.rotateRunIdentity({ runnerInstanceId: "new", environmentLeaseId: current.environmentLeaseId, normalizedSessionId: current.normalizedSessionId, runId: current.runId }); // changed instance, reused runId
// after
if (!store.state.commands.some((c) => c.status === "pending")) {
  controlPlane.rotateRunIdentity({ ...structuredClone(current), runId: generateNewRunId() });
}
Defensive patterns

Strategy: validation

Validate before calling

function assertRotatable(identity: DurableRecoveryIdentity, current: DurableRecoveryIdentity, state: { commands: { status: string }[] }): void {
  const okShape = Object.values(identity).every((v) => typeof v === "string" && stableIdPattern.test(v as string));
  if (!okShape) throw new Error("identity fields must be stable-id strings");
  if (identity.runnerInstanceId !== current.runnerInstanceId) throw new Error("runnerInstanceId must not change");
  if (identity.environmentLeaseId !== current.environmentLeaseId) throw new Error("environmentLeaseId must not change");
  if (identity.normalizedSessionId !== current.normalizedSessionId) throw new Error("normalizedSessionId must not change");
  if (identity.runId === current.runId) throw new Error("runId must change");
  if (state.commands.some((c) => c.status === "pending")) throw new Error("pending commands must settle first");
}

Type guard

function isStableIdentity(v: unknown): v is DurableRecoveryIdentity {
  return typeof v === "object" && v !== null && Object.values(v).every((x) => typeof x === "string" && stableIdPattern.test(x));
}

Try / catch

try {
  controlPlane.rotateRunIdentity(identity);
} catch (err) {
  if (err instanceof Error && err.message === "Durable PRP run identity rotation is invalid.") {
    await settlePendingCommands();
    controlPlane.rotateRunIdentity({ ...structuredClone(current), runId: generateRunId() });
  } else throw err;
}

Prevention

When it happens

Trigger: 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.).

Common situations: 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).

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/29ae17cbb22562d2. Report an issue: GitHub.