paperclipai/paperclip · error · Error

Discord correction draft ownership changed

Error message

Discord correction draft ownership changed

What it means

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.

Source

Thrown at server/src/services/chat-discord-question-forms.ts:224

        ...scope,
        key: stateKey(actionId),
        expectedVersion: prior?.version ?? null,
        value: draft,
        expiresAt,
      })
    ) {
      return {
        action: "errors",
        errors: input.fieldErrors,
        paperclipDiscordCorrection: {
          version: 1,
          actionId,
          message: messages.join("\n").slice(0, 1500),
        },
      };
    }
  }
  throw new Error("Discord correction draft ownership changed");
}

export async function loadDiscordQuestionFormCorrection(
  persistence: ChatSdkStatePersistence,
  scope: ChatSdkStateScope,
  actionId: string,
  owner: DiscordQuestionFormDraftOwner,
  threadId: string,
  now = new Date(),
): Promise<DiscordQuestionFormCorrectionDraft | null> {
  if (!isDiscordQuestionFormCorrectionId(actionId)) return null;
  const prior = await persistence.read(scope, stateKey(actionId));
  if (!prior) return null;
  const value = prior.value;
  if (
    !validDraft(value) ||
    !prior.expiresAt ||
    prior.expiresAt.getTime() !== Date.parse(value.expiresAt)

View on GitHub (pinned to 01ad858492)

Solutions

  1. Surface a transient conflict to the user and have them resubmit the correction (a fresh flow will pick up current state)
  2. Increase MAX_CAS_ATTEMPTS or add small backoff between attempts if contention is expected and benign
  3. Add idempotency: key submissions by interaction id so duplicate deliveries short-circuit instead of racing

Example fix

// before
throw new Error("Discord correction draft ownership changed");
// after
try {
  await retainCorrectionWithRetry(...);
} catch (err) {
  if (isOwnershipConflict(err)) return respondWithRetryPrompt();
  throw err;
}
Defensive patterns

Strategy: retry

Validate before calling

// Pre-check (best-effort, racy): only proceed if state exists and looks owned by us
const prior = await persistence.read(scope, stateKey(expectedActionId));
if (prior && prior.ownerId !== expectedOwnerId) return respondConflict();

Type guard

const isOwnershipConflict = (err) => err instanceof Error && err.message === "Discord correction draft ownership changed";

Try / catch

try {
  await retainDiscordQuestionFormCorrection(...);
} catch (err) {
  if (isOwnershipConflict(err)) {
    return respondWithMessage("This correction was already processed. Please start a new edit.");
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

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/65943630103545f7. Report an issue: GitHub.