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 1b32f8fca5b..ce92c7ec471 100644 --- a/server/src/__tests__/issue-update-comment-wakeup-routes.test.ts +++ b/server/src/__tests__/issue-update-comment-wakeup-routes.test.ts @@ -104,7 +104,7 @@ function registerModuleMocks() { })); } -async function createApp() { +async function createApp(actorOverrides: Record = {}) { const [{ errorHandler }, { issueRoutes }] = await Promise.all([ vi.importActual("../middleware/index.js"), vi.importActual("../routes/issues.js"), @@ -118,6 +118,7 @@ async function createApp() { companyIds: ["company-1"], source: "local_implicit", isInstanceAdmin: false, + ...actorOverrides, }; next(); }); @@ -252,4 +253,66 @@ describe("issue update comment wakeups", () => { }), ); }); + + it("does not self-wake on issue updates when a local implicit run comments on its own task", async () => { + const runId = "run-self-comment"; + const existing = makeIssue({ + assigneeAgentId: ASSIGNEE_AGENT_ID, + assigneeUserId: null, + status: "in_progress", + executionRunId: runId, + }); + const updated = { + ...existing, + status: "done", + executionRunId: null, + checkoutRunId: null, + executionAgentNameKey: null, + executionLockedAt: null, + }; + mockIssueService.getById.mockResolvedValue(existing); + mockIssueService.update.mockResolvedValue(updated); + mockIssueService.addComment.mockResolvedValue({ + id: "comment-3", + issueId: existing.id, + companyId: existing.companyId, + body: "wrapped", + }); + + const res = await request(await createApp({ runId })) + .patch(`/api/issues/${existing.id}`) + .send({ + status: "done", + comment: "wrapped", + }); + + expect(res.status).toBe(200); + expect(mockHeartbeatService.wakeup).not.toHaveBeenCalled(); + }); + + it("does not self-wake on direct comments when a local implicit run comments on its own task", async () => { + const runId = "run-self-comment-direct"; + const existing = makeIssue({ + assigneeAgentId: ASSIGNEE_AGENT_ID, + assigneeUserId: null, + status: "in_progress", + executionRunId: runId, + }); + mockIssueService.getById.mockResolvedValue(existing); + mockIssueService.addComment.mockResolvedValue({ + id: "comment-4", + issueId: existing.id, + companyId: existing.companyId, + body: "still working", + }); + + const res = await request(await createApp({ runId })) + .post(`/api/issues/${existing.id}/comments`) + .send({ + body: "still working", + }); + + expect(res.status).toBe(201); + expect(mockHeartbeatService.wakeup).not.toHaveBeenCalled(); + }); }); diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index 15ffd41dd45..776ddddf1e7 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -167,6 +167,22 @@ function shouldImplicitlyReopenCommentForAgent(input: { return true; } +function isSelfAuthoredAssigneeComment(input: { + actorType: "agent" | "user"; + actorId: string; + assigneeAgentId: string | null | undefined; + runId: string | null; + executionRunId: string | null | undefined; + previousExecutionRunId?: string | null | undefined; +}) { + if (typeof input.assigneeAgentId !== "string" || input.assigneeAgentId.length === 0) return false; + if (input.actorType === "agent") { + return input.actorId === input.assigneeAgentId; + } + return Boolean(input.runId) + && (input.executionRunId === input.runId || input.previousExecutionRunId === input.runId); +} + function diffExecutionParticipants( previousPolicy: NormalizedExecutionPolicy | null, nextPolicy: NormalizedExecutionPolicy | null, @@ -1982,8 +1998,14 @@ export function issueRoutes( if (commentBody && comment) { const assigneeId = issue.assigneeAgentId; - const actorIsAgent = actor.actorType === "agent"; - const selfComment = actorIsAgent && actor.actorId === assigneeId; + const selfComment = isSelfAuthoredAssigneeComment({ + actorType: actor.actorType, + actorId: actor.actorId, + assigneeAgentId: assigneeId, + runId: actor.runId, + executionRunId: issue.executionRunId, + previousExecutionRunId: existing.executionRunId, + }); const skipAssigneeCommentWake = selfComment || isClosed; if (assigneeId && !assigneeChanged && (reopened || !skipAssigneeCommentWake)) { @@ -2580,7 +2602,14 @@ export function issueRoutes( const wakeups = new Map[1]>(); const assigneeId = currentIssue.assigneeAgentId; const actorIsAgent = actor.actorType === "agent"; - const selfComment = actorIsAgent && actor.actorId === assigneeId; + const selfComment = isSelfAuthoredAssigneeComment({ + actorType: actor.actorType, + actorId: actor.actorId, + assigneeAgentId: assigneeId, + runId: actor.runId, + executionRunId: currentIssue.executionRunId, + previousExecutionRunId: issue.executionRunId, + }); const skipWake = selfComment || isClosed; if (assigneeId && (reopened || !skipWake)) { if (reopened) {