paperclipai/paperclip · error
PRP recovery identity does not match the durable session bin
Error message
PRP recovery identity does not match the durable session binding
What it means
During local authority epoch rotation, rotateLocalAuthorityEpoch() compares the identity read from the prior durable state against the desired recovery identity. The runnerInstanceId, environmentLeaseId, and normalizedSessionId must match exactly, and the runId must differ (proving this is a new epoch of the same session, not a foreign or identical state). On any mismatch it throws 'PRP recovery identity does not match the durable session binding'.
Source
Thrown at packages/paperclip-runner/src/live/runnerd-codex-transport.ts:288
return state;
}
}
return null;
}
function rotateLocalAuthorityEpoch(
root: string,
controlPlaneState: Record<string, unknown>,
desired: DurableRecoveryIdentity,
): Record<string, unknown> {
const priorIdentity = controlPlaneIdentity(controlPlaneState);
if (
priorIdentity.runnerInstanceId !== desired.runnerInstanceId ||
priorIdentity.environmentLeaseId !== desired.environmentLeaseId ||
priorIdentity.normalizedSessionId !== desired.normalizedSessionId ||
priorIdentity.runId === desired.runId
) {
throw new Error(
"PRP recovery identity does not match the durable session binding",
);
}
const runnerDirectory = resolve(root, "runner");
const runnerStatePath = resolve(runnerDirectory, "runner-state.json");
const archive = authorityArchiveDirectory(root, priorIdentity);
const archivedControlPlane = resolve(archive, "control-plane");
const archivedRunnerState = resolve(archive, "runner-state.json");
const runnerStateSource = existsSync(runnerStatePath)
? runnerStatePath
: archivedRunnerState;
if (!existsSync(runnerStateSource)) {
throw new Error("native_runner_authority_rotation_state_unavailable");
}
assertRealDirectory(runnerDirectory);
const runnerState = readRunnerState(runnerStateSource);
if (
runnerState.runnerInstanceId !== priorIdentity.runnerInstanceId ||View on GitHub (pinned to 01ad858492)
Solutions
- Verify the session/lease you are resuming with actually owns this state root; use the original session's identity or start fresh state
- If the state belongs to another session, move or delete it so rotation does not run against foreign identity
- Ensure each rotation attempt uses a new runId; the prior and desired runId must differ
- Check for concurrent rotators — another process may have already rotated, changing the prior identity
Example fix
// before
await transport.resume({ runnerInstanceId: "other-instance", environmentLeaseId: leaseB, ... });
// after
await transport.resume({
runnerInstanceId: priorState.runnerInstanceId,
environmentLeaseId: priorState.environmentLeaseId,
normalizedSessionId: priorState.normalizedSessionId,
runId: nextRunId, // must differ from priorState.runId
}); Defensive patterns
Strategy: validation
Validate before calling
function identityMatchesBinding(prior: Identity, desired: Identity): boolean {
return prior.runnerInstanceId === desired.runnerInstanceId &&
prior.environmentLeaseId === desired.environmentLeaseId &&
prior.normalizedSessionId === desired.normalizedSessionId &&
prior.runId !== desired.runId;
} Try / catch
try {
await transport.resume(desired);
} catch (err) {
if (err.message.includes("PRP recovery identity does not match")) {
logger.error("resume identity != durable binding; use owning session or fresh state");
} else throw err;
} Prevention
- Track which session/lease owns each state root; never resume with another session's identity
- Ensure each rotation/resume uses a fresh runId
- Avoid cloning or copying state directories between environments
- Guard against concurrent resumers of the same state root
When it happens
Trigger: #resume() requests rotation when the on-disk prior state belongs to a different runner instance, a different environment lease, a different session, or when the runId is unchanged (no epoch progress).
Common situations: Pointing a new session at a reused state directory from another session/lease; recycled environment lease IDs after infra churn; stale state not archived between runs; attempt to rotate twice with the same runId.
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
- native_runner_authority_rotation_requires_settled_state
- native_runner_authority_rotation_state_unavailable
- thread/read returned a different driver session
- thread/items/list returned a different turn
- native_runner_control_plane_state_unsafe
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/e63df1d699a3c3b0.
Report an issue: GitHub.