paperclipai/paperclip · error · QueuedCommentMutationForbiddenError
Only the queued message author can discard it
Error message
Only the queued message author can discard it
What it means
QueuedCommentMutationForbiddenError thrown by createDiscardQueuedComment when decideQueuedCommentActorOwnsEntry determines the acting user/agent did not author the queued comment. Only the author may discard their own queued message; this maps onto the route's HTTP 403 forbidden response with the message unchanged.
Solutions
- Have the actual author (same actorType/actorId/agentId) perform the discard, or log in as that user.
- If operators must be able to remove others' queued messages, extend decideQueuedCommentActorOwnsEntry/route policy to allow board-operator overrides rather than working around the 403.
- Check ownership client-side (compare actor to entry.comment.authorUserId/authorAgentId) and hide/disable the discard control when not the author.
- Verify the right credentials are being sent — a shared API key or wrong user token can make the request look like a non-author.
Example fix
// before
for (const entry of queue.entries) await api.discardQueuedComment({ issueId, commentId: entry.comment.id }); // 403 on others' entries
// after
for (const entry of queue.entries) {
if (entry.canDiscard) await api.discardQueuedComment({ issueId, commentId: entry.comment.id });
} Defensive patterns
Strategy: validation
Validate before calling
function canDiscard(actor, entry) {
if (actor.actorType === "agent") return entry.comment.authorAgentId === actor.agentId;
return entry.comment.authorUserId === actor.userId;
} Type guard
function isOwnQueuedEntry(entry) {
return typeof entry === "object" && entry !== null && "canDiscard" in entry && entry.canDiscard === true;
} Try / catch
try {
await discardQueuedComment(input);
} catch (e) {
if (e.name === "QueuedCommentMutationForbiddenError" || /author can discard/.test(e.message)) {
notify("Only the author can discard this queued message");
} else throw e;
} Prevention
- Check entry.canDiscard (or compare actor to authorUserId/authorAgentId) before rendering the discard control.
- Ensure the correct user token / agent API key is used; mismatched credentials change who the actor appears to be.
- For operator removal of others' messages, request an explicit policy change rather than circumventing the check.
- Never iterate-and-discard all queue entries blindly; filter to entries the current actor authored.
When it happens
Trigger: Calling discardQueuedComment (authenticated as actor X) on a queued comment whose authorAgentId/authorUserId belong to actor Y. Common with board operators trying to delete another agent's queued message, or an agent API key acting on a comment queued by a different agent.
Common situations: An operator wants to clear a colleague agent's queued message and hits 403. An agent automation iterates all queued comments on an issue and discards them, but only owns some. A user session and an agent key both point at the same issue but different authorship.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- Discord command registration authorization denied
- grant_audience_denied
- grant_owner_membership_inactive
- Only the queued message author can edit it
- railway_api_authorization_required
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/a9f3d3ac355b5ed7.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/modules/wake-queue/application/queued-comment-use-cases.ts:267
{ issue: input.issue, actor: input.actor, queueId: input.queueId },
async (locked, tx) => {
if (input.revision !== undefined) {
requireMutationTarget(locked.queue, input.queueId, input.revision);
}
const entry = locked.queue.entries.find((candidate) => candidate.comment.id === input.commentId);
if (!entry) {
throw new QueuedCommentMutationError("queued_comment_not_pending", "The queued message is no longer pending");
}
const owns = decideQueuedCommentActorOwnsEntry({
actorType: input.actor.actorType,
actorId: input.actor.actorId,
actorAgentId: input.actor.agentId,
authorAgentId: entry.comment.authorAgentId,
authorUserId: entry.comment.authorUserId,
});
if (!owns) {
throw new QueuedCommentMutationForbiddenError("Only the queued message author can discard it");
}
const deleted = await tx.deleteComment({ issueId: input.issue.id, commentId: input.commentId });
if (!deleted) {
throw new QueuedCommentMutationError("queued_comment_not_pending", "The queued message is no longer pending");
}
await tx.deleteCommentReferenceSource(input.commentId);
await tx.syncCommentExternalObjectsSafely(input.commentId);
const remainingIds = locked.queue.entries.map((candidate) => candidate.comment.id).filter((id) => id !== input.commentId);
const queueBecomesEmpty = remainingIds.length === 0;
let cancelledRun: { id: string } | null = null;
let nextWake = locked.wake;
let nextQueueRun = locked.queueRun;
if (queueBecomesEmpty) {
await tx.cancelWake({View on GitHub (pinned to 3f1d897a7c)