Files
PaperClipAI/server
DottaandClaude a1ec05a1a4 fix(native): read settled harness state again after a dropped sandbox connection (#20690)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native Runner keeps one provider conversation across the per-turn
runs of a task. After each turn, the controller reads the settled Runner
and provider state out of the sandbox and saves it as the harness
backup.
> - A per-turn sandbox is released after the turn. The harness backup is
then the only copy of the provider session.
> - On staging, one dropped sandbox connection (`write ECONNRESET`)
during that read lost the backup. The run itself succeeded, because its
result was already accepted.
> - The next run of the task could not resume the conversation: `PRP
provider resume state is unavailable`. When the task was waiting for a
reply, the run stopped at "Recovery needed" with no Retry. A person had
to send a message to start a new conversation.
> - This pull request reads the settled state again when the sandbox
connection drops during that read.
> - The benefit is that one network reset no longer loses a task's
conversation.

## Linked Issues or Issue Description

Refs #20627 (warm backup restore when the source sandbox is idle) and
#20683 (relaunch of a settled runner). This PR changes a different step:
the backup capture at the end of a turn.

**What happened?**

A per-turn OpenCode task ran on a remote Daytona sandbox with reuse
disabled.

1. Run A accepted its result. The Runner suspended.
2. The controller read the settled state out of the sandbox. The read
failed with `remote checkpoint failed: write ECONNRESET`.
`harness_state.backup.persist` and `session.checkpoint.persist` failed.
3. Run A still finished as `succeeded`, because the result was durable.
The sandbox was released. No harness backup of turn A existed.
4. Run B started in a new sandbox. It failed in 0.2 s with
`native_session_recovery_failed: Error: PRP provider resume state is
unavailable`. When the prior turn yielded for a reply, the checkpoint
policy does not allow a replacement session, so the task showed
"Recovery needed" with only inspect actions.

On staging this happened on 4 tasks in 48 hours. In all 3 predecessor
runs that I could inspect, the backup capture failed. 2 of them show the
`ECONNRESET` in the run log. No deploy happened between run A and run B
in one case, so a deploy is not the cause.

**Expected behavior**

A transient connection drop while reading settled state does not lose
the provider conversation. The read is repeated, and the backup is
saved.

**Steps to reproduce**

1. Run the first durable turn of a remote OpenCode session.
2. Make the sandbox `syncOut` of the backup directories fail once with
`ECONNRESET`.
3. Call the settled checkpoint. On `master` it throws `write
ECONNRESET`, and no backup is saved.

The new test `uses the bundled Linux image authority for remote
opencode-new-durable-reset without controller overrides` runs these
steps.

**Paperclip version or commit**

`master` at `7435abcc63`.

## What Changed

- The settled-state capture (inspect plus `syncOut` of each persistence
directory into a pending directory) is now one step that can repeat. The
step writes nothing durable, so it is safe to repeat.
- A dropped, reset or refused sandbox connection repeats the step, at
most 3 attempts, with 1 s and 2 s delays. Each repeat clears the pending
directory and inspects the live state again, so the copy always matches
the inspected identity.
- Any other failure fails at once, as before: a state mismatch, a
missing directory, an unsafe archive, or a TLS failure.
- The durable backup upload and the local publish run only after a
complete capture. They did not change.
- `isTransientRemoteCheckpointReadFailure` classifies the error with the
existing `classifyRemoteConnectionError`, plus a message match for SDK
errors that lose their `code`.
- The first completeness inspection and its visibility-window re-reads
use the same bounded retry (`retryTransientRemoteCheckpointRead`). A
dropped provider-state read keeps its `provider_state_unreadable`
refusal for every other caller, but the checkpoint retry looks through
it to the dropped read. Undici `fetch failed` and `no response returned`
also count as drops.

## Verification

- `pnpm --filter @paperclipai/server exec vitest run
src/services/native-runtime/native-session-executor.test.ts`: 1343
passed.
- Red on `master`: the new `opencode-new-durable-reset` case fails with
`Error: write ECONNRESET`. Green with this change.
- The `isTransientRemoteCheckpointReadFailure` test checks that state
mismatches, unsafe archives and TLS failures do not repeat.
- `pnpm --filter @paperclipai/server typecheck` passes.

## Risks

- Low risk. The normal path has no added wait. A repeat adds at most 3
s, and only after a connection failure.
- A repeated read inspects the live state again. If the state changed
between attempts, the existing identity checks still refuse it.
- A connection that stays down for all 3 attempts fails the same way as
before.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## 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 <noreply@anthropic.com>
2026-11-10 20:46:29 -06:00
..