Files
PaperClipAI/server
Devin FoleyandPaperclip 7840d894fa fix: retain files after accepted remote copy-back exhausts retries (#19491)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Remote workspaces keep unsaved files bound to the run that wrote
them.
> - An accepted result can exhaust its copy-back retries before those
files are saved.
> - Failed-workspace preservation rejects every accepted result, so a
stopped writer can keep the workspace held after a newer setup attempt
fails.
> - This pull request lets the existing owner retain files from that
exact stopped remote writer.
> - File recovery can then release the hold without applying the old
result or repeating its provider work.

## Linked Issues or Issue Description

**What happened?**

A failed native remote writer had an accepted result and exhausted
copy-back retries. Its sandbox was stopped with a valid retained-source
receipt. A newer attempt failed on the writer's unsaved workspace. Task
Retry could not register the missing file-recovery obligation because
preservation returned as soon as it found the old accepted result.

**Expected behavior**

Task Retry must let the existing recovery owner save the exact retained
files. The accepted result, failed attempts and current task must stay
intact. The old provider work must not run again.

**Steps to reproduce**

1. Finish native work in a remote task workspace and accept its
completion result.
2. Exhaust copy-back retries, stop and retain that exact sandbox, and
leave the workspace dirty.
3. Let a newer setup attempt fail on the old writer's hold.
4. Use ordinary task Retry. Before this fix, the missing file obligation
is never recorded.

**Paperclip version or commit**

Reproduced against master at `51a65af9b81f3c60f8210047e9a3eeaada7bc155`
with isolated PostgreSQL and real files.

Related: #19385 preserves accepted copy-back interrupted by Stop. #19422
admits saved input after verified recovery. #19464 and #19466 repair
preparation and Retry continuation. This change covers an older remote
writer whose accepted copy-back has exhausted its retries.

## What Changed

- Admit only terminal, exhausted accepted copy-back on a failed remote
writer with a finished run and no live controller or finalizer.
- Require the exact stop-and-retain receipt, accepted completion
contract and current dirty workspace binding. Keep native-sync sources
on their existing export path.
- Use the existing file-preservation recorder and worker. Add no new
recovery worker, schema, polling or replay path.
- Cover bound and orphaned entry points, reusable leases, changed proof
and the ordinary HTTP Retry journey. Document the recovery behavior.

## Verification

- Causal baseline: the corrected bound-writer and orphaned-lease tests
both fail without the source change and pass with it.
- Assembled acceptance: authenticated HTTP Retry records the old
writer's missing obligation; the real recovery service saves its files;
concurrent continuation calls queue exactly one successor for the newer
failed setup. PostgreSQL, filesystem, tar, Git and the API/service path
are real. Provider transport is controlled, and no model turn is
dispatched.
- `pnpm check:module-boundaries`, `pnpm -r typecheck`, `pnpm build` and
`git diff --check` pass.
- Four full affected recovery suites pass: **659 tests**. Command: `pnpm
--dir server exec vitest run --config vitest.config.ts
src/services/workspace-recovery.test.ts
src/services/native-runtime/native-failed-workspace-recovery.test.ts
src/services/native-runtime/native-cancelled-workspace-preservation.test.ts
src/services/native-runtime/native-workspace-export-retry.test.ts`. This
includes the final 16 new regression, refusal and assembled journey
cases.
- `pnpm verify --explain` completed. It selected broad transitive
coverage; this is planning evidence only. Hosted CI will supply the
remaining broad coverage.
- Dependency cost: extra indexed source/result reads run only for this
exact terminal retained-result path, inside the existing bounded
transaction. Existing notifications activate the existing recovery
owner. No idle scan is added.
- Fixture failures are accounted for: the first fixture used an
active-only stop helper on a released lease. The corrected fixture
produced the causal baseline above. One negative fixture tried to remove
a contract while an accepted result referenced it and failed the
database foreign key. That impossible fixture was removed; the positive
journey passed in the same run. Neither fixture failure is reported as a
product regression or a passing full run.
- Hosted CI is fully green at the reviewed commit: 54 successful checks
and 3 intentional skips, with no incomplete or failed checks. Greptile
is 5/5 with no unresolved threads.
- Hosted rollout and a live task continuation remain pending. Local API
admission does not prove a deployed model run.

## Risks

This expands file preservation for a narrow terminal failure. Wrong
source identity could retain unrelated files; unchanged writer, epoch,
location, provider lease, stop receipt and accepted contract checks fail
closed. Active ownership, unsafe archives, rejected results and
native-sync sources stay on their existing paths. Existing recovery owns
physical source locking, copy verification and bounded retries. It
preserves unknown action outcomes and leaves the accepted result
uncommitted. There is no migration or configuration change.

## Model Used

OpenAI Codex, `gpt-6.1-sol`, xhigh reasoning effort. Context window size
is unavailable. Used repository tools, isolated tests and code
execution. No delegated agents.

## 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
- [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>
2026-11-06 08:23:36 -08:00
..