fix(server): Pi rollback works past a turn you stopped - #13797
Conversation
Stop always restarts the Pi runtime, so the adapter killed Pi before finalizeTurn could read the session tree. The stopped turn kept its weak synthetic ref, and any later rollback past it failed with "Pi rollback target has no captured session-tree entry" while the effect retried. Read the turn's session-tree refs after the abort and before terminating, and let finalizeTurn reuse them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: unavailable · PR result: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — This is a narrowly scoped Pi rollback bug fix that captures session-tree references before Stop terminates the provider and adds targeted replay coverage for the affected path. The remaining changes are test fixtures and generated transcript data, with no product-default or static-analysis override changes. You can add or adjust custom eligibility rules. Learn more. |
d2269c3
into
t3code/codex-turn-mapping
Rolling back to a Pi checkpoint fails when any later turn was stopped. Found live with Pi 0.87.1 and
openrouter/deepseek/deepseek-v4-flash: turn 1, turn 2 stopped mid-tool, turn 3, then roll back to turn 1. No run was marked rolled back, Pi still had every message, and the effect worker retried the rollback until it gave up:The web client's rewind waits for a
rolled_backrun, so it times out after 120s.Cause
Stop always sends
requestRuntimeRestart, soPiAdapterV2.interruptTurnaborts and then terminates Pi. Pi settles the aborted turn (agent_settled), butfinalizeTurn'sget_entriesthen hits a dead transport, so the stopped turn keeps its weak syntheticnativeTurnRef.piRollbackForkEntryforks at the first discarded turn's user entry, gets no strong ref, and fails. Pi does record that user entry, so the session file has it. We just never read it.Fix
In the Stop-with-restart branch, read the turn's session-tree refs after the abort and before terminating, bounded by the same 2s as the abort.
finalizeTurnreuses them instead of querying the dead process. The read runs under the session event permit, so it cannot race a finalize that is already reading the tree.Verification
thread_rollback_after_stop(Pi), recorded live withscripts/record-pi-rpc-replay-fixture.ts. It runs turn 1, stops turn 2 mid-tool, runs turn 3, rolls back to turn 1, then sends a prompt. It asserts that the stopped turn's ref is strong, that theget_entriesread comes after the abort, thatforktargets that entry, and that the final answer only knows turn 1.await_thread_idle.turn_interrupt_mid_tool/pi_transcript.ndjson, which now has the extraget_entriesbefore the process exits, and added an assertion that its interrupted turn carries a strong ref.vp test run src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts -t "/pi through": 8/8 passed.vp test run src/orchestration-v2/Adapters/PiAdapterV2.test.ts: 49/49 passed.vp exec tsc --noEmit -p .(apps/server): clean. Knip on apps/server exports: clean.vp linton touched files: only the pre-existing unusedlayerwarning.OpenCode doesn't have this gap. Its Stop only calls
session.abortand keeps the server alive, and a live rollback past a stopped OpenCode turn worked.Model: Claude Opus 5.5 (Claude Code)
🤖 Generated with Claude Code