mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 18:19:05 +02:00
fix(recovery): show progress once an explicit native retry starts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
2321ddd573
commit
9d4cee911d
6 files changed
+33
-6
No files matched your search
@@ -1476,4 +1476,7 @@ not require or synthesize a legacy `scheduledRetryReason`.
|
||||
The card describes recovery of the existing run. It does not describe a fresh
|
||||
owner disposition turn. This also covers native bootstrap/session recovery,
|
||||
which shares the same resume policy. Inbox rows, source cards, and blocker chips
|
||||
use the same activity projection and keep board-owned repairs actionable.
|
||||
use the same activity projection. Board-owned repairs stay actionable until a
|
||||
verified native retry actually starts. Once an explicit board retry is running,
|
||||
its live activity takes precedence over the prior owner and exhausted budget.
|
||||
Resolved and cancelled actions remain resolved.
|
||||
@@ -150,6 +150,18 @@ describe("IssueRecoveryActionCard", () => {
|
||||
expect(node.querySelector('[data-testid="recovery-retry-progress"]')?.getAttribute("data-recovery-lane")).toBe("native_run");
|
||||
});
|
||||
|
||||
it("describes actual export progress after the board retries an exhausted repair", () => {
|
||||
const node = render(<IssueRecoveryActionCard action={buildAction({
|
||||
kind: "active_run_watchdog", cause: "native_finalization_invalid", ownerType: "board", status: "escalated", attemptCount: 3,
|
||||
wakePolicy: { kind: "resume_native_run", runId: "native-run" },
|
||||
nativeRunActivity: { runId: "native-run", status: "running", workspaceOperationId: "export-operation" },
|
||||
})} />);
|
||||
expect(node.querySelector("section")?.getAttribute("data-recovery-state")).toBe("in_progress");
|
||||
expect(node.textContent).toContain("Paperclip is recovering the existing run");
|
||||
expect(node.querySelector('[data-testid="recovery-recovery-owner"]')?.textContent).toContain("Paperclip");
|
||||
expect(node.textContent).not.toContain("Automatic retries are finished");
|
||||
});
|
||||
|
||||
it("renders state and kind attributes with owner names and the recorded next action", () => {
|
||||
const node = render(
|
||||
<IssueRecoveryActionCard
|
||||
|
||||
@@ -904,8 +904,8 @@ function formatTimeAbsolute(value: string | Date | null | undefined): string | n
|
||||
*/
|
||||
function lineageHeadline(lineage: RecoveryRetryLineage): string {
|
||||
if (lineage.lane === "native_run") {
|
||||
if (lineage.exhausted) return "Paperclip could not recover the existing run within its retry budget. Review the recorded failure before retrying. The task stays with its original owner.";
|
||||
if (lineage.liveRunId) return "Paperclip is recovering the existing run. The task stays with its original owner.";
|
||||
if (lineage.exhausted) return "Paperclip could not recover the existing run within its retry budget. Review the recorded failure before retrying. The task stays with its original owner.";
|
||||
if (lineage.retryExpired) return "The retry for the existing run came due and did not start. Review the recorded failure and retry when ready. The task stays with its original owner.";
|
||||
if (lineage.nextRetryAt) return "Paperclip has scheduled another attempt to resume the existing run. The task stays with its original owner.";
|
||||
return "The existing run still needs recovery. Review the recorded failure before retrying. The task stays with its original owner.";
|
||||
@@ -1028,7 +1028,7 @@ export function IssueRecoveryActionCard({
|
||||
// the budget ran out or the scheduled attempt simply never fired.
|
||||
const wakeSummary = lineage?.retryExpired
|
||||
? "The scheduled retry did not run — a retry or a decision is needed"
|
||||
: lineage?.exhausted && lineage.lane !== "board"
|
||||
: lineage?.exhausted && !lineage.liveRunId && lineage.lane !== "board"
|
||||
? "Automatic retries are finished — a decision is needed"
|
||||
: readWakePolicySummary(action);
|
||||
const evidenceSummary = pickEvidenceSummary(action);
|
||||
@@ -1184,7 +1184,7 @@ export function IssueRecoveryActionCard({
|
||||
className="inline-flex flex-wrap items-center gap-1.5"
|
||||
data-testid="recovery-recovery-owner"
|
||||
>
|
||||
{lineage.lane === "native_run" && action.ownerType !== "board" ? (
|
||||
{lineage.lane === "native_run" && (action.ownerType !== "board" || Boolean(lineage.liveRunId)) ? (
|
||||
<>
|
||||
<span className="font-medium">Paperclip</span>
|
||||
<span className="text-muted-foreground">recovers the existing run</span>
|
||||
|
||||
@@ -171,6 +171,13 @@ describe("deriveRecoveryDisplayState", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["active", "escalated"] as const)("shows a board-admitted export as progress despite prior %s repair ownership", (status) => {
|
||||
expect(deriveRecoveryDisplayState({
|
||||
...action, status, ownerType: "board", attemptCount: 3,
|
||||
nativeRunActivity: { runId: "finalizing-run", status: "running", workspaceOperationId: "export-operation" },
|
||||
}, { now })).toBe("in_progress");
|
||||
});
|
||||
|
||||
it("keeps exhausted retries and board-owned failures actionable", () => {
|
||||
const wakePolicy = { ...action.wakePolicy, notBefore: "2026-09-27T02:01:00.000Z" };
|
||||
expect(deriveRecoveryDisplayState({ ...action, wakePolicy, attemptCount: 3 }, { now })).toBe("needed");
|
||||
|
||||
@@ -71,7 +71,6 @@ export function deriveRecoveryDisplayState(
|
||||
context?: RecoveryLivenessContext,
|
||||
): RecoveryDisplayState {
|
||||
if (action.status === "resolved") return "resolved";
|
||||
if (action.status === "escalated") return "escalated";
|
||||
if (action.status === "cancelled") return "resolved";
|
||||
// A bounded retry lineage still holding a durable path is work the server will do on its
|
||||
// own. Shouting "recovery needed" over it would ask a human to fix something nobody has to
|
||||
@@ -86,6 +85,11 @@ export function deriveRecoveryDisplayState(
|
||||
timeoutAt: action.timeoutAt,
|
||||
nativeRunActivity: action.nativeRunActivity,
|
||||
}, context);
|
||||
// An explicit board retry may retain its old owner/budget while the exact
|
||||
// native run is already making progress. Actual activity wins over that
|
||||
// historical repair state, but a merely scheduled board retry does not.
|
||||
if (lineage?.lane === "native_run" && lineage.liveRunId) return "in_progress";
|
||||
if (action.status === "escalated") return "escalated";
|
||||
if (action.kind === "active_run_watchdog") {
|
||||
// Native finalization shares the watchdog kind, but resumes a failed
|
||||
// coordinator rather than observing a live agent turn. Preserve board
|
||||
|
||||
@@ -198,7 +198,8 @@ export function readRecoveryRetryLineage(
|
||||
sourceAttempt: nativeResume ? null : lane === "source_owner" ? attempt : evidenceSourceAttempt,
|
||||
sourceMaxAttempts: nativeResume ? null : lane === "source_owner" ? maxAttempts : evidenceSourceMaxAttempts,
|
||||
hasDurablePath:
|
||||
!exhausted && (liveRunId !== null || (nextRetryAt !== null && !retryExpired)),
|
||||
(nativeResume && liveRunId !== null) ||
|
||||
(!exhausted && (liveRunId !== null || (nextRetryAt !== null && !retryExpired))),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user