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
- 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
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
- 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
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
- stale_revision
- Failed to start worktree port reservation lock heartbeat at
- Timed out waiting for worktree port reservation lock at ${lo
- sandbox runtime asset key collides with a reserved runtime a
- DUPLEX_CHANNEL_ROUTE_BUSY
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/65943630103545f7.
Report an issue: GitHub.