paperclipai/paperclip · error

Warm run identity rotation requires a durable transition rec

Error message

Warm run identity rotation requires a durable transition receipt.

What it means

rotateRunIdentity() was called with no warm transition and no completed transition receipt in the durable store, while the command log already contains a "run.attach" command. Identity rotation without a durable transition receipt is only allowed on a fresh (never-attached) store; an existing run.attach command means the run was previously attached, and its identity can only rotate through a proper warm transition with a receipt.

Source

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

        if (
          canonicalJson(runAttachTemplate) !== canonicalJson(expectedTemplate)
        ) {
          throw new Error(
            "Warm run transition template conflicts with its exact command.",
          );
        }
        const candidate = structuredClone(this.#store.state);
        candidate.runAttachTemplate = structuredClone(runAttachTemplate);
        this.#store.commit(candidate);
      }
      return;
    }
    if (
      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]) => [

View on GitHub (pinned to 01ad858492)

Solutions

  1. Perform the warm transition flow first so a durable receipt exists, then rotate via rotateRunIdentity.
  2. If the store should be fresh, start from a new store/identity instead of rotating an attached one.
  3. Recover from a backup/snapshot of the store that includes the transition receipt.
  4. Audit why warmTransition and completedWarmTransition are both absent while run.attach history exists (compaction bug) before proceeding.

Example fix

// before
controlPlane.rotateRunIdentity(newIdentity); // store already has run.attach, no receipt
// after
await performWarmTransition(newIdentity); // creates durable receipt
controlPlane.rotateRunIdentity(newIdentity, attachTemplate);
Defensive patterns

Strategy: validation

Validate before calling

const hasAttach = store.state.commands.some((c) => c.type === "run.attach");
const hasReceipt = store.state.warmTransition != null || store.state.completedWarmTransition != null;
if (hasAttach && !hasReceipt) {
  throw new Error("Cannot cold-rotate: run attached without durable receipt; use warm transition recovery.");
}

Try / catch

try {
  controlPlane.rotateRunIdentity(identity);
} catch (err) {
  if (err instanceof Error && err.message.includes("requires a durable transition receipt")) {
    await performWarmTransition(identity); // create receipt, then rotate
    controlPlane.rotateRunIdentity(identity, attachTemplate);
  } else throw err;
}

Prevention

When it happens

Trigger: Calling rotateRunIdentity(identity) on a store that has recorded a run.attach command but whose warmTransition/completedWarmTransition entries are absent (e.g. they were compacted, or the transition was never performed and the caller attempts a cold rotation over an attached run).

Common situations: Store compaction or reset dropped transition records while command history remained; calling cold-start rotation on a resumed run; migration of a store from a version that did not persist transitions.

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


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