Surface scheduled retry cancel failures

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-05-19 16:21:16 -05:00
1 parent 7593fffb30
commit 133457c2f4
2 files changed
+90 -17

No files matched your search

@@ -635,6 +635,40 @@ describe.sequential("issue comment reopen routes", () => {
));
});
it("does not move scheduled-retry issues to todo when POST comment retry cancellation fails", async () => {
const issue = {
...makeIssue("in_progress"),
executionRunId: "retry-run-1",
};
mockIssueService.getById.mockResolvedValue(issue);
mockIssueService.getCurrentScheduledRetry.mockResolvedValue({
runId: "retry-run-1",
status: "scheduled_retry",
agentId: "22222222-2222-4222-8222-222222222222",
agentName: "CodexCoder",
retryOfRunId: "source-run-1",
scheduledRetryAt: new Date("2026-05-18T14:00:00.000Z"),
scheduledRetryAttempt: 1,
scheduledRetryReason: "transient_failure",
error: null,
errorCode: null,
});
mockHeartbeatService.cancelRun.mockRejectedValue(new Error("cancel failed"));
const res = await request(await installActor(createApp()))
.post("/api/issues/11111111-1111-4111-8111-111111111111/comments")
.send({ body: "I added the missing detail; please continue." });
expect(res.status).toBe(500);
expect(mockHeartbeatService.cancelRun).toHaveBeenCalledWith("retry-run-1");
expect(mockIssueService.update).not.toHaveBeenCalled();
expect(mockIssueService.addComment).not.toHaveBeenCalled();
expect(mockLogActivity).not.toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ action: "issue.updated" }),
);
});
it("keeps ordinary in-progress POST human comments in progress when no scheduled retry exists", async () => {
const issue = makeIssue("in_progress");
mockIssueService.getById.mockResolvedValue(issue);
@@ -874,6 +908,40 @@ describe.sequential("issue comment reopen routes", () => {
));
});
it("does not move scheduled-retry issues to todo when PATCH comment retry cancellation fails", async () => {
const issue = {
...makeIssue("in_progress"),
executionRunId: "retry-run-1",
};
mockIssueService.getById.mockResolvedValue(issue);
mockIssueService.getCurrentScheduledRetry.mockResolvedValue({
runId: "retry-run-1",
status: "scheduled_retry",
agentId: "22222222-2222-4222-8222-222222222222",
agentName: "CodexCoder",
retryOfRunId: "source-run-1",
scheduledRetryAt: new Date("2026-05-18T14:00:00.000Z"),
scheduledRetryAttempt: 1,
scheduledRetryReason: "transient_failure",
error: null,
errorCode: null,
});
mockHeartbeatService.cancelRun.mockRejectedValue(new Error("cancel failed"));
const res = await request(await installActor(createApp()))
.patch("/api/issues/11111111-1111-4111-8111-111111111111")
.send({ comment: "Retry window is over; please continue." });
expect(res.status).toBe(500);
expect(mockHeartbeatService.cancelRun).toHaveBeenCalledWith("retry-run-1");
expect(mockIssueService.update).not.toHaveBeenCalled();
expect(mockIssueService.addComment).not.toHaveBeenCalled();
expect(mockLogActivity).not.toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ action: "issue.updated" }),
);
});
it("rejects non-assignee agent PATCH comments on closed issues", async () => {
mockIssueService.getById.mockResolvedValue(makeIssue("done"));
mockIssueService.addComment.mockResolvedValue({
+22 -17
View File
@@ -912,11 +912,11 @@ export function issueRoutes(
});
return cancelledRunId;
} catch (err) {
logger.warn(
logger.error(
{ err, issueId: input.issue.id, runId: scheduledRetryRunId },
"failed to cancel scheduled retry superseded by issue comment",
);
return null;
throw err;
}
}
@@ -3534,6 +3534,18 @@ export function issueRoutes(
) {
updateFields.status = "todo";
}
let cancelledScheduledRetryRunId: string | null = null;
if (
commentBody &&
shouldResumeInProgressScheduledRetry &&
updateFields.status === "todo"
) {
cancelledScheduledRetryRunId = await cancelScheduledRetrySupersededByComment({
scheduledRetryRunId: scheduledRetryForHumanComment?.runId,
issue: existing,
actor,
});
}
if (req.body.executionPolicy !== undefined) {
updateFields.executionPolicy = applyActorMonitorScheduledBy(
normalizeIssueExecutionPolicy(req.body.executionPolicy),
@@ -3792,13 +3804,6 @@ export function issueRoutes(
previous.status !== undefined &&
existing.status === "in_progress" &&
issue.status === "todo";
const cancelledScheduledRetryRunId = scheduledRetrySupersededByComment
? await cancelScheduledRetrySupersededByComment({
scheduledRetryRunId: scheduledRetryForHumanComment?.runId,
issue,
actor,
})
: null;
const statusChangedFromBlockedToTodo =
existing.status === "blocked" &&
issue.status === "todo" &&
@@ -5125,22 +5130,22 @@ export function issueRoutes(
effectiveMoveToTodoRequested &&
(isClosed || (isBlocked && !hasUnresolvedFirstClassBlockers) || shouldResumeInProgressScheduledRetry)
) {
scheduledRetrySupersededByComment = shouldResumeInProgressScheduledRetry && issue.status === "in_progress";
cancelledScheduledRetryRunId = scheduledRetrySupersededByComment
? await cancelScheduledRetrySupersededByComment({
scheduledRetryRunId: scheduledRetryForHumanComment?.runId,
issue,
actor,
})
: null;
const reopenedIssue = await svc.update(id, { status: "todo" });
if (!reopenedIssue) {
res.status(404).json({ error: "Issue not found" });
return;
}
reopened = isClosed || (isBlocked && !hasUnresolvedFirstClassBlockers);
scheduledRetrySupersededByComment = shouldResumeInProgressScheduledRetry && issue.status === "in_progress";
reopenFromStatus = reopened ? issue.status : null;
currentIssue = reopenedIssue;
cancelledScheduledRetryRunId = scheduledRetrySupersededByComment
? await cancelScheduledRetrySupersededByComment({
scheduledRetryRunId: scheduledRetryForHumanComment?.runId,
issue: currentIssue,
actor,
})
: null;
await logActivity(db, {
companyId: currentIssue.companyId,