diff --git a/ui/src/pages/IssueDetail.test.tsx b/ui/src/pages/IssueDetail.test.tsx
index 22412b2f1b8..f4fac0ee087 100644
--- a/ui/src/pages/IssueDetail.test.tsx
+++ b/ui/src/pages/IssueDetail.test.tsx
@@ -1502,6 +1502,49 @@ describe("IssueDetail", () => {
await flushReact();
expect(mockIssuesApi.update).toHaveBeenLastCalledWith("PAP-1", { status: "cancelled" });
+ expect(mockHeartbeatsApi.cancel).toHaveBeenCalledTimes(2);
+ expect(mockHeartbeatsApi.cancel.mock.invocationCallOrder[1])
+ .toBeLessThan(mockIssuesApi.update.mock.invocationCallOrder[1]);
+ });
+
+ it("reports partial success when run finalization stops the run but task status update fails", async () => {
+ mockIssuesApi.get.mockResolvedValue(createIssue({
+ status: "in_progress",
+ assigneeAgentId: "agent-1",
+ executionRunId: "run-active-1",
+ }));
+ mockIssuesApi.update.mockRejectedValue(new Error("Status write failed"));
+ mockHeartbeatsApi.cancel.mockResolvedValue(undefined);
+ mockAgentsApi.list.mockResolvedValue([createAgent()]);
+ mockAuthApi.getSession.mockResolvedValue({
+ session: { userId: "user-1" },
+ user: { id: "user-1" },
+ });
+
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ await flushReact();
+
+ const stopAndDoneButton = Array.from(container.querySelectorAll("button"))
+ .find((button) => button.textContent?.trim() === "Stop and done");
+ expect(stopAndDoneButton).toBeTruthy();
+
+ await act(async () => {
+ stopAndDoneButton!.click();
+ });
+ await flushReact();
+
+ expect(mockHeartbeatsApi.cancel).toHaveBeenCalledWith("run-active-1");
+ expect(mockPushToast).toHaveBeenCalledWith(expect.objectContaining({
+ title: "Run stopped; task update failed",
+ body: "Run was stopped, but updating the task failed: Status write failed",
+ tone: "error",
+ }));
});
it("passes planning work mode to the issue chat thread", async () => {
diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx
index 69225d15b95..a286121c18c 100644
--- a/ui/src/pages/IssueDetail.tsx
+++ b/ui/src/pages/IssueDetail.tsx
@@ -166,6 +166,24 @@ import {
type IssueTreeControlMode,
} from "@paperclipai/shared";
+type StopAndFinalizeRunError = Error & {
+ runCancelledBeforeStatusUpdateFailed?: boolean;
+};
+
+function createRunCancelledStatusUpdateError(err: unknown): StopAndFinalizeRunError {
+ const message = err instanceof Error
+ ? `Run was stopped, but updating the task failed: ${err.message}`
+ : "Run was stopped, but updating the task failed. Retry the task status update.";
+ const error = new Error(message) as StopAndFinalizeRunError;
+ error.runCancelledBeforeStatusUpdateFailed = true;
+ return error;
+}
+
+function didRunCancelBeforeStatusUpdateFail(err: unknown): err is StopAndFinalizeRunError {
+ return err instanceof Error &&
+ (err as StopAndFinalizeRunError).runCancelledBeforeStatusUpdateFailed === true;
+}
+
type CommentReassignment = IssueCommentReassignment;
type ActionableIssueThreadInteraction =
| SuggestTasksInteraction
@@ -1978,7 +1996,11 @@ export function IssueDetail() {
const stopAndFinalizeRun = useMutation({
mutationFn: async ({ runId, status }: { runId: string; status: "cancelled" | "done" }) => {
await heartbeatsApi.cancel(runId);
- return issuesApi.update(issueId!, { status });
+ try {
+ return await issuesApi.update(issueId!, { status });
+ } catch (err) {
+ throw createRunCancelledStatusUpdateError(err);
+ }
},
onSuccess: ({ comment: _comment, ...nextIssue }, { status }) => {
const issueRefs = new Set([issueId!, nextIssue.id]);
@@ -1993,15 +2015,18 @@ export function IssueDetail() {
});
},
onError: (err, { status }) => {
+ const runWasStopped = didRunCancelBeforeStatusUpdateFail(err);
pushToast({
- title: status === "done" ? "Stop and done failed" : "Stop and cancel failed",
+ title: runWasStopped
+ ? "Run stopped; task update failed"
+ : status === "done" ? "Stop and done failed" : "Stop and cancel failed",
body: err instanceof Error ? err.message : "Unable to stop the run and update the task",
tone: "error",
});
},
- onSettled: () => {
+ onSettled: (_data, err) => {
queryClient.invalidateQueries({ queryKey: queryKeys.issues.detail(issueId!) });
- invalidateIssueRunState();
+ if (err) invalidateIssueRunState();
if (selectedCompanyId) {
queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(selectedCompanyId) });
}
@@ -3136,7 +3161,7 @@ export function IssueDetail() {
stopAndFinalizeRun.variables?.status === "cancelled",
disabled: stopAndFinalizeRun.isPending,
onSelect: (runId) =>
- stopAndFinalizeRun.mutateAsync({ runId, status: "cancelled" }).then(() => undefined),
+ stopAndFinalizeRun.mutateAsync({ runId, status: "cancelled" }).then(() => undefined, () => undefined),
},
{
id: "done",
@@ -3147,7 +3172,7 @@ export function IssueDetail() {
stopAndFinalizeRun.variables?.status === "done",
disabled: stopAndFinalizeRun.isPending,
onSelect: (runId) =>
- stopAndFinalizeRun.mutateAsync({ runId, status: "done" }).then(() => undefined),
+ stopAndFinalizeRun.mutateAsync({ runId, status: "done" }).then(() => undefined, () => undefined),
},
], [
stopAndFinalizeRun.isPending,