mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 18:19:05 +02:00
fix(ui): retry the latest failed task attempt (#13765)
## Thinking Path > - Paperclip manages work done by AI agents. > - The task thread lets an operator retry a failed run. > - Legacy runs with transcript output did not get a failure marker. > - The thread could therefore offer Try again for an older failure. > - The server correctly reused that failure's existing retry, even when it had already failed. > - This change keeps the latest failure actionable and reports stopped retry responses to the operator. ## Linked Issues or Issue Description **What happened?** Try again could return success without starting work. An initial setup failure had an empty transcript. Its later retry produced output and failed. Only the initial failure had a retry marker, so the button kept requesting the initial failure's already-failed successor. **Expected behavior** Try again targets the latest failed attempt. An already-stopped retry response shows an error and refreshes the task's run state. **Steps to reproduce** 1. Start a legacy adapter task that fails before producing transcript output. 2. Retry it. Let this attempt produce output and a final failure comment before it fails. 3. Click Try again in the task thread. 4. Before this fix, the click targets the original failure and replays the stopped successor. **Paperclip version or commit** Reproduced against `1483bb8bcf`; the regression is also present on the branch base `8813a50105`. **Deployment mode** Authenticated server with a legacy adapter. The bug is in the shared task UI and retry API client. Related: #11650 adds a different recovery-notice action. This change fixes failed-run markers and retry response handling. Searches found no duplicate of this failure case. ## What Changed - Render legacy failure markers even when the run has a transcript or final comment. - Keep later cancelled automatic retries from replacing the failed run's retry action. - Reject already-stopped retry responses in the API client so existing error feedback appears. - Refresh task run queries after both successful and failed retry requests. - Add regression coverage for failed and timed-out attempts, execution gates with output, and retry response states. ## Verification - Before the fix, the new regression tests failed: two selected the original failure, and four accepted a stopped successor as success. - Targeted task-thread, retry API, marker, and issue-page tests: 279 passed. - `pnpm --filter @paperclipai/ui typecheck`: passed. - `pnpm --filter @paperclipai/ui build`: passed. - `pnpm check:token-gates`: passed. - Full UI suite: 6,529 tests passed across 626 files. - [CI run 35650385023](https://github.com/paperclipai/paperclip/actions/runs/35650385023): all 53 checks passed, including full workspace build, typecheck, unit/integration suites, and browser tests. The redundant local full-workspace test run was stopped after CI passed; it is not counted as a completed local pass. - Greptile: 5/5 on `608ee58c99`, with no review threads or unresolved comments. The branch is mergeable. - `pnpm -r typecheck` and `pnpm build` were attempted. Both stop at the Runner's Rust checks because this host has no `cargo`. The full workspace checks passed in CI. ## Risks Low risk. This changes UI presentation and response handling only. The server's exact-retry idempotency, authorization, execution ownership, and recovery gates remain in place. No schema changes or live task mutations. Existing documentation describes this retry action; the fix restores that behavior. ## Model Used OpenAI GPT-6 (Codex), with reasoning, repository tools, and code execution. ## 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 (targeted checks; full-workspace limits described above) - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes (existing behavior restored; no documentation change needed) - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
8813a50105
commit
df66219780
5 files changed
+160
-45
No files matched your search
@@ -0,0 +1,36 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { agentsApi } from "./agents";
|
||||
import { api } from "./client";
|
||||
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
describe("agentsApi.retryFailedRun", () => {
|
||||
it.each(["failed", "timed_out", "cancelled", "interrupted"])(
|
||||
"reports a replayed %s retry instead of silently succeeding",
|
||||
async (status) => {
|
||||
const post = vi.spyOn(api, "post").mockResolvedValue({ id: "previous-retry", status });
|
||||
await expect(agentsApi.retryFailedRun("agent-1", "original-failure", "company-1"))
|
||||
.rejects.toThrow("The previous retry has already stopped");
|
||||
expect(post).toHaveBeenCalledOnce();
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["queued", "running", "succeeded"])("accepts a %s successor without dispatching again", async (status) => {
|
||||
const post = vi.spyOn(api, "post").mockResolvedValue({ id: "successor", status });
|
||||
await expect(agentsApi.retryFailedRun("agent-1", "original-failure", "company-1"))
|
||||
.resolves.toEqual({ runId: "successor", issueId: null });
|
||||
expect(post).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("preserves a durable chat retry that is waiting for dispatch", async () => {
|
||||
vi.spyOn(api, "post").mockResolvedValue({ actionId: "retry-action", status: "queued", runId: null, issueId: "issue-1" });
|
||||
await expect(agentsApi.retryFailedRun("agent-1", "original-failure", "company-1"))
|
||||
.resolves.toEqual({ runId: null, issueId: "issue-1" });
|
||||
});
|
||||
|
||||
it("reports a skipped wake", async () => {
|
||||
vi.spyOn(api, "post").mockResolvedValue({ skipped: true, message: "Task execution is paused." });
|
||||
await expect(agentsApi.retryFailedRun("agent-1", "original-failure", "company-1"))
|
||||
.rejects.toThrow("Task execution is paused.");
|
||||
});
|
||||
});
|
||||
+10
-1
@@ -259,7 +259,16 @@ export const agentsApi = {
|
||||
reason: "retry_failed_run",
|
||||
failedRunId,
|
||||
});
|
||||
if ("id" in result) return { runId: result.id, issueId: null };
|
||||
if ("id" in result) {
|
||||
// Exact retries are idempotent: a repeated click can return a successor
|
||||
// that has already stopped. It does not mean a new attempt was queued.
|
||||
if (["failed", "timed_out", "cancelled", "interrupted"].includes(result.status)) {
|
||||
throw new Error(
|
||||
"The previous retry has already stopped. Refresh the task and retry its latest failed run.",
|
||||
);
|
||||
}
|
||||
return { runId: result.id, issueId: null };
|
||||
}
|
||||
if ("actionId" in result) {
|
||||
if (result.status === "failed" || result.status === "cancelled") {
|
||||
throw new Error(
|
||||
|
||||
Reference in new issue
Block a user