Files
PaperClipAI/server/src/services
DottaandClaude Opus 5.5 01dd6ab336 fix(recovery): show the saved-work recovery step and record why it failed (#19260)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Sometimes a failed run left files in a sandbox. Then Retry first
runs system recovery to save those files before the task continues.
> - Recovery has steps: confirm that the previous attempt stopped, save
its files, then finish.
> - The task page did not show these steps. People saw "Queued" or
"Trying again in…" with a retry time that kept moving.
> - Each unclassified failure of the stop-confirmation step was recorded
only as `provider_unavailable`, so a repeated deferral could not be
diagnosed.
> - This pull request shows the current recovery step on the task. It
also records why each failed attempt failed, in closed labels.
> - The benefit is that people can see what Retry waits for, and
operators can see why recovery deferred.

## Linked Issues or Issue Description

Refs #19249

**What happened?**
A person clicked Retry on a task whose failed run had a retained
sandbox. For about 7 minutes the task showed only "Queued", and the
retry time moved forward twice. Recovery deferred twice in the
stop-confirmation step. Each deferral was recorded only as
`provider_unavailable` with an opaque fingerprint, so the cause could
not be found.

**Expected behavior**
The task shows the recovery step that it waits for, such as "Checking
that the previous attempt stopped. Trying again in 30s…". Each failed
attempt keeps a closed-label cause, for example `sandbox_not_running` or
`transport_timeout`, with an optional errno-style code and HTTP status.

**Steps to reproduce**
1. Get a remote task run to fail after its sandbox was placed, so that
recovery owns saved work.
2. Click Retry while the sandbox provider does not respond to the stop
check.
3. The task shows "Queued" or "Trying again in…". The recovery evidence
records only `provider_unavailable`.

**Paperclip version or commit**
master at 499712c6.

**Deployment mode**
Authenticated cloud deployment (staging).

## What Changed

- `system-recovery-state.ts`: a failed attempt can keep an optional
`failure.cause` with a closed `label`, an errno-style `code` and an HTTP
`status`. The schema drops a value that does not match.
- `workspace-recovery.ts`: an unclassified failure records
`sandboxProviderFailureLabel(error)` as its cause, and the warning log
includes the label. The cause never contains provider text.
- `system-recovery.ts`: the board handoff message after exhaustion names
the last cause.
- `TaskInterruptionPresentation` has an optional `recoveryStep`:
`checking_previous_run`, `saving_previous_files` or
`finishing_recovery`. The server sets it from the recovery phase only
for a scheduled or actively running system recovery.
- `ExecutionBlockerNotice` shows the step. While recovery runs it shows
"Saving files from the previous attempt…". While a recovery retry is
scheduled it shows "Checking that the previous attempt stopped. Trying
again in 30s…". Without a recovery step, the output does not change.

## Verification

- Red, then green: the new tests in `system-recovery-state.test.ts`,
`task-interruption-presentation.test.ts` and
`ExecutionBlockerNotice.test.tsx` failed before the change and pass
after it.
- `workspace-recovery.test.ts` (380 tests) passes. Its exhausted
unknown-failure case now expects `cause: { label: "provider_error" }`.
- An ordinary scheduled retry, a working run without recovery and the
existing failed states render as before.
- Server typecheck passes. UI typecheck passes for `src`. The only UI
errors are in Storybook support files whose generated catalog is not
built locally.
- `pnpm check:token-gates` passes.

## Risks

- Low risk. All new fields are optional JSON fields. No database
migration is necessary, and old records parse as before.
- This change does not change recovery behavior, retry timing or
authority. It adds presentation and diagnostics only.
- The cause label uses the existing closed transport label set. An
unrecognized error is recorded as `provider_error`.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 10:03:25 -06:00
..
…
…
…