paperclipai/paperclip · warning · QueuedCommentMutationError
queued_comment_already_dispatching
queued_comment_already_dispatching
Error message
The queued message is already being dispatched
What it means
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.
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
Example fix
// before
await editQueuedComment(id, rev, text); // throws while dispatching
// after
try { await editQueuedComment(id, rev, text); }
catch (e) { if (e.code === "queued_comment_already_dispatching") await queueFollowUp(text); else throw e; } Defensive patterns
Strategy: try-catch
Validate before calling
const snap = await getQueuedCommentSnapshot(commentId);
if (snap.status === "dispatching") { console.log("dispatch in flight — mutate later"); return; } Type guard
function isMutable(snap: { status: string }): boolean { return snap.status === "pending"; } Try / catch
try { await editQueuedComment(...); }
catch (e) {
if (e instanceof QueuedCommentMutationError && e.code === "queued_comment_already_dispatching") {
await pollUntilDispatched(commentId);
await queueFollowUp(editedText); // post-dispatch alternative
} else throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
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_not_pending
- 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/d142f8a562ad8865.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/modules/wake-queue/adapters/queued-comment-postgres.ts:245
)
.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)
.where(
and(
eq(heartbeatRuns.id, wakeRow.runId!),View on GitHub (pinned to 3f1d897a7c)