# Files changed · PR \#23052 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/23052/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[fix\(pi\): fork completed turns while the next turn is running](https://git.cafe/pingdotgg/t3code/pull/23052.md)

Version: 10

Head commit: 1372a7af05a99986ff154c41ec3d6c83b21f51c3

Base commit: d27c6067a9162150f654e38f455a7a936ef79178

Comparison base: d27c6067a9162150f654e38f455a7a936ef79178

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## packages/provider-pi/src/server/adapter.test.ts

Status: modified; +46; −0

```diff
@@ -21,6 +21,7 @@
 import * as Cause from "effect/Cause";
 import * as DateTime from "effect/DateTime";
 import * as Effect from "effect/Effect";
+import * as Exit from "effect/Exit";
 import * as Fiber from "effect/Fiber";
 import * as Layer from "effect/Layer";
 import * as Queue from "effect/Queue";
@@ -1801,6 +1802,51 @@
     }).pipe(Effect.scoped, Effect.provide(layerTest)),
   );
 
+  it.effect("natively forks before a running turn whose start entry was not captured yet", () =>
+    Effect.gen(function* () {
+      const fake = yield* makeFakePi;
+      const forkFake = yield* makeFakePi;
+      const forkFile = "/fake/forked.jsonl";
+      const { runtime } = yield* openRuntime(fake, "default", THREAD_ID, SESSION_ID, forkFake);
+      const source = yield* runtime.ensureThread({
+        threadId: THREAD_ID,
+        modelSelection: modelSelection("default"),
+        runtimePolicy,
+      });
+      // Turn 2 is still in flight, so it has no end to bound the session-tree search.
+      const turns = uncapturedTurns(source.id, { startedAt: 100, completedAt: 200 }).map((turn) =>
+        turn.ordinal === 2 ? { ...turn, status: "running" as const, completedAt: null } : turn,
+      );
+      forkFake.queueEntries(UNCAPTURED_TURN_TREE);
+      forkFake.queueState({ sessionFile: forkFile });
+      fake.queueState({ sessionFile: forkFile });
+
+      const result = yield* runtime
+        .forkThread({
+          sourceProviderThread: source,
+          sourceProviderTurns: turns,
+          providerTurnId: turns[0]!.id,
+          targetThreadId: ThreadId.make("fork-target"),
+        })
+        .pipe(Effect.exit);
+      assert.isTrue(
+        Exit.isSuccess(result),
+        `forking turn 1 while turn 2 runs must locate turn 2's prompt in the session tree, got: ${
+          Exit.isFailure(result) ? Cause.pretty(result.cause) : ""
+        }`,
+      );
+      if (!Exit.isSuccess(result)) return;
+      assert.equal(result.value.nativeThreadRef?.nativeId, forkFile);
+
+      const forkedEntries = forkFake
+        .allRequests()
+        .filter((request) => request.type === "fork")
+        .map((request) => request.entryId);
+      // Pi keeps only the fork entry's ancestors, so u2 (not steer2) drops all of turn 2.
+      assert.deepEqual(forkedEntries, ["u2"]);
+    }).pipe(Effect.scoped, Effect.provide(layerTest)),
+  );
+
   it.effect("presents explicitly namespaced MCP extension tools", () =>
     Effect.gen(function* () {
       const fake = yield* makeFakePi;
```

## packages/provider-pi/src/server/adapter.ts

Status: modified; +5; −4

```diff
@@ -3234,8 +3234,9 @@
 
 /**
  * Pi shares the server clock. An adopted wake can precede its T3 turn, so search from the target
+ * turn's end through the boundary turn's end. A boundary turn still running has no end yet, so
+ * the window stays open through the tree read. At thread start, use the earlier of thread
+ * creation and turn start to include adopted wakes while preserving inherited session history.
- * turn's end through the boundary turn's end. At thread start, use the earlier of thread creation
- * and turn start to include adopted wakes while preserving inherited session history.
  */
 function piUncapturedForkEntry(
   listing: PiSessionListing,
@@ -3243,12 +3244,12 @@
   providerThreadCreatedAt: DateTime.Utc,
 ): string | undefined {
   const { startedAt, completedAt } = boundary.turn;
-  if (startedAt === null || completedAt === null) return undefined;
+  if (startedAt === null) return undefined;
   const from =
     boundary.target === null
       ? Math.min(DateTime.toEpochMillis(providerThreadCreatedAt), DateTime.toEpochMillis(startedAt))
       : DateTime.toEpochMillis(boundary.target.completedAt ?? startedAt);
-  const to = DateTime.toEpochMillis(completedAt);
+  const to = completedAt === null ? Infinity : DateTime.toEpochMillis(completedAt);
   const targetRef = boundary.target?.nativeTurnRef;
   const stopAt = targetRef?.strength === "strong" ? targetRef.nativeId : null;
   if (stopAt !== null && !listing.entries.has(stopAt)) return undefined;
```
