{"record":{"id":"3a00b02013b5f1bb","repo":"paperclipai/paperclip","slug":"queued-comment-revision-conflict","errorCode":"queued_comment_revision_conflict","errorMessage":"The queued messages changed in another session","messagePattern":"The queued messages changed in another session","errorType":"error_code","errorClass":"QueuedCommentMutationError","httpStatus":null,"severity":"warning","filePath":"server/src/modules/wake-queue/application/queued-comment-use-cases.ts","lineNumber":47,"sourceCode":"    super(message);\n    this.name = \"QueuedCommentMutationError\";\n  }\n}\n\n/** The route maps this onto the `forbidden(...)` HTTP error it threw before this move, using `message` unchanged. */\nexport class QueuedCommentMutationForbiddenError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = \"QueuedCommentMutationForbiddenError\";\n  }\n}\n\nfunction requireMutationTarget(queue: QueuedCommentQueueSnapshot, queueId: string, revision: string): void {\n  if (queue.queueId !== queueId) {\n    throw new QueuedCommentMutationError(\"queued_comment_stale_queue\", \"The queued message targets a stale queue\");\n  }\n  if (queue.revision !== revision) {\n    throw new QueuedCommentMutationError(\"queued_comment_revision_conflict\", \"The queued messages changed in another session\");\n  }\n}\n\nasync function updateQueueRunCommentIdsGuarded(\n  tx: QueuedCommentQueueTransaction,\n  input: { queueRun: QueuedCommentRunRow | null; ids: string[]; updatedAt: Date },\n): Promise<QueuedCommentRunRow | null> {\n  if (!input.queueRun) return null;\n  const updated = await tx.updateQueueRunCommentIds({\n    queueRunId: input.queueRun.id,\n    contextSnapshot: input.queueRun.contextSnapshot,\n    ids: input.ids,\n    updatedAt: input.updatedAt,\n  });\n  if (!updated) {\n    throw new QueuedCommentMutationError(\"queued_comment_already_dispatching\", \"The queued message is already being dispatched\");\n  }\n  return updated;","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/modules/wake-queue/application/queued-comment-use-cases.ts#L29-L65","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Re-fetch the current queue snapshot to obtain the fresh revision, reapply the intended change on the new state, and retry the mutation","Ensure the client always reads the revision from the most recent GET of the queue rather than caching it across mutations","Serialize mutations for one queue through a single session or refresh the queue view after every successful mutation","Check whether the intended change is still meaningful on the refreshed queue; skip the retry if another session already made the change"],"exampleFix":"// before\nawait editQueuedComment({ issue, actor, queueId, revision: cachedRevision, commentId, body });\n\n// after\nconst queue = await getQueuedComments({ issue, actor });\nif (queue.revision !== cachedRevision) {\n  // refresh UI state and reapply intent against queue.revision\n}\nawait editQueuedComment({ issue, actor, queueId, revision: queue.revision, commentId, body });","handlingStrategy":"retry","validationCode":"const queue = await getQueuedComments({ issue, actor });\nif (queue.revision !== revision || queue.queueId !== queueId) {\n  throw new Error('stale revision: refresh queue before mutating');\n}","typeGuard":"function isFreshQueue(queue: { queueId: string; revision: string }, queueId: string, revision: string): boolean {\n  return queue.queueId === queueId && queue.revision === revision;\n}","tryCatchPattern":"try {\n  await mutateQueuedComments(input);\n} catch (e) {\n  if (e?.code === 'queued_comment_revision_conflict') {\n    const fresh = await getQueuedComments({ issue, actor });\n    return retryMutationWithRevision(fresh.revision);\n  }\n  throw e;\n}","preventionTips":["Always read the revision from the latest queue GET immediately before mutating","Refresh the queue view after every successful mutation","Avoid holding queue snapshots in long-lived client state","Treat conflict as a signal to re-render, not to force-apply"],"tags":["optimistic-concurrency","conflict","state","api"],"backgroundTag":"invalid-state-transition","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}