diff --git a/doc/execution-semantics.md b/doc/execution-semantics.md
index 19e215df864..50a386a359d 100644
--- a/doc/execution-semantics.md
+++ b/doc/execution-semantics.md
@@ -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.
diff --git a/ui/src/components/IssueRecoveryActionCard.test.tsx b/ui/src/components/IssueRecoveryActionCard.test.tsx
index 077b1707b8e..75e4b3330cf 100644
--- a/ui/src/components/IssueRecoveryActionCard.test.tsx
+++ b/ui/src/components/IssueRecoveryActionCard.test.tsx
@@ -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();
+ 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(
- {lineage.lane === "native_run" && action.ownerType !== "board" ? (
+ {lineage.lane === "native_run" && (action.ownerType !== "board" || Boolean(lineage.liveRunId)) ? (
<>
Paperclip
recovers the existing run
diff --git a/ui/src/lib/recovery-display.test.ts b/ui/src/lib/recovery-display.test.ts
index 5c71fed94e2..8c6275febf6 100644
--- a/ui/src/lib/recovery-display.test.ts
+++ b/ui/src/lib/recovery-display.test.ts
@@ -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");
diff --git a/ui/src/lib/recovery-display.ts b/ui/src/lib/recovery-display.ts
index dff2fc5d67b..83458672d9c 100644
--- a/ui/src/lib/recovery-display.ts
+++ b/ui/src/lib/recovery-display.ts
@@ -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
diff --git a/ui/src/lib/recovery-lineage.ts b/ui/src/lib/recovery-lineage.ts
index c172ef4db08..1bd8e61edf3 100644
--- a/ui/src/lib/recovery-lineage.ts
+++ b/ui/src/lib/recovery-lineage.ts
@@ -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))),
};
}