Skip to content

[Bug]: Pi: forking from a turn fails while the next turn is still running #23050

Closed

Description

@zmooter

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

  1. Open a Pi thread and run one turn to completion.
  2. Send a second prompt. Leave it running.
  3. While turn 2 is still running, fork the thread from turn 1 ("Fork from conversation").
  4. 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.

  1. A Pi turn gets its strong nativeTurnRef only when captureTurnTreeRefs runs at turn end (adapter.ts:1432, :1499). While turn 2 runs, it carries the synthetic weak ref.
  2. 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" }.
  3. piForkEntry (:3210) falls back to piUncapturedForkEntry (:3240). That function returns undefined immediately when completedAt === null (:3246), because the search window ends at the boundary turn's completedAt. A running turn has no completedAt, 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:

  1. Open-ended window for a running boundary turn. When the boundary turn is running, use "now" (or the time of the get_entries read) as the upper bound instead of completedAt. 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.
  2. 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.
  3. 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

  1. juliusmarminge commented on Oct 21, 2026

    @juliusmarminge
    Member

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions