From 02898b98de8b4fcc2b2979a17a6e47238b4af5a1 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Sun, 8 Nov 2026 00:52:22 -0600 Subject: [PATCH] fix(issues): do not wake the assignee for an issue created already closed (#19961) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Creating an issue with an agent assignee queues an `issue_assigned` wake for that agent > - The create path skips the wake only for `backlog` and the onboarding first task > - An issue created already `done` or `cancelled` is a record of finished work, but it still wakes the assignee > - The update path already refuses this case ("Restoring an assignee on completed work is not a reopen request"), and create-time fast responses already skip `done` and `cancelled` > - This pull request applies the same closed-status guard to the create-time assignment wake > - The benefit is that recording finished work does not start a run, spend tokens, or queue behind the agent's current work ## Linked Issues or Issue Description **What happened** In a chat eval, setup created a historical issue with `status: "done"` assigned to an agent that was already running a task. The create queued a second run for that agent (`invocationSource: assignment`, `wakeReason: issue_assigned`, `source: issue.create`). The run waited behind the active run, and then was cancelled or failed. The eval expected no work for a finished record. **Expected behavior** Creating an issue that is already `done` or `cancelled` does not wake its assignee. This matches the update path, where an assignee change on closed work is not a reopen request. **Steps to reproduce** 1. Create an agent. 2. `POST /api/companies//issues` with `{"title":"Record","status":"done","assigneeAgentId":""}`. 3. See a new `agent_wakeup_requests` row and a queued heartbeat run for the agent. ## What Changed - `server/src/routes/issues.ts`: the create route does not call `queueIssueAssignmentWakeup` when the created issue status is `done` or `cancelled` (`isClosedIssueStatus`). - `server/src/__tests__/issue-onboarding-first-task-routes.test.ts`: a regression test for `done` and `cancelled` creates. The existing test that an ordinary assigned create still wakes is unchanged. ## Verification - `cd server && pnpm exec vitest run src/__tests__/issue-onboarding-first-task-routes.test.ts`: 11 passed. - Negative control: with the route file from `origin/master`, both new cases fail. ## Risks - Low risk. Only creates in a closed status change. A caller that relied on a create-time wake for a closed issue no longer gets one. To start work, the caller creates the issue in an open status or reopens it, which still wakes the assignee. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code execution in Claude Code. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 --- ...issue-onboarding-first-task-routes.test.ts | 19 +++++++++++++++++++ server/src/routes/issues.ts | 6 ++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/server/src/__tests__/issue-onboarding-first-task-routes.test.ts b/server/src/__tests__/issue-onboarding-first-task-routes.test.ts index 0ae5a48a5e3..87b590e517c 100644 --- a/server/src/__tests__/issue-onboarding-first-task-routes.test.ts +++ b/server/src/__tests__/issue-onboarding-first-task-routes.test.ts @@ -342,6 +342,25 @@ describeEmbeddedPostgres("issue create onboarding first-task routes", () => { expect(mockAdapterExecute).not.toHaveBeenCalled(); }); + it.each(["done", "cancelled"])("does not queue an assignment wake for an issue created already %s", async (status) => { + const companyId = await seedCompany(); + const agentId = await seedAgent(companyId); + const app = createApp(); + + await request(app) + .post(`/api/companies/${companyId}/issues`) + .send({ title: "Historical record", status, assigneeAgentId: agentId }) + .expect(201); + + await drainHeartbeatRunsToQuiescence(db, heartbeatService(db)); + const wakeups = await db + .select() + .from(agentWakeupRequests) + .where(eq(agentWakeupRequests.companyId, companyId)); + expect(wakeups).toHaveLength(0); + expect(mockAdapterExecute).not.toHaveBeenCalled(); + }); + it("still queues an assignment wake for an ordinary assigned issue", async () => { const companyId = await seedCompany(); const agentId = await seedAgent(companyId); diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index 73c7c754ca5..9d7fee46aec 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -13319,9 +13319,11 @@ export function issueRoutes( // Do not auto-wake the onboarding first task. Nothing should run and no // token should be spent until the user types: the greeting is posted above // (deterministic, no LLM) and the user's first comment wakes the assignee - // through the normal comment path. Every other create path keeps its wake. + // through the normal comment path. A task created already done or + // cancelled is a record, not work: like restoring an assignee on closed + // work, it must not wake the assignee. Every other create keeps its wake. - if (!isOnboardingFirstTask) { + if (!isOnboardingFirstTask && !isClosedIssueStatus(issue.status)) { void queueIssueAssignmentWakeup({ heartbeat, issue,