{"record":{"id":"2f7e427e2f8b8c0e","repo":"paperclipai/paperclip","slug":"queued-comment-not-pending","errorCode":"queued_comment_not_pending","errorMessage":"The queued message is no longer pending","messagePattern":"The queued message is no longer pending","errorType":"error_code","errorClass":"QueuedCommentMutationError","httpStatus":null,"severity":"warning","filePath":"server/src/modules/wake-queue/adapters/queued-comment-postgres.ts","lineNumber":242,"sourceCode":"              eq(agentWakeupRequests.companyId, companyId),\n              input.issue.assigneeAgentId ? eq(agentWakeupRequests.agentId, input.issue.assigneeAgentId) : undefined,\n            ),\n          )\n          .for(\"update\")\n          .limit(1)\n          .then((rows) => rows[0] ?? null);\n\n        const wakePayload = parseObject(wakeRow?.payload);\n        const lookup = decideQueuedCommentWakeLookup({\n          wakePresent: wakeRow !== null,\n          wakeIssueIdMatches: readNonEmptyString(wakePayload.issueId) === input.issue.id,\n          hasQueuedCommentIds: queuedCommentIdsFromWakePayload(wakeRow?.payload ?? null).length > 0,\n          wakeStatus: wakeRow?.status ?? null,\n          wakeHasRunId: Boolean(wakeRow?.runId),\n        });\n\n        if (lookup.kind === \"not_pending\") {\n          throw new QueuedCommentMutationError(\"queued_comment_not_pending\", \"The queued message is no longer pending\");\n        }\n        if (lookup.kind === \"already_dispatching\") {\n          throw new QueuedCommentMutationError(\"queued_comment_already_dispatching\", \"The queued message is already being dispatched\");\n        }\n\n        // Unreachable: `decideQueuedCommentWakeLookup` only returns \"deferred\" or \"check_queue_run\" when the wake row is present.\n        if (!wakeRow) throw new Error(\"wake-queue: queued-comment lookup resolved without a wake row\");\n\n        let state: \"deferred\" | \"queued\";\n        let queueRunRow: RunRow | null = null;\n\n        if (lookup.kind === \"deferred\") {\n          state = \"deferred\";\n        } else {\n          // check_queue_run: `wakeHasRunId` was true for this branch to have been reached.\n          queueRunRow = await tx\n            .select()\n            .from(heartbeatRuns)","sourceCodeStart":224,"sourceCodeEnd":260,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/modules/wake-queue/adapters/queued-comment-postgres.ts#L224-L260","documentation":"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.","triggerScenarios":"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\".","commonSituations":"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.","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"],"exampleFix":"// before\nawait discardQueuedComment(id, rev); // throws\n// after\ntry { await discardQueuedComment(id, rev); }\ncatch (e) { if (e.code === \"queued_comment_not_pending\") await refreshSnapshot(); else throw e; }","handlingStrategy":"try-catch","validationCode":"const snap = await getQueuedCommentSnapshot(commentId);\nif (snap.status !== \"pending\") {\n  console.log(\"comment no longer pending — skip mutation\");\n  return;\n}","typeGuard":"function isPending(snap: { status: string }): snap is { status: \"pending\" } { return snap.status === \"pending\"; }","tryCatchPattern":"try { await mutateQueuedComment(...); }\ncatch (e) {\n  if (e instanceof QueuedCommentMutationError && e.code === \"queued_comment_not_pending\") {\n    await refreshSnapshot(); // expected race: reconcile UI, no retry\n  } else throw e;\n}","preventionTips":["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)"],"tags":["race-condition","queue","concurrency"],"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-22T10:30:35.592Z"}