paperclipai/paperclip · warning · QueuedCommentMutationError
queued_comment_not_pending
queued_comment_not_pending
Error message
The queued message is no longer pending
What it means
In the wake-queue's locked mutation path, the queued comment lookup classified the row as not_pending: the comment is no longer in a state that can be mutated (already dispatched, discarded, or consumed) by the time the row lock was acquired. withLockedQueue throws QueuedCommentMutationError with code queued_comment_not_pending so callers can treat it as a benign race rather than a bug.
Solutions
- Refetch the queued-comment snapshot and, if it is dispatched/discard, surface 'message already sent' to the user instead of retrying
- Handle code queued_comment_not_pending in the mutation caller as an expected conflict and no-op/reconcile the UI
- Re-run the operation only after confirming via the snapshot that the comment returned to pending state (which normally does not happen)
- Reduce the race window by disabling mutation controls optimistically as soon as dispatch begins
Example fix
// before
await discardQueuedComment(id, rev); // throws
// after
try { await discardQueuedComment(id, rev); }
catch (e) { if (e.code === "queued_comment_not_pending") await refreshSnapshot(); else throw e; } Defensive patterns
Strategy: try-catch
Validate before calling
const snap = await getQueuedCommentSnapshot(commentId);
if (snap.status !== "pending") {
console.log("comment no longer pending — skip mutation");
return;
} Type guard
function isPending(snap: { status: string }): snap is { status: "pending" } { return snap.status === "pending"; } Try / catch
try { await mutateQueuedComment(...); }
catch (e) {
if (e instanceof QueuedCommentMutationError && e.code === "queued_comment_not_pending") {
await refreshSnapshot(); // expected race: reconcile UI, no retry
} else throw e;
} Prevention
- Disable edit/discard controls as soon as dispatch begins
- Treat queued_comment_not_pending as a benign race, not a bug
- Refetch the snapshot after any conflict before further actions
- Avoid duplicate submits (debounce send/discard buttons)
When it happens
Trigger: Two concurrent operations target the same queued comment (e.g. a wake dispatch and a user edit/discard); the loser, after obtaining withLockedQueue's lock, finds decideQueuedCommentWakeLookup returned kind "not_pending".
Common situations: User discards a queued message in the UI at nearly the same moment the dispatcher picks it up; double-click on a send button racing the first dispatch; stale UI snapshot retrying an action on an already-sent message.
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
- queued_comment_already_dispatching
- Bridge envelope changed while reading.
- Capability live turn admission was abandoned during teardown
- Capability live turn start completed after admission…
- CreateOS lease cleanup is already in progress.
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/2f7e427e2f8b8c0e.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/modules/wake-queue/adapters/queued-comment-postgres.ts:242
eq(agentWakeupRequests.companyId, companyId),
input.issue.assigneeAgentId ? eq(agentWakeupRequests.agentId, input.issue.assigneeAgentId) : undefined,
),
)
.for("update")
.limit(1)
.then((rows) => rows[0] ?? null);
const wakePayload = parseObject(wakeRow?.payload);
const lookup = decideQueuedCommentWakeLookup({
wakePresent: wakeRow !== null,
wakeIssueIdMatches: readNonEmptyString(wakePayload.issueId) === input.issue.id,
hasQueuedCommentIds: queuedCommentIdsFromWakePayload(wakeRow?.payload ?? null).length > 0,
wakeStatus: wakeRow?.status ?? null,
wakeHasRunId: Boolean(wakeRow?.runId),
});
if (lookup.kind === "not_pending") {
throw new QueuedCommentMutationError("queued_comment_not_pending", "The queued message is no longer pending");
}
if (lookup.kind === "already_dispatching") {
throw new QueuedCommentMutationError("queued_comment_already_dispatching", "The queued message is already being dispatched");
}
// Unreachable: `decideQueuedCommentWakeLookup` only returns "deferred" or "check_queue_run" when the wake row is present.
if (!wakeRow) throw new Error("wake-queue: queued-comment lookup resolved without a wake row");
let state: "deferred" | "queued";
let queueRunRow: RunRow | null = null;
if (lookup.kind === "deferred") {
state = "deferred";
} else {
// check_queue_run: `wakeHasRunId` was true for this branch to have been reached.
queueRunRow = await tx
.select()
.from(heartbeatRuns)View on GitHub (pinned to 3f1d897a7c)