fix(recovery): continue Retry after completed file preservation (#18845)

Reuse completed file preservation for task Retry and show bounded admission explanations, including newer saved messages and identifier routes.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-11-03 16:01:27 -06:00
1 parent 519b345d69
commit a80eec1164
13 files changed
+276 -18

No files matched your search

@@ -70,6 +70,17 @@ describe("ordinary task interruption", () => {
await render({ ...failed, workspaceFailure, state: "working" });
expect(container.textContent).toBe("");
});
it("shows why an accepted Retry is waiting and keeps another attempt available", async () => {
const retryWait = { reason: "remote_cleanup", message: "Retry is waiting for the previous environment to stop. Your message is saved." };
await render({ ...failed, retryWait });
expect(container.textContent).toContain(retryWait.message);
expect(container.textContent).not.toContain("Couldn’t finish this attempt.");
await act(async () => container.querySelector<HTMLButtonElement>("button")!.click());
expect(retry).toHaveBeenCalledOnce();
await render({ ...failed, retryWait }, { canRetry: false });
expect(container.textContent).toContain(retryWait.message);
expect(container.querySelector("button")).toBeNull();
});
it("shows an unknown provider's original explanation as text and preserves read-only access", async () => {
const providerFailure = { schema: "paperclip.provider-failure.v1" as const, provider: "new-provider", phase: "startup" as const,
category: "unknown" as const, retryability: "unknown" as const, httpStatus: 429,
+2 -1
View File
@@ -25,7 +25,7 @@ export function ExecutionBlockerNotice({ presentation, canRetry, onRetry, optimi
canRetry={canRetry && presentation.canRetry} onRetry={onRetry} error={error} className={className} />;
if (error && !optimistic && !takingLonger) return <div role="alert" className={cn("flex items-center justify-between gap-2 text-sm text-destructive", className)}><span>{error.message}</span>{canRetry && <Button variant="outline" size="sm" onClick={onRetry}>Retry</Button>}</div>;
if (takingLonger) return <div role="status" className={cn("flex items-center justify-between gap-2 text-sm text-muted-foreground", className)}><span>Taking longer than usual…</span>{canRetry && <Button variant="outline" size="sm" onClick={onRetry}>Retry</Button>}</div>;
if (!optimistic && presentation?.providerFailure && ["failed", "retry_scheduled", "waiting_for_user"].includes(presentation.state)) {
if (!optimistic && !presentation?.retryWait && presentation?.providerFailure && ["failed", "retry_scheduled", "waiting_for_user"].includes(presentation.state)) {
return <ProviderFailureNotice failure={presentation.providerFailure} className={className}
actions={canRetry && presentation.canRetry ? <Button variant="outline" size="sm" onClick={onRetry}>Retry</Button> : undefined}>
{presentation.state === "retry_scheduled" && <p>Paperclip will retry automatically.</p>}
@@ -43,6 +43,7 @@ export function ExecutionBlockerNotice({ presentation, canRetry, onRetry, optimi
{presentation.state === "failed" && <AlertCircle className="size-5 shrink-0 text-destructive" aria-hidden="true" />}
<span className="min-w-0 flex-1">{presentation.state === "retry_scheduled"
? `Trying again in ${seconds > 60 ? `${Math.ceil(seconds / 60)}m` : `${seconds}s`}…`
: presentation.retryWait ? presentation.retryWait.message
: presentation.workspaceFailure
? <>Couldn’t prepare the task folders. {presentation.workspaceFailure.message}</>
: "Couldn’t finish this attempt. Your message is saved."}</span>