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

  1. Verify the session/lease you are resuming with actually owns this state root; use the original session's identity or start fresh state
  2. If the state belongs to another session, move or delete it so rotation does not run against foreign identity
  3. Ensure each rotation attempt uses a new runId; the prior and desired runId must differ
  4. 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

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


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