{"record":{"id":"65943630103545f7","repo":"paperclipai/paperclip","slug":"discord-correction-draft-ownership-changed","errorCode":null,"errorMessage":"Discord correction draft ownership changed","messagePattern":"Discord correction draft ownership changed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"server/src/services/chat-discord-question-forms.ts","lineNumber":224,"sourceCode":"        ...scope,\n        key: stateKey(actionId),\n        expectedVersion: prior?.version ?? null,\n        value: draft,\n        expiresAt,\n      })\n    ) {\n      return {\n        action: \"errors\",\n        errors: input.fieldErrors,\n        paperclipDiscordCorrection: {\n          version: 1,\n          actionId,\n          message: messages.join(\"\\n\").slice(0, 1500),\n        },\n      };\n    }\n  }\n  throw new Error(\"Discord correction draft ownership changed\");\n}\n\nexport async function loadDiscordQuestionFormCorrection(\n  persistence: ChatSdkStatePersistence,\n  scope: ChatSdkStateScope,\n  actionId: string,\n  owner: DiscordQuestionFormDraftOwner,\n  threadId: string,\n  now = new Date(),\n): Promise<DiscordQuestionFormCorrectionDraft | null> {\n  if (!isDiscordQuestionFormCorrectionId(actionId)) return null;\n  const prior = await persistence.read(scope, stateKey(actionId));\n  if (!prior) return null;\n  const value = prior.value;\n  if (\n    !validDraft(value) ||\n    !prior.expiresAt ||\n    prior.expiresAt.getTime() !== Date.parse(value.expiresAt)","sourceCodeStart":206,"sourceCodeEnd":242,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/chat-discord-question-forms.ts#L206-L242","documentation":"retainDiscordQuestionFormCorrection persists the draft with an optimistic compare-and-set loop (MAX_CAS_ATTEMPTS). This error is thrown after all CAS attempts fail, meaning another writer concurrently claimed or replaced the same correction state key, so this caller lost the ownership race and must not overwrite the winner.","triggerScenarios":"Two concurrent submissions (e.g. a Discord interaction retry plus the original) target the same actionId state key and each compareAndSet keeps failing because the prior value changed every attempt; a retry loop exhausts MAX_CAS_ATTEMPTS under sustained contention.","commonSituations":"Discord redelivers an interaction while the first handler is still processing; a user double-clicks 'Edit answers' producing parallel modal submissions; a background job touches the same state key.","solutions":["Surface a transient conflict to the user and have them resubmit the correction (a fresh flow will pick up current state)","Increase MAX_CAS_ATTEMPTS or add small backoff between attempts if contention is expected and benign","Add idempotency: key submissions by interaction id so duplicate deliveries short-circuit instead of racing"],"exampleFix":"// before\nthrow new Error(\"Discord correction draft ownership changed\");\n// after\ntry {\n  await retainCorrectionWithRetry(...);\n} catch (err) {\n  if (isOwnershipConflict(err)) return respondWithRetryPrompt();\n  throw err;\n}","handlingStrategy":"retry","validationCode":"// Pre-check (best-effort, racy): only proceed if state exists and looks owned by us\nconst prior = await persistence.read(scope, stateKey(expectedActionId));\nif (prior && prior.ownerId !== expectedOwnerId) return respondConflict();","typeGuard":"const isOwnershipConflict = (err) => err instanceof Error && err.message === \"Discord correction draft ownership changed\";","tryCatchPattern":"try {\n  await retainDiscordQuestionFormCorrection(...);\n} catch (err) {\n  if (isOwnershipConflict(err)) {\n    return respondWithMessage(\"This correction was already processed. Please start a new edit.\");\n  }\n  throw err;\n}","preventionTips":["Make Discord interaction handling idempotent by interaction id to eliminate duplicate races","Add small jittered backoff between CAS attempts","Never blind-overwrite correction state; always use compare-and-set"],"tags":["discord","concurrency","optimistic-locking"],"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-14T05:17:10.506Z"}