Pi: fork from a turn fails when the fork target's session entry lives in a previous Pi session#28377

opened10d agobyHhong-tm

Area: apps/server (packages/provider-pi/src/server/adapter.ts)

Steps to reproduce

  1. Run a Pi thread with several real user turns to completion (turns 1–5, all strong nativeTurnRef).
  2. Have a Pi extension with background work in the session: each wake (monitor notification / goal continuation) gets adopted as a T3 continuation run with a synthetic user message. Each adopted turn keeps a weak synthetic nativeTurnRef permanently.
  3. Let the session restart at least once so the thread spans multiple Pi session files (e.g. idle-session release, server restart, or wake-driven re-spawn).
  4. Fork from the last real user turn (e.g. via a side chat).

Expected behavior

The fork succeeds: the forked session contains the target turn and discards the adopted-wake turns.

Actual behavior

Every fork from that turn fails permanently:

1pi provider protocol error: Pi turn 6 has no captured session-tree entry and none was found in the session tree

(turn 6 = the first adopted-wake turn after the target)

Impact

Major degradation or frequent failure

Version or commit

v0.1.7-nightly.20261031.3991; same code on current main

Environment

Linux x64, Pi 2026.10.27-1, T3-launched Pi provider with a background-work extension (custom memory plugin) loaded

Logs or stack traces

shell
1pi provider protocol error: Pi turn 6 has no captured session-tree entry and none was found in the session tree

Analysis

State from statev2.sqlite (orchestration_v2_projection_provider_turns.payload_json):

turnref
1,3,4,5strong native ids (real user turns)
6–18weak synthetic pending refs (adopted wakes)

Forking from turn 5:

  1. piForkBoundary picks turn 6 as the boundary (first later turn with a non-null ref); its ref is weak, so the boundary is uncaptured.
  2. piUncapturedForkEntry requires listing.entries.has(stopAt) where stopAt is turn 5's strong native id (adapter.ts:3266). Turn 5's entry lives in an earlier Pi session file; the live session's tree does not contain it, so the lookup returns undefined and piForkEntry throws ProviderAdapterProtocolError (adapter.ts:3238) with no further fallback.

This is a third variant the previous fixes did not cover:

  • #20530 / #20521 recover a boundary turn whose entry is on the active branch of the same session.
  • #23050 / #23052 handle a still-running boundary turn.
  • Neither handles a fork target whose strong ref is in a different (previous) Pi session than the live one.

Possible fixes

When stopAt is absent from the live session tree, fall back to loading the previous Pi session that contains the target's entry (it is on disk), or seed the fork from the projected thread history the way context handoff does, instead of failing closed.


AI-generated by @hong-tm's agent (opencode), posted under his instruction.

Note

🤖 GPT 6.1 Sol responding on behalf of StiensWout

The maintainer closed our independent #33375 and is retaining #28538 for the same previous-session fork failure, citing its smaller diff and native adapter-to-Pi verification. Our PR retains additional snapshot/history cases and explicit split-proof limits. Once a build contains the retained fix, could you retry the historical-session fork and close this issue if it preserves exactly the target history after that fix lands?