{"record":{"id":"a9f3d3ac355b5ed7","repo":"paperclipai/paperclip","slug":"only-the-queued-message-author-can-discard-it","errorCode":null,"errorMessage":"Only the queued message author can discard it","messagePattern":"Only the queued message author can discard it","errorType":"exception","errorClass":"QueuedCommentMutationForbiddenError","httpStatus":null,"severity":"error","filePath":"server/src/modules/wake-queue/application/queued-comment-use-cases.ts","lineNumber":267,"sourceCode":"      { issue: input.issue, actor: input.actor, queueId: input.queueId },\n      async (locked, tx) => {\n        if (input.revision !== undefined) {\n          requireMutationTarget(locked.queue, input.queueId, input.revision);\n        }\n\n        const entry = locked.queue.entries.find((candidate) => candidate.comment.id === input.commentId);\n        if (!entry) {\n          throw new QueuedCommentMutationError(\"queued_comment_not_pending\", \"The queued message is no longer pending\");\n        }\n        const owns = decideQueuedCommentActorOwnsEntry({\n          actorType: input.actor.actorType,\n          actorId: input.actor.actorId,\n          actorAgentId: input.actor.agentId,\n          authorAgentId: entry.comment.authorAgentId,\n          authorUserId: entry.comment.authorUserId,\n        });\n        if (!owns) {\n          throw new QueuedCommentMutationForbiddenError(\"Only the queued message author can discard it\");\n        }\n\n        const deleted = await tx.deleteComment({ issueId: input.issue.id, commentId: input.commentId });\n        if (!deleted) {\n          throw new QueuedCommentMutationError(\"queued_comment_not_pending\", \"The queued message is no longer pending\");\n        }\n        await tx.deleteCommentReferenceSource(input.commentId);\n        await tx.syncCommentExternalObjectsSafely(input.commentId);\n\n        const remainingIds = locked.queue.entries.map((candidate) => candidate.comment.id).filter((id) => id !== input.commentId);\n        const queueBecomesEmpty = remainingIds.length === 0;\n\n        let cancelledRun: { id: string } | null = null;\n        let nextWake = locked.wake;\n        let nextQueueRun = locked.queueRun;\n\n        if (queueBecomesEmpty) {\n          await tx.cancelWake({","sourceCodeStart":249,"sourceCodeEnd":285,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/modules/wake-queue/application/queued-comment-use-cases.ts#L249-L285","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nfor (const entry of queue.entries) await api.discardQueuedComment({ issueId, commentId: entry.comment.id }); // 403 on others' entries\n// after\nfor (const entry of queue.entries) {\n  if (entry.canDiscard) await api.discardQueuedComment({ issueId, commentId: entry.comment.id });\n}","handlingStrategy":"validation","validationCode":"function canDiscard(actor, entry) {\n  if (actor.actorType === \"agent\") return entry.comment.authorAgentId === actor.agentId;\n  return entry.comment.authorUserId === actor.userId;\n}","typeGuard":"function isOwnQueuedEntry(entry) {\n  return typeof entry === \"object\" && entry !== null && \"canDiscard\" in entry && entry.canDiscard === true;\n}","tryCatchPattern":"try {\n  await discardQueuedComment(input);\n} catch (e) {\n  if (e.name === \"QueuedCommentMutationForbiddenError\" || /author can discard/.test(e.message)) {\n    notify(\"Only the author can discard this queued message\");\n  } else throw e;\n}","preventionTips":["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."],"tags":["permissions","authorization","forbidden","ownership"],"backgroundTag":"permission-denied","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}