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
- 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
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
- 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
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
- CreateOS returned an unsuccessful response.
- queued_comment_order_mismatch
- replacement_required
- A different semantic result was already committed
- ACPX session is not at a safe suspension point
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)