paperclipai/paperclip · warning · QueuedCommentMutationError

queued_comment_revision_conflict

queued_comment_revision_conflict

Error message

The queued messages changed in another session

What it means

QueuedCommentMutationError with code queued_comment_revision_conflict. Thrown by requireMutationTarget when the locked queue snapshot's queueId does not match the requested queueId, or when the queue revision supplied by the caller differs from the current queue revision. The revision acts as an optimistic-concurrency token: if it is stale, another session already mutated the queue, so this mutation is rejected to avoid clobbering those changes.

Solutions

  1. Re-fetch the current queue snapshot to obtain the fresh revision, reapply the intended change on the new state, and retry the mutation
  2. Ensure the client always reads the revision from the most recent GET of the queue rather than caching it across mutations
  3. Serialize mutations for one queue through a single session or refresh the queue view after every successful mutation
  4. Check whether the intended change is still meaningful on the refreshed queue; skip the retry if another session already made the change

Example fix

// before
await editQueuedComment({ issue, actor, queueId, revision: cachedRevision, commentId, body });

// after
const queue = await getQueuedComments({ issue, actor });
if (queue.revision !== cachedRevision) {
  // refresh UI state and reapply intent against queue.revision
}
await editQueuedComment({ issue, actor, queueId, revision: queue.revision, commentId, body });
Defensive patterns

Strategy: retry

Validate before calling

const queue = await getQueuedComments({ issue, actor });
if (queue.revision !== revision || queue.queueId !== queueId) {
  throw new Error('stale revision: refresh queue before mutating');
}

Type guard

function isFreshQueue(queue: { queueId: string; revision: string }, queueId: string, revision: string): boolean {
  return queue.queueId === queueId && queue.revision === revision;
}

Try / catch

try {
  await mutateQueuedComments(input);
} catch (e) {
  if (e?.code === 'queued_comment_revision_conflict') {
    const fresh = await getQueuedComments({ issue, actor });
    return retryMutationWithRevision(fresh.revision);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling editQueuedComment, reorderQueuedComments, or discardQueuedComment (via the create* factories) with a revision value captured before another session edited, reordered, added, or discarded a queued message on the same queue; also passing a queueId that no longer matches the queue resolved for the issue.

Common situations: Two browser tabs or two operators viewing the wake queue at the same time; a stale UI page that fetched the revision minutes ago; an API client caching the queue snapshot across multiple mutations; a retry of a request whose first attempt actually succeeded and bumped the revision.

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@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/3a00b02013b5f1bb. Report an issue: GitHub.

Appendix: source

Thrown at server/src/modules/wake-queue/application/queued-comment-use-cases.ts:47

    super(message);
    this.name = "QueuedCommentMutationError";
  }
}

/** The route maps this onto the `forbidden(...)` HTTP error it threw before this move, using `message` unchanged. */
export class QueuedCommentMutationForbiddenError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "QueuedCommentMutationForbiddenError";
  }
}

function requireMutationTarget(queue: QueuedCommentQueueSnapshot, queueId: string, revision: string): void {
  if (queue.queueId !== queueId) {
    throw new QueuedCommentMutationError("queued_comment_stale_queue", "The queued message targets a stale queue");
  }
  if (queue.revision !== revision) {
    throw new QueuedCommentMutationError("queued_comment_revision_conflict", "The queued messages changed in another session");
  }
}

async function updateQueueRunCommentIdsGuarded(
  tx: QueuedCommentQueueTransaction,
  input: { queueRun: QueuedCommentRunRow | null; ids: string[]; updatedAt: Date },
): Promise<QueuedCommentRunRow | null> {
  if (!input.queueRun) return null;
  const updated = await tx.updateQueueRunCommentIds({
    queueRunId: input.queueRun.id,
    contextSnapshot: input.queueRun.contextSnapshot,
    ids: input.ids,
    updatedAt: input.updatedAt,
  });
  if (!updated) {
    throw new QueuedCommentMutationError("queued_comment_already_dispatching", "The queued message is already being dispatched");
  }
  return updated;

View on GitHub (pinned to 3f1d897a7c)