fix(interactions): refuse to resolve a card whose issue is already closed - #11071
Open
juancarlosrial76-code wants to merge 1 commit into
Conversation
…osed `queueResolvedInteractionContinuationWakeup` drops the continuation wakeup when the issue is `done`/`cancelled`. The `accept`, `reject`, `respond` and `verdicts` routes never looked at the issue status, so a resolution there ran all the way through: the card flipped to `accepted`/`rejected`/`answered`, `logActivity` recorded it, the route answered 200 — and nobody was woken. The card reads as answerable in the UI, someone answers it in good faith, and nothing happens. No error, no hint. Closing an issue expires its pending cards (paperclipai#10251), so in the normal case the `status !== "pending"` check already refuses these. What it does not cover is the residue: rows that were filed before that expiry shipped are still `pending` on issues that closed months ago. On the instance this was found on, 154 such cards sit on 145 closed issues, 146 of them carrying `continuationPolicy: wake_assignee` — every one of them a trap that looks answerable and is not. Three of them ask the reader in their prompt to reply. So the guard is a second line rather than the first: it reads the issue status after the pending check and answers 409 with the closed status in the error details, which is the honest outcome for an answer that cannot travel. An already-resolved card still reports "Interaction has already been resolved" — that message is more specific, and the ordering keeps this change to the one case that is currently silent. `withdraw` and `cancel` are deliberately left alone. Retiring a stale card on a closed issue is exactly the cleanup those routes exist for, and blocking them would strand the residue this guard makes visible. The status is read from the database inside the resolution path rather than taken from the caller's issue object, so the plugin host's accept/reject path is covered by the same check and not just the HTTP routes. Co-Authored-By: Paperclip <noreply@paperclip.ing> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
juancarlosrial76-code
requested review from
cryppadotta,
devinfoley and
nickyleach
as code owners
August 8, 2026 00:09
Contributor
Greptile SummaryThis PR prevents pending interactions from being resolved after their issue has entered a terminal state, avoiding successful-looking answers that cannot resume work.
Confidence Score: 5/5The PR appears safe to merge, with no concrete changed-code defect identified. The new company-scoped terminal-status check covers all intended resolution entry points, preserves cleanup operations, and is supported by focused regression tests and synchronized API documentation.
|
| Filename | Overview |
|---|---|
| server/src/services/issue-thread-interactions.ts | Adds the terminal-issue guard to every user-facing interaction-resolution path while preserving intentional administrative cleanup paths. |
| server/src/services/issue-thread-interactions.test.ts | Extends the database test double and verifies refusal, non-mutation, error ordering, and permitted withdrawal behavior. |
| skills/paperclip/references/api-reference.md | Documents the closed-issue 409 response and the continued availability of withdraw and cancel operations. |
Reviews (1): Last reviewed commit: "fix(interactions): refuse to resolve a c..." | Re-trigger Greptile
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Thinking Path
Linked Issues or Issue Description
Refs #10251 (the auto-expire-on-close hook this backstops), #10920 (open — backfills
the pre-#10251 residue this guard makes visible). No public GitHub issue tracks the
resolution-side hole; describing it inline per the bug template:
What happened?
POST /api/issues/:id/interactions/:interactionId/accept(andreject,respond,verdicts) on an interaction that is stillpendingwhile its issue isdoneorcancelledreturns 200, sets the card toaccepted/rejected/answeredand writesthe activity entry — but
queueResolvedInteractionContinuationWakeupreturns early onisClosedIssueStatus(issue.status), so no agent is woken. The answer is recorded andhas no effect.
Expected behavior
An answer that cannot resume anything should not report success. Either the card is
unanswerable, or answering it does something.
Steps to reproduce
request_confirmationon it withcontinuationPolicy: "wake_assignee".status: "cancelled"by a path that predates or bypasses thefeat(interactions): add interaction withdrawal and terminal-issue expiry #10251 close hook — or simply hold any interaction filed before feat(interactions): add interaction withdrawal and terminal-issue expiry #10251 landed,
which is still
pendingon an issue that closed earlier.POST .../accept. Response is 200, the card showsaccepted, no wakeup is queuedand no error is surfaced anywhere.
Paperclip version
Reproduced against
@paperclipai/server2026.609.0 (dist/routes/issues.js, thebundle the live process loads); the code path is unchanged on current
master.Deployment mode
Self-hosted, local instance.
What Changed
assertIssueOpenForInteractionResolution()inserver/src/services/issue-thread-interactions.tsreads the issue's status from thedatabase and throws 409 (
{ issueId, issueStatus }in the error details) when it isdone/cancelled.getPendingInteractionForResolution(covers
acceptInteractionandrejectInteraction, and through them thesuggest-tasks and confirmation branches),
answerQuestions, andsubmitItemVerdicts(inside its existing transaction, on
tx).status !== "pending"check, so an already-resolved cardkeeps its more specific "Interaction has already been resolved" message and this
change only affects the case that is currently silent.
withdrawInteractionandcancelQuestionsare deliberately untouched: retiring astale card on a closed issue is the cleanup those exist for.
object, so the plugin host's accept/reject path gets the same guard, not just the
HTTP routes.
skills/paperclip/references/api-reference.md: documents the 409 next to theexisting
issue_closedoutcome, including thatwithdraw/cancelstay available.Verification
Red before the fix: with the service change reverted and the new tests in place,
the four refusal tests fail (
4 failed | 8 passed) — they are measuring the guard,not the harness.
New tests in
server/src/services/issue-thread-interactions.test.ts:doneissue and on acancelledissue → 409, and the card must stillbe
pendingafterwards (the silent failure is precisely a card that moves whilenothing is woken, so "does not throw" alone would not catch a regression)
respond(answerQuestions) on a closed issue → 409, no card updateexpiredcard on a closed issue still reports "Interaction has alreadybeen resolved", pinning the ordering of the two conflicts
The test double's
select()grew table awareness for theissuestable, opt-in via anew
issueRowsargument, so every existing call-ordered expectation in that file isunchanged.
Risks
Low, and deliberately narrow.
only fires for a card that is
pendingon adone/cancelledissue — a state thatpost-feat(interactions): add interaction withdrawal and terminal-issue expiry #10251 issues do not reach through the close path, and whose 200 was never
effective. No caller loses a working outcome; a silent no-op becomes a visible one.
SELECTper resolution, primary-key lookup onissues, on a path thatalready runs several statements.
calling in, so a missing row means an inconsistency, not a closed issue; refusing
there would turn a read anomaly into a blocked answer.
card update still lands on the close hook's expiry, and the existing
WHERE status = 'pending'guard on the update turns that into the same 409. Thepre-check is aimed at rows that are already pending on an already closed issue.
Model Used
Claude Opus 5 (
claude-opus-5[1m], 1M context), extended thinking, with tool use(repository search, local vitest/tsc runs against a throwaway worktree of this base).
Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template#NNN/github.com/paperclipai/paperclipURLs)docs/...,fix/...) and contains no internal Paperclip ticket id or instance-derived detailsSUCCESS, 1SKIPPED(Storybook visual regression; no UI files in this PR), 0 failures on9ec5c99d29ec5c99d2, confidence 5/5, no inline comments raised