Files
PaperClipAI/doc
DottaandPaperclip 254b44f72b fix(remote-workspaces): recover retired roots that cannot be renamed (#19674)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Remote workspaces must retain saved files across process and sandbox
replacement.
> - Restore uses identity receipts to distinguish a retired owned root
from an unrelated directory.
> - Some overlay filesystems reject a directory rename with EXDEV even
within one mount.
> - That error currently prevents a valid saved workspace from being
restored.
> - This pull request replaces only a verified retired root in place
when that rename fails.
> - Runtime state stays in place. Each restore holds a root-specific OS
lock.
> - A durable workspace epoch fence rejects delayed older commands. A
pending marker keeps interrupted cleanup recoverable.

## Linked Issues or Issue Description

**What happened?**

Workspace restore fails with `remote_workspace_restore_failed:
stage=publish code=EXDEV syscall=rename` when the retired directory
comes from a lower overlay layer. Retry repeats the same rename.

**Expected behavior**

Restore should publish the verified saved workspace and preserve runtime
state. An interrupted restore should remain safe to retry.

**Steps to reproduce**

1. Create an owned retired root with a valid identity receipt, runtime
state, and stale files.
2. Make directory rename return EXDEV for the existing lower-layer root.
3. Restore a saved workspace through `restoreRemoteWorkspaceDirectory`.
4. Repeat with an interruption before the new restore receipt is
published.

**Paperclip version or commit**

The regression fails before this change on base
`a2aa69bc6230c8b81de3588d7d5571b38db4dd04`.

**Deployment mode**

Remote sandbox with an overlay filesystem. Local regression: macOS
arm64, Node 26.4.0, source checkout.

Refs #19231. Refs #19251. Those changes establish the retained-root
restore path. This fix handles its EXDEV publication failure.

## What Changed

- Keep the EXDEV fallback limited to a verified retired workspace root.
Preserve runtime state and the existing ownership checks.
- Hold a private SQLite OS lock across receipt checks, file replacement,
cleanup, and receipt publication. Keep the lock file outside the
replaced root. Process exit releases ownership; a lost reply does not.
- Pass the existing workspace authority epoch from the real restore
caller. Write and sync a separate epoch fence before root changes.
Reject lower epochs, conflicting identities within one epoch, and
missing epochs on fenced roots. The fence survives SQLite rollback after
process death.
- Publish a pending marker without a completed identity until stale
preparation-receipt removal and directory-mode cleanup finish. A retry
can finish that cleanup. A higher authorized epoch can then restore its
selected save.
- Preserve later file and permission edits when adopting a completed
restore. Refuse unsafe lock files and malformed authority records.
Document compatibility and the stop-evidence boundary.

## Verification

- RED before the original fix: real restore commands failed with EXDEV.
The test preload changes only the kernel rename result.
- RED before the review fixes: a live command with an unconfirmed
timed-out reply overwrote edits made after retry. Tests also reproduced
incomplete cleanup after receipt publication.
- RED before the authority fence: a delayed older command replaced a
newer completed restore. It also passed after the newer command was
killed before root mutation. The final test uses real separate Node
processes, OS lock contention, SIGKILL, and a durable on-disk fence.
- Final local checks: 142 transport filesystem tests, 16 real PostgreSQL
restore-caller cases, and all seven server typecheck layers. The caller
cases verify the published receipt uses the current workspace epoch.
- Late review RED: an actual separate SQLite owner made the real
PostgreSQL restore caller mark an expired lock wait non-retryable.
Corrupt fence and receipt bytes also produced a raw syntax error without
the required refusal reason. Both regressions now pass. Only the test
clock advances the lock deadline; actual OS lock contention and the
production caller run unchanged.
- Lock contention stays retryable only under the existing same clean
writer, epoch, and snapshot proof. Four negative cases change those
fields and remain non-retryable. After the owner stops, normal restore
returns the useful saved files. Corrupt authority records stay untouched
and non-retryable.
- Safety cases cover same and different authorized identities,
process-death lock release, an unrelated root making progress, pending
cleanup interruption, malformed or missing fences, invalid input, and
unsafe lock parents/files.
- The first final transport attempt passed 139 cases and failed only
because the restricted shell could not open a Unix socket (`listen
EPERM`). The full suite was then run with the required local socket
permission. No production change was made for that setup failure.
- Reproduce the filesystem checks from `server/`:
`../node_modules/.bin/vitest run --config vitest.config.ts
src/services/remote-workspace-transport.test.ts`. Run the caller file
with `-t "restores a newer save over this workspace|preserves the
published workspace path"` and the repository test PostgreSQL setup. Run
`node scripts/typecheck-layers.mjs` from `server/` for all seven layers.
- Agent preflight, module boundary audit, and `git diff --check` passed.
No added code comments or private issue references occur in the patch.
- Local EXDEV proof uses macOS arm64 and Node 26.4.0. It does not claim
a new Linux overlay-kernel run or hosted recovery. Final source:
[`8092cc6b2ec448ff372337b1e7a43854eae7bb08`](https://github.com/paperclipai/paperclip/commit/8092cc6b2ec448ff372337b1e7a43854eae7bb08).
[Exact-head
CI](https://github.com/paperclipai/paperclip/actions/runs/42321313715)
completed with 59 successful checks, three conditional skips, and one
neutral check. Greptile gave this exact head 5/5. The independent review
passed. No review threads remain open.

## Risks

The EXDEV path changes files in place. It must finish before execution
admission. Participating restore commands use a private permanent SQLite
lock file and a separately synced epoch fence. The qualified sandbox
Node runtime must support `node:sqlite`. Lock contention has a
120-second limit and does not clear live ownership.

A legacy command that never acquired this lock remains outside its
protection. Hosted recovery must confirm that command stopped; a timeout
is insufficient. The new fence does not backfill that evidence. A
conflicting identity in the same epoch or a malformed fence stays
blocked. A higher epoch can recover an interrupted pending restore only
with the existing retired-root replacement authority. No controller
database schema, execution permission, or provider-state guard changes.

## Model Used

Anthropic Claude Opus 5.5 (`claude-opus-5-5`) authored the recovered
patch. OpenAI Codex, GPT-6 family, recovered, reproduced, repaired the
review findings, and verified the change with filesystem tests, shell
tools, and code execution. The Codex exact serving model ID and
context-window size are not exposed in this session. No value is
inferred for them.

## 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 19:12:21 -06:00
..
…
…
…
…
…