diff --git a/server/src/__tests__/issue-update-comment-wakeup-routes.test.ts b/server/src/__tests__/issue-update-comment-wakeup-routes.test.ts index e7a608cb2e..ce168995c1 100644 --- a/server/src/__tests__/issue-update-comment-wakeup-routes.test.ts +++ b/server/src/__tests__/issue-update-comment-wakeup-routes.test.ts @@ -477,6 +477,41 @@ describe("issue update comment wakeups", () => { ); }); + it("does not wake the assignee when a closure comment marks the issue done", async () => { + const existing = makeIssue({ + assigneeAgentId: ASSIGNEE_AGENT_ID, + assigneeUserId: null, + status: "in_progress", + }); + const updated = { + ...existing, + status: "done", + completedAt: new Date("2026-06-26T16:30:00.000Z"), + }; + mockIssueService.getById.mockResolvedValue(existing); + mockIssueService.update.mockResolvedValue(updated); + mockIssueService.addComment.mockResolvedValue({ + id: "comment-close-1", + issueId: existing.id, + companyId: existing.companyId, + body: "Closing this out.", + }); + + const res = await request(await createApp()) + .patch(`/api/issues/${existing.id}`) + .send({ + status: "done", + comment: "Closing this out.", + }); + + expect(res.status).toBe(200); + await new Promise((resolve) => setImmediate(resolve)); + const issueCommentedWakeCalls = mockHeartbeatService.wakeup.mock.calls.filter( + ([, wakeup]: [string, { reason?: string }]) => wakeup?.reason === "issue_commented", + ); + expect(issueCommentedWakeCalls).toEqual([]); + }); + it("wakes the assignee on top-level board issue comments", async () => { const existing = makeIssue({ assigneeAgentId: ASSIGNEE_AGENT_ID, diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index f2030255d5..694e70490d 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -9972,7 +9972,10 @@ export function issueRoutes( const assigneeId = issue.assigneeAgentId; const actorIsAgent = actor.actorType === "agent"; const selfComment = actorIsAgent && actor.actorId === assigneeId; - const skipAssigneeCommentWake = selfComment || isClosed; + // Re-derive closed-ness from the post-update issue so a status change + // like in_progress -> done with a closure comment does not enqueue a + // stale issue_commented wake for an already-completed issue. + const skipAssigneeCommentWake = selfComment || isClosedIssueStatus(issue.status); if (assigneeId && !assigneeChanged && (reopened || !skipAssigneeCommentWake)) { addWakeup(assigneeId, {