{"record":{"id":"d142f8a562ad8865","repo":"paperclipai/paperclip","slug":"queued-comment-already-dispatching","errorCode":"queued_comment_already_dispatching","errorMessage":"The queued message is already being dispatched","messagePattern":"The queued message is already being dispatched","errorType":"error_code","errorClass":"QueuedCommentMutationError","httpStatus":null,"severity":"warning","filePath":"server/src/modules/wake-queue/adapters/queued-comment-postgres.ts","lineNumber":245,"sourceCode":"          )\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)\n            .where(\n              and(\n                eq(heartbeatRuns.id, wakeRow.runId!),","sourceCodeStart":227,"sourceCodeEnd":263,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/modules/wake-queue/adapters/queued-comment-postgres.ts#L227-L263","documentation":"Inside withLockedQueue, the wake-row lookup classified the queued comment as already_dispatching: a dispatch of this comment is in flight. The mutation (edit/reorder/discard) is refused with QueuedCommentMutationError code queued_comment_already_dispatching to prevent changing a message while it is being sent.","triggerScenarios":"A mutation path (withLockedQueue) observes decideQueuedCommentWakeLookup return kind \"already_dispatching\" — i.e., the wake row/queue run for the comment is mid-dispatch when the lock is acquired.","commonSituations":"User tries to edit or discard a queued message at the exact moment the heartbeat wakes and starts dispatching it; a slow agent turn keeps the comment in dispatching state while the user retries the UI action.","solutions":["Wait for dispatch to complete (poll the comment status) and then operate on the resulting state (e.g., follow-up message instead of edit)","Treat code queued_comment_already_dispatching in the UI as 'message is being sent' and disable the action","Retry the mutation after the dispatch finishes only if the comment returns to a mutable state","Use a follow-up queued comment instead of mutating one already in flight"],"exampleFix":"// before\nawait editQueuedComment(id, rev, text); // throws while dispatching\n// after\ntry { await editQueuedComment(id, rev, text); }\ncatch (e) { if (e.code === \"queued_comment_already_dispatching\") await queueFollowUp(text); else throw e; }","handlingStrategy":"try-catch","validationCode":"const snap = await getQueuedCommentSnapshot(commentId);\nif (snap.status === \"dispatching\") { console.log(\"dispatch in flight — mutate later\"); return; }","typeGuard":"function isMutable(snap: { status: string }): boolean { return snap.status === \"pending\"; }","tryCatchPattern":"try { await editQueuedComment(...); }\ncatch (e) {\n  if (e instanceof QueuedCommentMutationError && e.code === \"queued_comment_already_dispatching\") {\n    await pollUntilDispatched(commentId);\n    await queueFollowUp(editedText); // post-dispatch alternative\n  } else throw e;\n}","preventionTips":["Show a 'sending...' state in the UI and block edits during dispatch","Prefer follow-up messages over mutating in-flight ones","Poll comment status before offering mutation actions","Keep heartbeat wake timing in mind when scripting mutations"],"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"}