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

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

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

Visibility: public

[fix\(server\): a fork's first /compact compacts its inherited history](https://git.cafe/pingdotgg/t3code/pull/24172.md)

Version: 15

Head commit: 2615e21decad822bf4dd520cf103f9a8e33d49df

Base commit: 66ee628e0875b2b735b199f42e6151e347833637

Comparison base: 66ee628e0875b2b735b199f42e6151e347833637

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

## apps/server/src/orchestration-v2/ProjectionStore.ts

Status: modified; +30; −7

```diff
@@ -351,6 +351,26 @@
   "waiting",
 ]);
 
+// A fork inherits its source's history before it has messages of its own,
+// unless a portable handoff's budget left none of that history to deliver.
+const inheritsForkConversation = ({
+  thread,
+  contextTransfers,
+  contextHandoffs,
+}: Pick<OrchestrationV2ThreadProjection, "thread" | "contextTransfers" | "contextHandoffs">) => {
+  if (thread.forkedFrom?.type !== "run") return false;
+  const resolution = contextTransfers.find(
+    (transfer) => transfer.type === "fork" && transfer.targetThreadId === thread.id,
+  )?.resolution;
+  return (
+    resolution?.strategy !== "portable_context" ||
+    contextHandoffs.some(
+      (handoff) =>
+        handoff.id === resolution.contextHandoffId && (handoff.history?.messages.length ?? 0) > 0,
+    )
+  );
+};
+
 // In-memory twin of the omitSettledLeafNodes SQL clause in readCanonicalProjection.
 const isSettledLeafNode = (node: ProjectionNode, runs: ReadonlyArray<ProjectionRun>) =>
   settledLeafNodeKinds.has(node.kind) &&
@@ -4265,7 +4285,9 @@
               ) AS present
             `;
             return {
+              hasConversation:
+                conversation[0]?.present === 1 ||
-              hasConversation: conversation[0]?.present === 1,
+                inheritsForkConversation({ thread, contextTransfers, contextHandoffs }),
               thread,
               runs,
               attempts,
@@ -7249,12 +7271,13 @@
         service.getThreadProjection(threadId).pipe(
           Effect.map((projection) => ({
             ...projection,
-            hasConversation: projection.messages.some(
-              (message) =>
-                message.role === "user" &&
-                (message.text.trim().toLowerCase() !== "/compact" ||
+            hasConversation:
+              projection.messages.some(
+                (message) =>
+                  message.role === "user" &&
+                  (message.text.trim().toLowerCase() !== "/compact" ||
-                  message.attachments.length > 0),
-            ),
+                    message.attachments.length > 0),
+              ) || inheritsForkConversation(projection),
             turnItems: projection.turnItems.filter((item) => item.runId === runId),
           })),
         ),
```

## apps/server/src/orchestration-v2/ThreadFork.execution.test.ts

Status: modified; +216; −1

```diff
@@ -20,11 +20,12 @@
 
 import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";
 import { ClaudeProviderCapabilitiesV2 } from "./Adapters/ClaudeAdapterV2.ts";
+import * as EffectWorker from "./EffectWorker.ts";
 import * as EventSink from "./EventSink.ts";
 import * as Orchestrator from "./Orchestrator.ts";
 import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";
 import * as ProviderReplayHarness from "./testkit/ProviderReplayHarness.ts";
-import type * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
+import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
 
 const forkCases = (
   [
@@ -395,3 +396,217 @@
       assert.isNull(target.providerThreads[0]?.forkedFrom);
     }).pipe(Effect.provide(layer)),
 );
+
+it.effect.each([
+  { execution: "portable_context", rejected: true },
+  { execution: "native_fork", rejected: false },
+] as const)(
+  "a $execution fork's first /compact is rejected only when no history reaches it: $rejected",
+  ({ execution, rejected }) => {
+    const opened: Array<ProviderInstanceId> = [];
+    const adapter = (driverName: "claudeAgent" | "codex") => {
+      const driver = ProviderDriverKind.make(driverName);
+      const instanceId = ProviderInstanceId.make(driverName);
+      return {
+        instanceId,
+        driver,
+        getCapabilities: () =>
+          Effect.succeed(
+            driver === "codex" ? CodexProviderCapabilitiesV2 : ClaudeProviderCapabilitiesV2,
+          ),
+        planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" }),
+        openSession: (input) =>
+          Effect.sync(() => opened.push(instanceId)).pipe(
+            Effect.andThen(
+              new ProviderAdapter.ProviderAdapterOpenSessionError({
+                driver,
+                providerSessionId: input.providerSessionId,
+              }),
+            ),
+          ),
+      } satisfies ProviderAdapter.ProviderAdapterV2["Service"];
+    };
+    const claude = adapter("claudeAgent");
+    const target = execution === "portable_context" ? adapter("codex") : claude;
+    return Effect.gen(function* () {
+      const orchestrator = yield* Orchestrator.OrchestratorV2;
+      const eventSink = yield* EventSink.EventSinkV2;
+      const worker = yield* EffectWorker.OrchestrationEffectWorkerV2;
+      const now = yield* DateTime.now;
+      const sourceThreadId = ThreadId.make("fork-compact-source");
+      const targetThreadId = ThreadId.make("fork-compact-target");
+      const providerThreadId = ProviderThreadId.make("fork-compact-native-thread");
+      const sourceRunId = RunId.make("fork-compact-source-run");
+      const userMessageId = MessageId.make("fork-compact-source-message");
+      const sourceSelection = { instanceId: claude.instanceId, model: "test-model" };
+
+      yield* orchestrator.dispatch({
+        type: "thread.create",
+        commandId: CommandId.make("fork-compact-create-source"),
+        threadId: sourceThreadId,
+        projectId: ProjectId.make("fork-compact-project"),
+        title: "Fork compact source",
+        modelSelection: sourceSelection,
+        runtimeMode: "full-access",
+        interactionMode: "default",
+        branch: null,
+        worktreePath: null,
+        createdBy: "user",
+        creationSource: "web",
+      });
+      const item = {
+        threadId: sourceThreadId,
+        runId: sourceRunId,
+        nodeId: null,
+        providerThreadId,
+        providerTurnId: null,
+        nativeItemRef: null,
+        parentItemId: null,
+        status: "completed",
+        title: null,
+        startedAt: now,
+        completedAt: now,
+        updatedAt: now,
+      } as const;
+      yield* eventSink.write({
+        events: [
+          {
+            id: EventId.make("fork-compact-provider-thread"),
+            type: "provider-thread.updated",
+            threadId: sourceThreadId,
+            occurredAt: now,
+            payload: {
+              id: providerThreadId,
+              driver: claude.driver,
+              providerInstanceId: claude.instanceId,
+              providerSessionId: null,
+              appThreadId: sourceThreadId,
+              ownerNodeId: null,
+              nativeThreadRef: {
+                driver: claude.driver,
+                nativeId: "native-fork-compact-source",
+                strength: "strong",
+              },
+              nativeConversationHeadRef: null,
+              status: "idle",
+              firstRunOrdinal: 1,
+              lastRunOrdinal: 1,
+              handoffIds: [],
+              forkedFrom: null,
+              createdAt: now,
+              updatedAt: now,
+            },
+          },
+          {
+            id: EventId.make("fork-compact-source-run"),
+            type: "run.created",
+            threadId: sourceThreadId,
+            runId: sourceRunId,
+            occurredAt: now,
+            payload: {
+              id: sourceRunId,
+              threadId: sourceThreadId,
+              ordinal: 1,
+              providerInstanceId: claude.instanceId,
+              modelSelection: sourceSelection,
+              providerThreadId,
+              userMessageId,
+              rootNodeId: null,
+              activeAttemptId: null,
+              status: "completed",
+              queuePosition: null,
+              requestedAt: now,
+              startedAt: now,
+              completedAt: now,
+              checkpointId: null,
+              contextHandoffId: null,
+            },
+          },
+          // Each message alone exceeds the default 16k handoff budget, so a
+          // portable handoff omits both whole items.
+          {
+            id: EventId.make("fork-compact-user-item"),
+            type: "turn-item.updated",
+            threadId: sourceThreadId,
+            runId: sourceRunId,
+            occurredAt: now,
+            payload: {
+              ...item,
+              id: TurnItemId.make("fork-compact-user-item"),
+              ordinal: 1,
+              type: "user_message",
+              createdBy: "user",
+              creationSource: "web",
+              inputIntent: "turn_start",
+              messageId: userMessageId,
+              text: "u".repeat(17_000),
+              attachments: [],
+            },
+          },
+          {
+            id: EventId.make("fork-compact-assistant-item"),
+            type: "turn-item.updated",
+            threadId: sourceThreadId,
+            runId: sourceRunId,
+            occurredAt: now,
+            payload: {
+              ...item,
+              id: TurnItemId.make("fork-compact-assistant-item"),
+              ordinal: 2,
+              type: "assistant_message",
+              messageId: MessageId.make("fork-compact-assistant-message"),
+              text: "a".repeat(17_000),
+              streaming: false,
+            },
+          },
+        ],
+      });
+      yield* orchestrator.dispatch({
+        type: "thread.fork",
+        commandId: CommandId.make("fork-compact-fork"),
+        sourceThreadId,
+        targetThreadId,
+        sourcePoint: { type: "run", runId: sourceRunId },
+        createdBy: "user",
+        creationSource: "web",
+      });
+      yield* orchestrator.dispatch({
+        type: "message.dispatch",
+        commandId: CommandId.make("fork-compact-compact"),
+        threadId: targetThreadId,
+        messageId: MessageId.make("fork-compact-compact"),
+        text: "/compact",
+        attachments: [],
+        modelSelection: { instanceId: target.instanceId, model: "test-model" },
+        dispatchMode: { type: "start_immediately" },
+        createdBy: "user",
+        creationSource: "web",
+      });
+      yield* worker.drain();
+
+      const projection = yield* orchestrator.getThreadProjection(targetThreadId);
+      if (execution === "portable_context") {
+        assert.equal(projection.contextTransfers[0]?.resolution?.strategy, "portable_context");
+        assert.deepEqual(projection.contextHandoffs[0]?.history?.messages, []);
+      } else {
+        assert.isEmpty(projection.contextHandoffs);
+      }
+      const emptyCompaction = projection.turnItems.some(
+        (turnItem) =>
+          turnItem.type === "error" &&
+          turnItem.failure.message === "Start a conversation before compacting this thread.",
+      );
+      assert.equal(emptyCompaction, rejected);
+      assert.deepEqual(opened, rejected ? [] : [target.instanceId]);
+    }).pipe(
+      Effect.provide(
+        ProviderReplayHarness.layerWithRegistry(
+          { name: `fork-compact-${execution}` },
+          ProviderAdapterRegistry.layerFromAdapters(
+            target === claude ? [claude] : [claude, target],
+          ),
+        ),
+      ),
+    );
+  },
+);
```

## apps/server/src/orchestration-v2/TurnStartReads.test.ts

Status: modified; +149; −0

```diff
@@ -1,10 +1,12 @@
 import { assert, it } from "@effect/vitest";
 import {
   CheckpointScopeId,
+  ContextTransferId,
   NodeId,
   EventId,
   MessageId,
   ProjectId,
+  ProviderDriverKind,
   ProviderInstanceId,
   RunId,
   ThreadId,
@@ -366,3 +368,150 @@
       ),
     ),
 );
+
+it.effect.each(["sqlite", "memory"] as const)(
+  "counts a native fork's inherited history as conversation for its first /compact in %s",
+  (storage) =>
+    Effect.gen(function* () {
+      const store = yield* ProjectionStore.ProjectionStoreV2;
+      const now = yield* DateTime.now;
+      const instanceId = ProviderInstanceId.make("claudeAgent");
+      const sourceId = ThreadId.make("thread:fork-source");
+      const forkId = ThreadId.make("thread:fork-target");
+      const sourceRunId = RunId.make("run:fork-source-third");
+      const forkRunId = RunId.make("run:fork-compact");
+      const source: OrchestrationV2AppThread = {
+        id: sourceId,
+        projectId: ProjectId.make("project:fork-compact"),
+        title: "Source",
+        providerInstanceId: instanceId,
+        modelSelection: { instanceId, model: "claude-opus-5-5" },
+        runtimeMode: "full-access",
+        interactionMode: "default",
+        branch: null,
+        worktreePath: null,
+        activeProviderThreadId: null,
+        lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: sourceId },
+        forkedFrom: null,
+        createdBy: "user",
+        creationSource: "web",
+        createdAt: now,
+        updatedAt: now,
+        archivedAt: null,
+        settledOverride: null,
+        settledAt: null,
+        lastVisitedAt: null,
+        deletedAt: null,
+      };
+      const fork: OrchestrationV2AppThread = {
+        ...source,
+        id: forkId,
+        title: "Fork",
+        lineage: { parentThreadId: sourceId, relationshipToParent: "fork", rootThreadId: sourceId },
+        forkedFrom: { type: "run", threadId: sourceId, runId: sourceRunId },
+      };
+      const run = (threadId: ThreadId, id: RunId, status: OrchestrationV2Run["status"]) =>
+        ({
+          id,
+          threadId,
+          ordinal: 1,
+          providerInstanceId: instanceId,
+          modelSelection: source.modelSelection,
+          providerThreadId: null,
+          userMessageId: MessageId.make(`message:${id}`),
+          rootNodeId: NodeId.make(`node:${id}`),
+          activeAttemptId: null,
+          status,
+          requestedAt: now,
+          startedAt: null,
+          completedAt: null,
+          checkpointId: null,
+          contextHandoffId: null,
+        }) satisfies OrchestrationV2Run;
+      const userMessage = (threadId: ThreadId, runId: RunId, text: string) =>
+        store.apply({
+          id: EventId.make(`event:message:${runId}`),
+          type: "message.updated",
+          threadId,
+          runId,
+          occurredAt: now,
+          payload: {
+            id: MessageId.make(`message:${runId}`),
+            threadId,
+            runId,
+            nodeId: null,
+            role: "user",
+            text,
+            attachments: [],
+            streaming: false,
+            createdBy: "user",
+            creationSource: "web",
+            createdAt: now,
+            updatedAt: now,
+          },
+        });
+      for (const thread of [source, fork]) {
+        yield* store.apply({
+          id: EventId.make(`event:thread:${thread.id}`),
+          type: "thread.created",
+          threadId: thread.id,
+          occurredAt: now,
+          payload: thread,
+        });
+      }
+      for (const [threadId, value] of [
+        [sourceId, run(sourceId, sourceRunId, "completed")],
+        [forkId, run(forkId, forkRunId, "queued")],
+      ] as const) {
+        yield* store.apply({
+          id: EventId.make(`event:run:${value.id}`),
+          type: "run.updated",
+          threadId,
+          runId: value.id,
+          occurredAt: now,
+          payload: value,
+        });
+      }
+      yield* userMessage(sourceId, sourceRunId, "THIRD");
+      yield* store.apply({
+        id: EventId.make("event:fork-transfer"),
+        type: "context-transfer.created",
+        threadId: forkId,
+        occurredAt: now,
+        payload: {
+          id: ContextTransferId.make("transfer:fork-compact"),
+          type: "fork",
+          sourceThreadId: sourceId,
+          targetThreadId: forkId,
+          sourcePoint: { threadId: sourceId, runId: sourceRunId },
+          basePoint: null,
+          sourceProviderInstanceId: instanceId,
+          targetProviderInstanceId: instanceId,
+          targetRunId: forkRunId,
+          status: "pending",
+          resolution: {
+            strategy: "native_fork",
+            providerThreadRef: {
+              driver: ProviderDriverKind.make("claudeAgent"),
+              nativeId: "native-fork-source",
+              strength: "strong",
+            },
+          },
+          createdBy: "user",
+          error: null,
+          createdAt: now,
+          updatedAt: now,
+          consumedAt: null,
+        },
+      });
+      yield* userMessage(forkId, forkRunId, "/compact");
+
+      assert.isTrue((yield* store.getTurnStartContext(forkId, forkRunId)).hasConversation);
+    }).pipe(
+      Effect.provide(
+        storage === "sqlite"
+          ? layerDatabase
+          : Layer.merge(SqlitePersistence.layerMemory, ProjectionStore.layerMemory),
+      ),
+    ),
+);
```
