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

  1. Refetch the queued-comment snapshot and, if it is dispatched/discard, surface 'message already sent' to the user instead of retrying
  2. Handle code queued_comment_not_pending in the mutation caller as an expected conflict and no-op/reconcile the UI
  3. Re-run the operation only after confirming via the snapshot that the comment returned to pending state (which normally does not happen)
  4. 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

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


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)