Files
PaperClipAI/server/src/services/native-runtime
DottaandClaude Opus 5.5 eab672e10a fix(workspaces): name the failing remote copy-back step without private output (#19152)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - A native run in a Daytona sandbox saves its workspace after the
provider turn. The save is called copy-back.
> - Copy-back fails on staging after successful provider turns. Issues
become blocked with `retry_exhausted`.
> - The recorded cause is only `remote_workspace_operation_failed` or
`workspace_sync_out_failed`. Operators cannot see which step failed or
why.
> - The remote command wrapper drops every unrecognized sandbox failure
to the bare code. The finalizer retry keeps only the disk-full cause.
> - This pull request records a bounded, redacted cause: the operation,
its exit code or signal, and the failing capture stage with structured
error fields.
> - The benefit is that the next copy-back failure shows its first
cause, and sandbox output still cannot leak paths or credentials.

## Linked Issues or Issue Description

**What happened?**
Native runs on a Daytona sandbox finish their provider turn and accept
their result. Then workspace copy-back fails. The first
`workspace_finalize` operation records only
`remote_workspace_operation_failed`. Each retry records only
`workspace_sync_out_failed`. The run has no error text. After the
retries stop, the issue is blocked.

**Steps to reproduce**
1. Run a native task in a Daytona sandbox.
2. Make the sandbox capture step fail with an error that is not
disk-full or a known archive validation error. For example, make Git
exit with 128.
3. Read the run's workspace operations.

**Expected behavior**
The failed operation names the step that failed and a redacted cause.

**Actual behavior**
The failed operation shows only `remote_workspace_operation_failed` and
then `workspace_sync_out_failed`.

Refs #19050, #19059, #19061, #19063. These are related Daytona and
finalizer repairs. This PR does not duplicate them.

## What Changed

- `remote-workspace-snapshot-archive.ts`: add
`remoteWorkspaceFailureDetail`. It is self-contained and runs in the
sandbox. It reports only fixed keys (`code`, `exit`, `signal`,
`syscall`, `killed`, `program=git`, `name`, `reason`). Each value is at
most 64 `[A-Za-z0-9_]` characters and comes from structured error
properties. It never reports message text, command lines, or paths. The
archive child now writes `remote_workspace_archive_failed: <detail>` for
an unclassified capture error.
- `remote-workspace-transport.ts`: the helper-install and capture
scripts write `remote_workspace_capture_failed:
stage=helper|archive|publish <detail>`. The server accepts this text
only when it matches a strict grammar. Unrecognized failures now throw
`RemoteWorkspaceOperationError` with the message
`remote_workspace_operation_failed: operation=<op> exit=<n>[
signal=<SIG>][; <validated cause>]`. Existing exact diagnostics (Git
operation in progress, full disk, archive validation) do not change.
- `native-workspace-finalizer.ts`: a retried remote save records the
redacted cause in the operation stderr and in
`metadata.workspaceSync.cause`. Other error text still stays off the
record.
- Tests: new red/green cases. The old privacy tests now assert the
anchored redacted form, which still excludes any synthetic private text.

## Verification

- Red, before the fix (only the error class existed). 8 tests failed on
behavior:
- The real Git failure produced `Received:
"remote_workspace_operation_failed"`.
  - The signal-terminated command produced the same bare message.
  - The forged-cause cases produced the same bare message.
  - The finalizer retry case lost the cause.
- Green, after the fix:
`npx vitest run src/services/remote-workspace-transport.test.ts
src/services/native-runtime/native-workspace-finalizer-failure.test.ts
src/services/remote-workspace-snapshot-archive.test.ts` reported 126
passed.
- The new capture test runs the real helper scripts with a controlled
`git` that exits 128 and prints a synthetic private path. The error is
exactly `remote_workspace_operation_failed: operation=capture exit=1;
remote_workspace_capture_failed: stage=archive exit=128 program=git
helper_exit=1`. The private path is absent. The workspace files are
unchanged. The test passes with split output and with one-shot output.
- Forged causes are dropped. These cases are tested: a path in a value,
an extra line, an unknown key, and a prefixed text.
- Wider sweep: I ran all 63 server test files that touch remote
workspaces, copy-back, or finalization. 6381 tests passed. 2 tests
failed in `chat-channels.integration.test.ts` and
`git-workspace-expiration.test.ts`. These 2 tests also fail with this
PR's three source files reverted to `master`. This PR does not cause
them.
- Typecheck: `tsc` reports no errors in the changed files. Other errors
are local, from unbuilt workspace packages.
- Not done: I did not run a live Daytona copy-back. This change adds
diagnostics. It does not repair the underlying failure. The staging
failures can be diagnosed after this PR is deployed.

## Risks

- The error message for an unrecognized remote failure changes from the
exact bare code to the same code plus validated fields. No production
code compares the exact text. Only tests did, and I updated them.
- Privacy: the sandbox detail contains only structured error properties.
The server re-validates them with an anchored grammar with a length
limit and a field limit. A path, a message text, or an unknown key fails
the grammar, and the server drops the whole cause.
- Retry and classification policy do not change.
`classifyNativeWorkspaceFailure` does not read the new text.
- This PR does not address two items: the final snapshot operation stays
`running` after retries are exhausted, and no disk-retention cause was
proven for the ACPX disk-full report.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, browser read-only GETs on staging, code
execution).

## 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-04 21:47:29 -06:00
..
…