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
- 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.
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
- 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.
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
- Completed warm transition template conflicts with its exact
- Warm run transition target conflicts with its durable receip
- Warm run transition template conflicts with its exact comman
- Warm run identity rotation requires a durable transition rec
- Durable PRP run attachment template is invalid.
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/29ae17cbb22562d2.
Report an issue: GitHub.