mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-13 07:12:00 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Native runs on a remote sandbox (Daytona) start a detached `paperclip-runnerd`. After a recoverable failure, the server resumes the same run and reattaches to that Runner > - #20923 stopped a failed close from force-stopping an unreachable remote Runner. It asks for retention only when the session was not cancelled > - The runtime cancels the session itself when a turn start or the event stream fails ("Native session event consumption failed."). That cleanup sets the same flag as a user Stop > - So after a failed turn start, the close still sent `SIGKILL` to the Runner, and the same-run reattachment failed with `runner_remote_process_identity_unavailable`. The task waited for Retry > - This pull request separates failure cleanup from Stop. Failure cleanup still revokes the turn's output, but the close keeps the Runner for the same-run reattachment > - The benefit is that these runs continue without Retry. Stop and protocol integrity faults still stop the Runner ## Linked Issues or Issue Description Refs #20923, #20853 **What happened?** After the staging redeploy, five remote native runs failed with error `runner_remote_process_identity_unavailable` and error code `native_continuation_requires_reconciliation`. In four of them, the first attempt started after the deploy. Its `turn/start` failed with `runnerd did not report the provider turn identity`. The run log then showed `runner did not prove durable suspension before checkpoint` and `runner shutdown failed: Error: Durable recovery runner timed out.` The diagnostic `remote runner left alive unsettled for same-run reattachment` from #20923 did not appear. One second later the Runner exited (`runner_remote_process_exited lifecycle=ready`). The same-run reattachment 20–40 s later found `process_verification` / `process_not_running` with `runnerSettled: false`. **What did you expect to happen?** The failed close keeps the Runner alive, as #20923 intended. The same-run reattachment adopts it, and the run continues without Retry. **Steps to reproduce** 1. Start a remote native Codex run. 2. Make `turn/start` fail (for example the Runner does not report the provider turn identity in time), or let the event stream fail or time out. 3. The runtime cancels the session as cleanup. `HarnessNativeSession` treats that as a Stop, so `close` asks for `retainUnsettledRemoteRunner: false`, and the transport kills the Runner. **Root cause** `packages/paperclip-runner/src/native-session-runtime.ts` `attemptOptionalSessionCancellation` calls `session.cancel`. `HarnessNativeSession.cancel` (`packages/paperclip-runner/src/backends/harness-driver-backend.ts`) sets `#explicitlyCancelled`, and `close` used `retainUnsettledRemoteRunner: !this.#explicitlyCancelled`. The fifth run is different. A server shutdown detached its controller and left the Runner alive. Then the sandbox environment was acquired fresh (45.9 s) before the reattachment, and the Runner was gone in the middle of a turn. This pull request does not change that path. See Risks. ## What Changed - `NativeSession.cancel` accepts an optional `failureCleanup` field. - `attemptOptionalSessionCancellation` passes `failureCleanup: true`, except for a `NativeSessionProtocolIntegrityError`. - `HarnessNativeSession` tracks a Stop separately from output revocation. `cancel` with `failureCleanup` revokes the turn's output only. `cancel` without it, and `revokeTurnPublication`, record a Stop. `close` asks for retention when no Stop was recorded and no protocol integrity fault occurred. - Tests: three runtime cases with the real Codex backend (turn start fails, event stream times out, Stop then failure) and a new backend case. - Docs: `doc/execution-semantics.md` describes the rule. ## Verification - Red, on `master` with only the new tests: `cd packages/paperclip-runner && npx vitest run src/native-session-runtime.test.ts -t "remote Runner retention after a failed run"` fails 2 of 3 (`expected false to be true`): the close asked to kill the Runner after the turn start failed and after the event stream timed out. The Stop case passed. - Green with the fix: the same command passes 3 of 3. `src/backends/harness-driver-backend.test.ts -t "retain an unsettled"` passes 3 of 3, including the failure-cleanup case. That case also checks that the turn's result stays unpublished. - `npx vitest run src/native-session-runtime.test.ts src/backends src/drivers/codex src/live/runnerd-codex-transport-remote-retention.test.ts --maxWorkers=4`: 31 files, 814 tests pass. `codex-protocol-integrity.test.ts` confirms that an integrity fault still stops the Runner. - `npx tsc --noEmit -p packages/paperclip-runner/tsconfig.json` is clean. - `pnpm verify` exits 2 because `doc/execution-semantics.md` is a global CI input. CI supplies that coverage. - Not run: a live Daytona run. The change is in the controller library that runs on the server, so it needs no new Runner image. ## Risks - Low to medium. After a failed turn start or event stream, an unreachable remote Runner now stays alive instead of being killed. This is the state #20923 already accepted for other failed closes. The session stays quarantined, and the same-run reattachment or a Stop cleanup owns the Runner. - Stop is unchanged. A server Stop calls `cancel` without `failureCleanup`, so the close still stops the Runner, also when the stopped run later fails. A governed wait uses `revokeTurnPublication`, which also records a Stop. - A protocol integrity fault needs an operator and is not resumed on the same run, so it still stops the Runner. - Previous-release story: this change adds and changes no persisted state. A Runner that an older release already killed keeps its current named refusal and Retry. A deploy during a run changes nothing, because the decision is made in the closing process. - Not covered: a Runner that dies with its sandbox while the controller is detached for a restart, in the middle of a turn. Its journal is a settled `ready` cut with an active turn. #20853 resumes only idle `ready` cuts. This needs a separate fix. - No wait is added before `turn.start`. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with tool use (shell, file edits, test runs, read-only staging API reads). ## 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>