paperclipai/paperclip · error

ACPX_PERSISTED_SESSION_IDENTITY_MISMATCH

ACPX_PERSISTED_SESSION_IDENTITY_MISMATCH

Error message

The persisted ACPX session identity changed after admission

What it means

After loading the persisted ACPX session record, persistedRuntimeStatus() compares the record's stored identity (acpxRecordId, acpSessionId, agentSessionId) against the identity captured at admission time. If any field differs, the runtime mutated its session identity after the session was admitted, and the adapter throws this coded error (ACPX_PERSISTED_SESSION_IDENTITY_MISMATCH) rather than reporting status based on a different underlying session. It protects against silently talking to a replaced backend session.

Source

Thrown at packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts:1255

): Promise<AcpxModelStatus> {
  const recordId = handle.acpxRecordId ?? handle.sessionKey;
  const record = await sessionStore.load(recordId);
  if (!record) {
    throw Object.assign(
      new Error("The pinned ACPX runtime omitted its persisted session record"),
      { code: "ACPX_PERSISTED_SESSION_MISSING" },
    );
  }
  const persistedAgentSessionId =
    nonEmptyRuntimeIdentity(record.agentSessionId) ?? record.acpSessionId;
  if (
    record.acpxRecordId !== identity.acpxRecordId ||
    record.acpSessionId !== identity.backendSessionId ||
    persistedAgentSessionId !== identity.agentSessionId
  ) {
    throw Object.assign(
      new Error("The persisted ACPX session identity changed after admission"),
      { code: "ACPX_PERSISTED_SESSION_IDENTITY_MISMATCH" },
    );
  }
  const currentModelId = record.acpx?.current_model_id;
  const availableModelIds = record.acpx?.available_models;
  return {
    summary: [
      `session=${record.acpxRecordId}`,
      `backendSessionId=${record.acpSessionId}`,
      `agentSessionId=${persistedAgentSessionId}`,
      record.closed === true ? "closed" : "open",
    ].join(" "),
    acpxRecordId: record.acpxRecordId,
    backendSessionId: record.acpSessionId,
    agentSessionId: persistedAgentSessionId,
    lastRequestId: record.lastRequestId,
    requestTokenUsage: structuredClone(record.request_token_usage ?? {}),
    usageCost: structuredClone(record.cumulative_cost),
    ...(currentModelId === undefined && !availableModelIds?.length

View on GitHub (pinned to 01ad858492)

Solutions

  1. Discard the stale handle and re-admit/re-create the ACPX session so identity and persisted record agree.
  2. Investigate runtime logs for a restart or session re-key between admission and the status call; upgrade the runtime if it rewrites identity improperly.
  3. If handles are persisted across process restarts, re-derive identity from the store instead of reusing the old admission identity.
  4. Clear the mismatched record and respawn to restore a consistent (record, identity) pair.

Example fix

// before
const status = await oldPort.getStatus(); // identity drifts from rewritten record
// after
try {
  const status = await oldPort.getStatus();
} catch (e) {
  if (e.code === "ACPX_PERSISTED_SESSION_IDENTITY_MISMATCH") {
    const fresh = await reacquireAcpxSession(handle.companyId); // new admission
    return fresh.getStatus();
  }
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

const record = await sessionStore.load(handle.acpxRecordId ?? handle.sessionKey);
if (record && (record.acpxRecordId !== identity.acpxRecordId ||
    record.acpSessionId !== identity.backendSessionId)) {
  throw new Error("persisted identity drifted; re-admit session");
}

Try / catch

try {
  return await port.getStatus();
} catch (e) {
  if (e.code === "ACPX_PERSISTED_SESSION_IDENTITY_MISMATCH") {
    return reacquireSession(handle); // fresh admission with matching identity
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling getStatus() where record.acpxRecordId !== identity.acpxRecordId, or record.acpSessionId !== identity.backendSessionId, or the record's agentSessionId/acpSessionId-derived agent id !== identity.agentSessionId.

Common situations: The ACPX runtime restarted and generated a new backend session id but reused the same record; a stale handle from a previous run is being checked against a rewritten record; session-store records from an older runtime version with different identity fields; a resume flow re-keying sessions without updating admission identity.

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/57c565d16f52cecc. Report an issue: GitHub.