mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-12 06:10:44 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - A follow-up run on a remote sandbox inherits the previous run's session checkpoint at admission, rebound to the new run's identity > - A remote runner receives work only through the controller's Postgres checkpoint, which the controller writes before every command. A run with no checkpoint rows, no runner events and no recorded runner process never sent its runner anything > - Restart recovery still read the rebound admission checkpoint's provider session id as provider evidence, and classified such a run `reattach_remote_runner`. The executor then found the root still owned by the previous run and failed with `native_remote_recovery_lease_mismatch: stage=runner_verification field=identity_run_id` > - This pull request classifies that run as an incomplete bootstrap, and lets the executor start it from the previous owner's settled root > - The benefit is that a follow-up interrupted before its runner ever got a command restarts on its own ## Linked Issues or Issue Description Refs #20137 **What happened?** Daytona per-turn restart-chaos M1 rerun w-20261109T013733-s9501 (master8e3b64a257, image 34bc12bf), t02 (codex M1 parent): 1. Wake run `a867cb18` was admitted at 01:48:31 with a rebound checkpoint inherited from run `dfeddafa`: identity of the new run, `cursor: null`, no terminal turns, no active turn. 2. It recorded `native.process_start_requested` at 01:48:36, then a K4 hold froze the control plane. 3. At 01:53:11 recovery classified it `reattach_remote_runner` (`hasCheckpoint: true`, `hasProviderEvidence: true`, from the rebound checkpoint), although the run had no `native_runner_checkpoints` rows, no runner-originated events and no recorded process identity. 4. The executor found the session root still owned by `dfeddafa` and failed with `native_remote_recovery_lease_mismatch: stage=runner_verification field=identity_run_id`. The task needed a Retry. **Expected behavior** A remote run whose runner provably never received a command restarts its bootstrap. The executor treats the previous owner's root as it would for any follow-up start. **Steps to reproduce** 1. Claim a remote sandbox follow-up that recorded `native.process_start_requested` but has no controller checkpoint rows, no runner events and no `native.process_identity_recorded`, with only the admission-rebound session checkpoint. 2. Restart. Before this change, recovery claims `reattach_remote_runner`, and the executor fails with `identity_run_id`. **Paperclip version or commit**8e3b64a257(wave). Branch rebased on current master. ## What Changed - `native-restart-recovery.ts`: the new `remoteRunnerNeverReceivedCommands` condition. - It applies to a remote sandbox run (not a plugin task) with a prepared remote workspace when all of these hold: - no durable controller record, and no staged attachment; - only the admission-rebound checkpoint (or none), and no provider process events; - no runner-originated events, and no `native.process_identity_recorded`; - no run process fields; - the same claimed and dead previous-controller conditions as `remoteRunnerNeverLaunched`. - It yields `bootstrap_incomplete` with the reason `remote_runner_never_received_commands`, and does not count the rebound checkpoint as this run's checkpoint. - `native-session-executor.ts`: - a `bootstrap_incomplete` claim requires the incomplete-bootstrap proof only when the root carries this run's own identity; - a root still owned by the previous run takes the normal previous-owner path, and may retire a finished previous owner, as any follow-up start does. - Policy change, called out for review: a claimed remote run that recorded only `native.process_start_requested` was held as `ambiguous_remote_recovery_evidence`. Under the conditions above it now bootstraps again. - The controller writes every command to the Postgres checkpoint before sending it, so with no checkpoint rows no command reached any runner, and no provider work can exist. - A runner process that was spawned but never commanded is stopped before the new bootstrap by the existing stale-runner stop. - A recorded process identity, an adapter invocation, a runner event, process fields or a provider session still hold the run. - Tests: - `native-runner-restart-recovery.integration.test.ts`: the classification for a follow-up with an inherited checkpoint and for a first run without one, plus a held case when a runner process identity was recorded. The old held expectation for "a runner process start was requested" moved here; - `native-recovered-owner-followup.test.ts`: the executor admitting a `bootstrap_incomplete` follow-up from the previous owner's settled root. ## Verification - Red/green: - On master, the classification test claims `reattach_remote_runner`, as in production. With the change it claims `bootstrap_incomplete` with `remote_runner_never_received_commands`. - A recorded runner process identity keeps the run off this path. - The executor test is admitted with the change. On master it fails. - `vitest run` over the restart-recovery, bootstrap, recovered-owner and failed-workspace-recovery test files and `native-session-executor.test.ts`: 9 files, all passing after the policy test moved; `native-runner-restart-recovery.integration.test.ts` alone: 144 passed (14 skipped). - `PAPERCLIP_TYPECHECK_LOW_MEMORY=1 pnpm typecheck` (`server`): passed. ## Risks - Low risk: - The new path requires positive proof that nothing reached a runner: no controller record, no runner events and no recorded process. Any of these keeps the existing classification. - In the executor, a root that carries this run's identity still needs the full bootstrap proof. - A root that carries the previous run's identity goes through the same previous-owner verification as any follow-up start. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with tool use (shell, file edits, test 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>