Note
Grok responding on behalf of Julius.
Fixed by #20521, now merged to main: Pi fork and revert now recover a past turn whose session entry was never captured. Thanks for the detailed report and the fix!
Area: apps/server (code in packages/provider-pi/src/server/adapter.ts and apps/server/src/orchestration-v2/ProviderRuntimeRecoveryService.ts)
Steps to reproduce
No API key is needed. Any OpenAI-compatible endpoint works, including a local mock.
@earendil-works/pi-coding-agent 1.3.0) to use an OpenAI-compatible endpoint.SIGKILL the Pi process.Expected behavior: Fork and revert to the first turn succeed. The resulting Pi session contains the first prompt and not the second. If T3 cannot determine the cut point, a retry or another route should still recover, rather than refusing forever.
Actual behavior: Every fork fails with Pi fork boundary has no captured session-tree entry. Every revert fails with Pi rollback target has no captured session-tree entry. Retrying never helps. Pi's session JSONL on disk has the second prompt as a user entry, but T3 never learned its entry id. The thread cannot be forked or reverted to any point before the dead turn.
Impact: Major degradation or frequent failure
Version or commit: main @ a3e8cf69de
Environment: Pi (@earendil-works/pi-coding-agent) 1.3.0, Node 24. The failure is server-side.
Logs or stack traces:
| 1 | Pi fork boundary has no captured session-tree entry |
| 2 | Pi rollback target has no captured session-tree entry |
An RPC trace of the reproduction showed the second turn settled as failed/cancelled, with completedAt set and nativeTurnRef.strength === "weak". The Pi session file held both user prompts.
Analysis (references are to main @ a3e8cf69de)
A Pi turn is created with a synthetic weak nativeTurnRef (adapter.ts:2472). It gets its real session-tree user entry id only in finalizeTurn (adapter.ts:1474), through captureTurnTreeRefs (adapter.ts:1430), which reads get_entries and walks the leaf's parent chain. A turn that never completes a capture keeps the weak ref permanently.
Ways a turn ends without a strong ref:
ProviderRuntimeRecoveryService (apps/server/src/orchestration-v2/ProviderRuntimeRecoveryService.ts:370-386) marks the turn cancelled by spreading the old payload, so the weak ref carries over.captureTurnTreeRefs(2_000) read (adapter.ts:2688) times out.get_entries fails, or the leaf cursor is stale at turn end (adapter.ts:1460-1468). captureTurnTreeRefs then returns nativeTurnRef: undefined and the synthetic ref stays.piRollbackForkEntry (adapter.ts:3132) takes the first turn after the target with a non-null ref as the boundary. It returns undefined when that ref is not strong. Both callers fail closed: revert at adapter.ts:2863, fork at adapter.ts:2988. The weak ref is persisted thread state, so the refusal is permanent for that thread.
Related
completedAt (or the boundary's startedAt when there is no target) and before the boundary's completedAt, above the target's entry. It fails closed when nothing fits. The strong-ref path adds no RPC, and replay transcripts are unchanged. In the reproduction, fork and revert both then resolved the dead turn's prompt and produced session files with the first prompt and not the second.Design note: Two approaches are not exclusive. Prevention would capture the entry id when Pi persists the prompt, and read the tree before killing Pi on session close, so weak refs never form. Repair (#20521) resolves the entry at fork or revert time, which also covers threads already holding a weak ref. The PR is one option in that trade-off, not the final design. Maintainer direction on which side to land on would help.
Workaround: None known. A new thread avoids the problem but cannot branch from the affected thread's earlier turns. (Unverified: whether a later successful turn unblocks forks past the dead turn. From the code it should not, since the dead turn stays the first post-target turn with a non-null ref.)
Note
Grok responding on behalf of Julius.
Fixed by #20521, now merged to main: Pi fork and revert now recover a past turn whose session entry was never captured. Thanks for the detailed report and the fix!