{"record":{"id":"3d6c062acfa20856","repo":"paperclipai/paperclip","slug":"acpx-recovery-identity-does-not-match-the-persiste","errorCode":null,"errorMessage":"ACPX recovery identity does not match the persisted runtime record","messagePattern":"ACPX recovery identity does not match the persisted runtime record","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/drivers/acpx/recovery-identity.ts","lineNumber":190,"sourceCode":"  if (persisted === null) return;\n\n  const record = parsePersistedRecord(persisted);\n  if (\n    record.acpxRecordId !== expected.acpxRecordId ||\n    record.backendSessionId !== expected.backendSessionId ||\n    record.agentSessionId !== expected.agentSessionId ||\n    record.normalizedSessionId !== binding.normalizedSessionId ||\n    record.profileDigest !== binding.profileDigest ||\n    record.workspaceDigest !== binding.workspaceDigest ||\n    record.requestedModel !== binding.requestedModel ||\n    record.effectiveModel !== binding.effectiveModel ||\n    record.permissionMode !== binding.permissionMode ||\n    !sameFenceCandidates(\n      record.providerLifetimeFenceCandidates,\n      expected.providerLifetimeFenceCandidates,\n    )\n  ) {\n    throw new Error(\n      \"ACPX recovery identity does not match the persisted runtime record\",\n    );\n  }\n}\n\nfunction parsePersistedRecord(value: unknown): AcpxIdentityRecord {\n  const record = object(value);\n  rejectUnknownKeys(record, [\n    \"schema\",\n    \"normalizedSessionId\",\n    \"acpxRecordId\",\n    \"backendSessionId\",\n    \"agentSessionId\",\n    \"profileDigest\",\n    \"workspaceDigest\",\n    \"requestedModel\",\n    \"effectiveModel\",\n    \"permissionMode\",","sourceCodeStart":172,"sourceCodeEnd":208,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/drivers/acpx/recovery-identity.ts#L172-L208","documentation":"verifyExpectedAcpxIdentity compares an expected session identity against the persisted runtime identity record (profile/workspace digests, permission mode, and provider lifetime fence candidates). Any mismatch means the runtime state on disk belongs to a different identity than the one being recovered, so it throws to prevent resuming a mismatched ACPX session. This protects against attaching a recovered session to the wrong agent configuration.","triggerScenarios":"Calling open() / acpxProviderSessionIdentity() / createAcpxIdentityRecord() to recover an existing ACPX session where any of: record agent digest differs from binding digest, record.profileDigest differs, record.workspaceDigest differs, record.permissionMode !== binding.permissionMode, or the persisted fence candidates differ from expected.providerLifetimeFenceCandidates.","commonSituations":"Upgrading or downgrading the runner (profile digest changes) and then trying to resume an old session; editing the workspace between runs; launching with a different --permission-mode flag against a persisted session; reusing a runtime directory across sessions whose fence candidates were regenerated.","solutions":["Start a fresh ACPX session instead of recovering — the persisted record belongs to a previous identity.","Re-run with the exact same agent profile, workspace contents, and permissionMode used when the record was persisted.","Delete or archive the stale runtime directory for that session id so a new identity record can be written.","If this happens after a package upgrade, expect old sessions to be unrecoverable; do not force-match the record."],"exampleFix":"// before\nawait acpx.open({ sessionId: oldId, permissionMode: \"bypassAll\" }); // record had \"default\"\n// after\nawait acpx.open({ sessionId: oldId, permissionMode: \"default\" }); // match persisted record\n// or: start a new session entirely","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await acpx.open({ sessionId, ...expected });\n} catch (e) {\n  if (e.message === \"ACPX recovery identity does not match the persisted runtime record\") {\n    // fall back to a brand-new session; the old record is for a different identity\n    await acpx.open({ sessionId: newSessionId(), ...currentConfig });\n  } else throw e;\n}","preventionTips":["Keep the same permissionMode and workspace contents across attempts to recover a session.","Treat runner upgrades as invalidating persisted sessions.","Clean up stale runtime directories when identity inputs change."],"tags":["recovery","identity","state-mismatch","acpx"],"backgroundTag":"invalid-state-transition","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}