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 warm follow-up rotates authority before it attaches. It archives the previous epoch (suspended runner state and settled controller) under `authority-epochs/`, then writes its own control plane > - #20146 taught recovery to roll back a rotation whose new control plane held only an undelivered `run.attach`, once its runner was proven dead > - A hard kill can land one step earlier, after the archive and before the new control plane exists. The root then holds only the archive, with no active control plane and no runner state. The executor read "no durable identity" and failed with `runner_state_identity_mismatch: durable_identity_unreadable` > - This pull request applies the same proven rollback to that earlier point of the rotation > - The benefit is that a hard kill anywhere in the rotation window resumes the follow-up from the previous turn ## Linked Issues or Issue Description Refs #20146 **What happened?** Local mixed restart-chaos wave w-20261109T081456-s9901 (master09e24dd12c), t15 (codex M1): - Run `4b85497f` started at 08:29:32 as a warm follow-up and requested its process, then K1 killed the server. - Recovery stopped the previous run's warm runner (pid 60249) and claimed `resume_dead_runner`. - The executor failed with `runner_state_identity_mismatch: durable_identity_unreadable`. The quarantined root held: - `authority-epochs/epoch-c2842347…` with the previous owner `dc7d447b`: runner `suspended`, every command completed; - no `control-plane/` state and no `runner/runner-state.json`, only provider state. A Retry ran fresh. Phase 3 also saw the same signature on Daytona; #20214 covers that remote shape. **Expected behavior** For a local runner proven dead (`resume_dead_runner`), a root that holds only the archive of a suspended previous owner of this session restores that epoch, and the follow-up continues from the previous turn. Without a dead-runner claim it still fails closed. **Steps to reproduce** 1. Start a warm follow-up and hard-kill the server after the rotation archives the previous epoch, before it writes the new control plane. 2. Restart. Before this change, the follow-up fails with `durable_identity_unreadable`. **Paperclip version or commit**09e24dd12c(wave). Branch rebased on current master. ## What Changed - `native-session-executor.ts`: - `undeliveredAttachRotationSource` becomes `interruptedRotationSource`, and the new `latestSuspendedArchivedEpoch` shares the epoch checks: named by its identity digest, a matching suspended runner state. - Besides #20146's undelivered-attach shape, it accepts a root with no `runner-state.json` and no active control plane (absent or empty). In that case the epoch must belong to this session (`normalizedSessionId`) and to a different run. - The rollback moves an active control plane aside only when one exists. - `native-recovered-owner-followup.test.ts`: #20146's rotation test adds the "cut off before its new control plane" shape. With a dead-runner claim it is admitted from the restored previous owner; without the claim it still fails closed. ## Verification - Red/green: on master, the new admitted case fails with exactly `durable_identity_unreadable`. With the change it is admitted. The fail-closed cases are unchanged. - `vitest run` of the recovered-owner, restart-recovery and bootstrap test files and `native-session-executor.test.ts`: 8 files, 1627 tests passed (14 skipped). - `PAPERCLIP_TYPECHECK_LOW_MEMORY=1 pnpm typecheck` (`server`): passed. ## Risks - Low risk: - The rollback still requires a `resume_dead_runner` claim on a local runner, an absent runner state, and an archived epoch of this session whose runner is suspended and whose name matches its identity. - Restoring only moves files back within the root, and nothing is deleted. ## 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>