Repository navigation
[Bug]: Pi: forking from a turn fails while the next turn is still running #23050
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server (code in packages/provider-pi/src/server/adapter.ts)
Steps to reproduce
- Open a Pi thread and run one turn to completion.
- Send a second prompt. Leave it running.
- While turn 2 is still running, fork the thread from turn 1 ("Fork from conversation").
- Send a message in the forked thread.
Expected behavior
The fork succeeds. The forked Pi session contains turn 1 and not turn 2's prompt. Turn 2's user entry is already in Pi's session tree while the turn runs, so the cut point can be found.
Actual behavior
The forked thread's first turn fails with "Provider turn failed to start":
pi provider protocol error: Pi turn 2 has no captured session-tree entry and none was found in the session tree.
Impact
Minor bug or occasional failure
Version or commit
0.1.2-nightly.20261021.3473 (1688f965682d). The same code is on main @ d27c6067a916.
Environment
Windows 11, T3 Code desktop (Nightly), Pi @earendil-works/pi-coding-agent 1.3.0, model Claude Opus 5.5.
Logs or stack traces
pi provider protocol error: Pi turn 2 has no captured session-tree entry and none was found in the session tree.State from statev2.sqlite for the source thread at the time of the fork:
turn 1 completed started 18:53:01.244Z completed 18:55:08.895Z nativeTurnRef {nativeId: "0433454c", strength: "strong"}
turn 2 running started 18:56:48.949Z completed null nativeTurnRef {nativeId: "<synthetic provider-thread id>", strength: "weak"}
The forked thread was created at 18:57:56Z, forking from turn 1 (forkedFrom.providerTurnId = turn 1).
Analysis
References are to main @ d27c6067a916.
This is a follow-up to #20530 / #20521. That fix covers a boundary turn that ended without a captured ref. It doesn't cover a boundary turn that is still running.
- A Pi turn gets its strong
nativeTurnRefonly whencaptureTurnTreeRefsruns at turn end (adapter.ts:1432,:1499). While turn 2 runs, it carries the synthetic weak ref. - Forking from turn 1 calls
piForkBoundary(adapter.ts:3007, defined at:3192). It picks turn 2 as the boundary, because turn 2 is the first later turn with a non-null ref. Since that ref is weak, it returns{ type: "uncaptured" }. piForkEntry(:3210) falls back topiUncapturedForkEntry(:3240). That function returnsundefinedimmediately whencompletedAt === null(:3246), because the search window ends at the boundary turn'scompletedAt. A running turn has nocompletedAt, so the fork always fails, even though turn 2's user entry is already on Pi's active branch.
Rollback uses the same helpers (:2877, :2881). I haven't checked whether rollback can be reached while a turn is running.
Possible fixes
These are options for maintainers to choose from, not a decided design:
- Open-ended window for a running boundary turn. When the boundary turn is running, use "now" (or the time of the
get_entriesread) as the upper bound instead ofcompletedAt. This is the smallest change. The fix(pi): fork and revert past turns whose session entry was never captured #20521 review found a wrong-cut case caused by later steers and adopted wakes, so the open-ended bound needs the same scrutiny. A steer sent during the running turn would also fall inside the window. The code takes the earliest matching user entry, which looks correct there, but I haven't verified it. - Capture at prompt persistence. Record the boundary turn's user entry id as soon as Pi persists the prompt, so a running turn already has a strong ref. [Bug]: Pi: fork and revert fail permanently past a turn whose session entry was never captured #20530 suggested this as the prevention approach.
- Defer or block the fork while the source has a running turn. This changes product behavior, so it needs maintainer direction.
Workaround
Let the source thread's running turn finish (or stop it), then fork again. Once turn 2 completes, captureTurnTreeRefs records a strong ref and the boundary resolves directly. I haven't verified whether the forked thread that already failed recovers on its own, or whether it has to be re-forked.
Investigated and filed with help from Claude Opus 5.5 via pi in T3 Code.
Activity
juliusmarminge commentedon Oct 21, 2026
Reproduced with the Pi adapter's fake session-tree harness on main: forking from a completed turn while the next turn is still running picked the running turn as the boundary and failed with the "no captured session-tree entry" error, because the boundary check rejected any turn without a completion time.
Fix in https://git.cafe/pingdotgg/t3code/pulls/23052: a running later turn is accepted as an open upper bound, so the fork cuts after the completed turn and before the running turn's prompt, while the existing ancestry and lower-bound checks from https://git.cafe/pingdotgg/t3code/issues/20530 stay in place. Regression tests cover the running-boundary case. The live Pi runtime was not exercised; the adapter harness was.