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

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

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

Visibility: public

[fix\(server\): a thread's branch and worktree follow the agent when it moves](https://git.cafe/pingdotgg/t3code/pull/15989.md)

Version: 36

Head commit: f5a80658991a671a53a8f330baa44c59f12cdfba

Base commit: efa5381246675a337269a2a8843e98d02f2bc442

Comparison base: 05b697f6a2012497ae790425e197fbd1fcef7de7

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

## apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts

Status: modified; +237; −0

```diff
@@ -964,6 +964,243 @@
   );
 });
 
+describe("ClaudeAdapterV2 worktree location follow", () => {
+  it.effect.each(["reuse", "replace"] as const)(
+    "reports EnterWorktree/ExitWorktree cwd moves as provider_session.updated across %s after failure",
+    (lifecycle) =>
+      Effect.scoped(
+        Effect.gen(function* () {
+          const fileSystem = yield* FileSystem.FileSystem;
+          const idAllocator = yield* IdAllocator.IdAllocatorV2;
+          const attachmentsDir = yield* fileSystem.makeTempDirectoryScoped({
+            prefix: "t3-claude-worktree-follow-",
+          });
+          let openedOptions: ClaudeAdapterV2.ClaudeAgentSdkQueryOptions | undefined;
+          const openedCwds: Array<string | undefined> = [];
+          const processQueues: Array<Queue.Queue<SDKMessage>> = [];
+          let closedProcesses = 0;
+          const adapter = yield* ClaudeAdapterV2.makeClaudeAdapterV2({
+            instanceId: ClaudeAdapterV2.CLAUDE_DEFAULT_INSTANCE_ID,
+            settings: DEFAULT_CLAUDE_SETTINGS,
+            environment: {},
+            attachmentsDir,
+            fileSystem,
+            path: yield* Path.Path,
+            crypto: yield* Crypto.Crypto,
+            idAllocator,
+            queryRunner: {
+              allocateSessionId: Effect.succeed("native-thread-claude-worktree-follow"),
+              open: (input) =>
+                Effect.gen(function* () {
+                  openedOptions = input.options;
+                  openedCwds.push(input.options.cwd);
+                  const messages = yield* Queue.unbounded<SDKMessage>();
+                  processQueues.push(messages);
+                  return {
+                    messages: Stream.fromQueue(messages),
+                    offer: () => Effect.void,
+                    setModel: () => Effect.void,
+                    applyFlagSettings: () => Effect.void,
+                    setPermissionMode: () => Effect.void,
+                    stopTask: () => Effect.void,
+                    backgroundTasks: () => Effect.succeed(false),
+                    interrupt: Effect.void,
+                    close: Queue.shutdown(messages).pipe(
+                      Effect.tap(() =>
+                        Effect.sync(() => {
+                          closedProcesses += 1;
+                        }),
+                      ),
+                    ),
+                  };
+                }),
+              forkSession: () => Effect.die("unused"),
+              subagentLaunchToolUseId: () => Effect.succeed(null),
+              assertComplete: Effect.void,
+            },
+          });
+          const originalCwd = "/workspace";
+          const threadId = ThreadId.make("thread-claude-worktree-follow");
+          const runtime = yield* adapter.openSession({
+            threadId,
+            providerSessionId: ProviderSessionId.make("provider-session-claude-worktree-follow"),
+            modelSelection: CLAUDE_TEST_MODEL_SELECTION,
+            runtimePolicy: CLAUDE_TEST_RUNTIME_POLICY,
+          });
+          const providerThread = yield* runtime.ensureThread({
+            threadId,
+            modelSelection: CLAUDE_TEST_MODEL_SELECTION,
+            runtimePolicy: CLAUDE_TEST_RUNTIME_POLICY,
+          });
+          const now = yield* DateTime.now;
+          yield* runtime.startTurn(
+            makeClaudeTestTurnInput({
+              threadId,
+              providerThread,
+              now,
+              attemptId: RunAttemptId.make("attempt-claude-worktree-follow"),
+              text: "Enter the worktree.",
+              attachments: [],
+            }),
+          );
+          const hook = openedOptions?.hooks?.PostToolUse?.[0]?.hooks[0];
+          assert.isFunction(hook);
+          const fireHook = (input: {
+            readonly cwd: string;
+            readonly toolName: "EnterWorktree" | "ExitWorktree";
+            readonly agentId?: string;
+          }) =>
+            Effect.promise(() =>
+              openedOptions!.hooks!.PostToolUse![0]!.hooks[0]!(
+                {
+                  hook_event_name: "PostToolUse",
+                  session_id: "sdk-session",
+                  transcript_path: "/tmp/transcript.jsonl",
+                  cwd: input.cwd,
+                  tool_name: input.toolName,
+                  tool_use_id: `tool-${input.toolName}`,
+                  tool_input: {},
+                  tool_response: {},
+                  ...(input.agentId === undefined ? {} : { agent_id: input.agentId }),
+                },
+                undefined,
+                { signal: new AbortController().signal },
+              ),
+            );
+          const nextSessionUpdate = Effect.gen(function* () {
+            const event = yield* runtime.events.pipe(
+              Stream.filter((item) => item.type === "provider_session.updated"),
+              Stream.runHead,
+            );
+            assert.isTrue(Option.isSome(event));
+            assert.equal(
+              Option.isSome(event) ? event.value.providerSession.cwdChangeSource : undefined,
+              originalCwd,
+            );
+            return Option.isSome(event) ? event.value.providerSession.cwd : null;
+          });
+
+          const worktreeCwd = `${originalCwd}/.claude/worktrees/feature`;
+          yield* fireHook({ cwd: worktreeCwd, toolName: "EnterWorktree" });
+          assert.equal(yield* nextSessionUpdate, worktreeCwd);
+
+          yield* fireHook({
+            cwd: `${originalCwd}/.claude/worktrees/subagent-only`,
+            toolName: "EnterWorktree",
+            agentId: "agent-1",
+          });
+          yield* fireHook({ cwd: originalCwd, toolName: "ExitWorktree" });
+          assert.equal(yield* nextSessionUpdate, originalCwd);
+
+          yield* fireHook({ cwd: worktreeCwd, toolName: "EnterWorktree" });
+          assert.equal(yield* nextSessionUpdate, worktreeCwd);
+          const sessionUpdates: Array<
+            Extract<
+              ProviderAdapter.ProviderAdapterV2Event,
+              { type: "provider_session.updated" }
+            >["providerSession"]
+          > = [];
+          const finishTurn = (ordinal: number, failed = false) =>
+            Effect.gen(function* () {
+              yield* Queue.offer(processQueues.at(-1)!, {
+                type: "result",
+                subtype: failed ? "error_during_execution" : "success",
+                duration_ms: 10,
+                duration_api_ms: 10,
+                is_error: failed,
+                num_turns: 1,
+                result: failed ? "boom" : "Done",
+                errors: failed ? ["boom"] : [],
+                stop_reason: "end_turn",
+                total_cost_usd: 0,
+                usage: {
+                  input_tokens: 1,
+                  output_tokens: 1,
+                  cache_creation_input_tokens: 0,
+                  cache_read_input_tokens: 0,
+                  cache_creation: { ephemeral_5m_input_tokens: 0, ephemeral_1h_input_tokens: 0 },
+                  inference_geo: "us",
+                  iterations: [],
+                  server_tool_use: { web_fetch_requests: 0, web_search_requests: 0 },
+                  service_tier: "standard",
+                  speed: "standard",
+                },
+                modelUsage: {},
+                permission_denials: [],
+                terminal_reason: failed ? "model_error" : "completed",
+                uuid: `00000000-0000-4000-8000-00000000000${ordinal}`,
+                session_id: "native-thread-claude-worktree-follow",
+              });
+              const terminal = yield* runtime.events.pipe(
+                Stream.tap((event) =>
+                  Effect.sync(() => {
+                    if (event.type === "provider_session.updated")
+                      sessionUpdates.push(event.providerSession);
+                  }),
+                ),
+                Stream.filter((event) => event.type === "turn.terminal"),
+                Stream.runHead,
+              );
+              assert.equal(Option.getOrThrow(terminal).status, failed ? "failed" : "completed");
+            });
+          yield* finishTurn(1, true);
+
+          const nextTurn = (ordinal: number) =>
+            makeClaudeTestTurnInput({
+              threadId,
+              providerThread,
+              now,
+              attemptId: RunAttemptId.make(`attempt-claude-worktree-follow-${ordinal}`),
+              providerTurnOrdinal: ordinal,
+              text: "Continue.",
+              attachments: [],
+              modelSelection:
+                lifecycle === "replace"
+                  ? CLAUDE_TEST_RESTART_MODEL_SELECTION
+                  : CLAUDE_TEST_MODEL_SELECTION,
+            });
+          yield* runtime.startTurn(nextTurn(2));
+          assert.deepEqual(
+            openedCwds,
+            lifecycle === "replace" ? [originalCwd, originalCwd] : [originalCwd],
+          );
+          assert.equal(closedProcesses, lifecycle === "replace" ? 1 : 0);
+          yield* finishTurn(2);
+          const afterSuccess = sessionUpdates.map(({ cwd, cwdChangeSource }) => ({
+            cwd,
+            cwdChangeSource,
+          }));
+
+          yield* runtime.startTurn(nextTurn(3));
+          yield* fireHook({ cwd: worktreeCwd, toolName: "EnterWorktree" });
+          yield* finishTurn(3);
+          assert.deepEqual(
+            {
+              afterSuccess,
+              afterRepeatedEnter: sessionUpdates.map(({ cwd, cwdChangeSource }) => ({
+                cwd,
+                cwdChangeSource,
+              })),
+            },
+            lifecycle === "replace"
+              ? {
+                  afterSuccess: [{ cwd: originalCwd, cwdChangeSource: undefined }],
+                  afterRepeatedEnter: [
+                    { cwd: originalCwd, cwdChangeSource: undefined },
+                    { cwd: worktreeCwd, cwdChangeSource: originalCwd },
+                  ],
+                }
+              : { afterSuccess: [], afterRepeatedEnter: [] },
+          );
+        }).pipe(
+          Effect.provide(
+            Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
+          ),
+        ),
+      ),
+  );
+});
+
 describe("ClaudeAdapterV2 approval cancellation", () => {
   it.effect("observes an approval signal that was already aborted", () =>
     Effect.gen(function* () {
```

## apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts

Status: modified; +55; −0

```diff
@@ -27,6 +27,7 @@
   type ForkSessionOptions,
   type ForkSessionResult,
   getSessionMessages,
+  type HookCallback,
   getSubagentMessages,
   query,
   resolveSettings as resolveClaudeSettings,
@@ -1287,6 +1288,7 @@
   readonly supportedDialogKinds?: ClaudeQueryOptions["supportedDialogKinds"];
   readonly allowDangerouslySkipPermissions?: boolean;
   readonly appendSystemPromptFileText?: string;
+  readonly hooks?: ClaudeQueryOptions["hooks"];
 }): ClaudeAgentSdkQueryOptions {
   const compiledSelection = compileClaudeModelSelection(input.modelSelection);
   const {
@@ -1370,6 +1372,7 @@
     ...(input.supportedDialogKinds === undefined
       ? {}
       : { supportedDialogKinds: input.supportedDialogKinds }),
+    ...(input.hooks === undefined ? {} : { hooks: input.hooks }),
     ...(input.settings?.binaryPath
       ? { pathToClaudeCodeExecutable: input.settings.binaryPath }
       : {}),
@@ -4300,6 +4303,40 @@
         const emitProviderEvent = (event: ProviderAdapter.ProviderAdapterV2Event) =>
           Queue.offer(events, event).pipe(Effect.asVoid);
 
+        // A root session can move without a T3 worktree handoff.
+        const trackedCwd = yield* Ref.make(session.cwd);
+        const followWorktreeCwdChange = Effect.fn("ClaudeAdapterV2.followWorktreeCwdChange")(
+          function* (nextCwd: string) {
+            const previousCwd = yield* Ref.get(trackedCwd);
+            if (nextCwd === previousCwd) return;
+            yield* Ref.set(trackedCwd, nextCwd);
+            const updatedAt = yield* DateTime.now;
+            const context = yield* Ref.get(activeTurn);
+            yield* emitProviderEvent({
+              type: "provider_session.updated",
+              driver: CLAUDE_PROVIDER,
+              providerSession: {
+                ...session,
+                cwd: nextCwd,
+                cwdChangeSource: context?.input.runtimePolicy.cwd ?? undefined,
+                updatedAt,
+              },
+            });
+          },
+        );
+
+        const onWorktreeTool: HookCallback = (hookInput) =>
+          runPromise(
+            Effect.gen(function* () {
+              // agent_type can also describe the root of a --agent session.
+              if (hookInput.agent_id) return {};
+              if (hookInput.hook_event_name === "PostToolUse") {
+                yield* followWorktreeCwdChange(hookInput.cwd);
+              }
+              return {};
+            }),
+          );
+
         // Reports one root request's usage as the current context. Usage
         // without input is a placeholder, never a measurement.
         const emitContextUsage = Effect.fnUntraced(function* (
@@ -9294,6 +9331,9 @@
             onUserDialog,
             supportedDialogKinds: ["resume_return"],
             ...(appendSystemPromptFileText === undefined ? {} : { appendSystemPromptFileText }),
+            hooks: {
+              PostToolUse: [{ matcher: "^(EnterWorktree|ExitWorktree)$", hooks: [onWorktreeTool] }],
+            },
           });
           const querySession = yield* queryRunner
             .open({
@@ -9313,6 +9353,21 @@
                   : Effect.void,
               ),
             );
+          // A replacement process starts at its requested cwd, not the previous process's move.
+          const openedCwd = queryOptions.cwd ?? process.cwd();
+          if (openedCwd !== (yield* Ref.get(trackedCwd))) {
+            yield* Ref.set(trackedCwd, openedCwd);
+            yield* emitProviderEvent({
+              type: "provider_session.updated",
+              driver: CLAUDE_PROVIDER,
+              providerSession: {
+                ...session,
+                cwd: openedCwd,
+                cwdChangeSource: undefined,
+                updatedAt: yield* DateTime.now,
+              },
+            });
+          }
           // Marked only after a successful open: a failed create must not
           // leave the runtime believing the native session exists, or the
           // retry would resume a session that was never created.
```

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

Status: modified; +2; −0

```diff
@@ -92,6 +92,8 @@
     type: Schema.Literal("checkpoint.capture"),
     runId: RunId,
     scopeId: CheckpointScopeId,
+    // Queue promotion can replace the provider thread's session before capture runs.
+    providerSessionId: Schema.optional(ProviderSessionId),
   }),
   Schema.Struct({
     type: Schema.Literal("scheduled-tasks.cleanup"),
```

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

Status: modified; +1; −0

```diff
@@ -448,6 +448,7 @@
                 threadId: effect.threadId,
                 runId: effect.request.runId,
                 scopeId: effect.request.scopeId,
+                providerSessionId: effect.request.providerSessionId,
               })
               .pipe(
                 Effect.mapError(
```

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

Status: modified; +112; −4

```diff
@@ -114,6 +114,7 @@
   emptyProjection,
   threadShellFromProjection,
   ProjectionStoreV2,
+  WORKSPACE_CHANGED_DETACH_REASON,
   type ProjectionRecordField,
   type ProjectionTimelinePage,
   type ProjectionTimelinePageOptions,
@@ -2743,6 +2744,76 @@
         cause: `Thread ${command.threadId} branch changed before the metadata update could be applied.`,
       });
     }
+    if (command.type === "thread.metadata.update" && command.expectedActiveRunId !== undefined) {
+      const current = yield* projectionStore
+        .getThreadShell(command.threadId)
+        .pipe(mapDispatchError(command));
+      let pendingSuccessor = false;
+      if (command.finalizedRunId !== undefined) {
+        const { runs } = yield* projectionStore
+          .getThreadRecords(command.threadId, ["runs"])
+          .pipe(mapDispatchError(command));
+        const active = runs.find((run) => run.id === current?.activeRunId);
+        const finalized = runs.find((run) => run.id === command.finalizedRunId);
+        // The capture effect still owns the thread's effect lane. Promotion can
+        // commit, but its provider-start effect cannot execute until capture returns.
+        pendingSuccessor =
+          active?.status === "starting" &&
+          active.startedAt === null &&
+          finalized?.checkpointId != null &&
+          (finalized.status === "completed" ||
+            finalized.status === "interrupted" ||
+            finalized.status === "cancelled") &&
+          latestExecutedRun(runs.filter((run) => run.status !== "starting"))?.id === finalized.id;
+      }
+      if (
+        (current?.activeRunId !== command.expectedActiveRunId ||
+          (command.finalizedRunId !== undefined &&
+            current?.activeRunId !== null &&
+            current?.activeRunId !== command.finalizedRunId)) &&
+        !pendingSuccessor
+      ) {
+        return yield* new OrchestratorDispatchError({
+          commandId: command.commandId,
+          commandType: command.type,
+          cause: `Thread ${command.threadId} active run changed before the metadata update could be applied.`,
+        });
+      }
+    }
+    let metadataBranchOverride: string | null | undefined = undefined;
+    if (command.type === "thread.metadata.update" && command.requireExclusiveWorktree === true) {
+      const targetWorktreePath =
+        command.worktreePath !== undefined ? command.worktreePath : thread.worktreePath;
+      const exclusive =
+        targetWorktreePath !== null &&
+        !(yield* projectionStore
+          .hasSiblingThreadWithWorktreePath({
+            threadId: command.threadId,
+            worktreePath: targetWorktreePath,
+          })
+          .pipe(mapDispatchError(command))) &&
+        // A project rooted at or inside this checkout shares it with that project's threads.
+        !(yield* projects.list().pipe(mapDispatchError(command))).some((project) => {
+          const relative = path.relative(targetWorktreePath, project.workspaceRoot);
+          return (
+            relative === "" ||
+            (relative !== ".." &&
+              !relative.startsWith(`..${path.sep}`) &&
+              !path.isAbsolute(relative))
+          );
+        });
+      if (!exclusive) {
+        if (command.worktreePath !== undefined) {
+          metadataBranchOverride = null;
+        } else {
+          return yield* new OrchestratorDispatchError({
+            commandId: command.commandId,
+            commandType: command.type,
+            cause: `Thread ${command.threadId}'s worktree is shared with another thread; not adopting the checked-out branch.`,
+          });
+        }
+      }
+    }
     if (command.type === "thread.metadata.update" && command.expectedEmpty === true) {
       const records = yield* projectionStore
         .getThreadRecords(command.threadId, ["runs"])
@@ -3205,7 +3276,11 @@
                     Date.parse(thread.limitRecovery.resetAt)
                 ? { snoozedUntil: null, snoozedAt: null, lastSnoozeWakeAt: now }
                 : {}),
+            ...(metadataBranchOverride !== undefined
+              ? { branch: metadataBranchOverride }
-            ...(command.branch === undefined ? {} : { branch: command.branch }),
+              : command.branch === undefined
+                ? {}
+                : { branch: command.branch }),
             ...(command.worktreePath === undefined ? {} : { worktreePath: command.worktreePath }),
             ...(command.linkedPullRequest === undefined
               ? {}
@@ -3553,6 +3628,35 @@
       payload: updatedThread,
     });
 
+    if (
+      command.type === "thread.metadata.update" &&
+      command.finalizedRunId !== undefined &&
+      command.worktreePath !== undefined &&
+      command.worktreePath !== thread.worktreePath
+    ) {
+      const { checkpointScopes } = yield* projectionStore
+        .getThreadRecords(command.threadId, ["checkpointScopes"])
+        .pipe(mapDispatchError(command));
+      const scope = checkpointScopes.find((candidate) => candidate.kind === "root_run");
+      if (scope !== undefined) {
+        const resolved = yield* runtimePolicy
+          .resolve({ thread: updatedThread, modelSelection: updatedThread.modelSelection })
+          .pipe(mapDispatchError(command));
+        // Capture is durable before location-follow. Refresh the shared root
+        // scope now so an already-promoted successor uses the destination.
+        yield* emit(
+          events,
+          command,
+        )({
+          type: "checkpoint-scope.created",
+          threadId: command.threadId,
+          providerInstanceId: updatedThread.providerInstanceId,
+          occurredAt: now,
+          payload: { ...scope, cwd: resolved.cwd ?? command.worktreePath ?? process.cwd() },
+        });
+      }
+    }
+
     if (command.type === "thread.metadata.update" && command.regenerateTitle === true) {
       yield* Ref.update(effects, (existing) => [
         ...existing,
@@ -3671,7 +3775,8 @@
         ? (providerContext?.providerSessions ?? []).map((session) => session.id)
         : command.type === "thread.metadata.update" &&
             command.worktreePath !== undefined &&
+            command.worktreePath !== thread.worktreePath &&
+            command.preserveProviderSession !== true
-            command.worktreePath !== thread.worktreePath
           ? (providerContext?.providerSessions ?? []).map((session) => session.id)
           : command.type === "thread.runtime-mode.set"
             ? (providerContext?.providerSessions ?? [])
@@ -3710,7 +3815,7 @@
                     : command.type === "thread.settle"
                       ? "Thread settled."
                       : command.type === "thread.metadata.update"
-                        ? "Workspace changed."
+                        ? WORKSPACE_CHANGED_DETACH_REASON
                         : command.type === "thread.runtime-mode.set"
                           ? "Runtime mode changed."
                           : "Provider or model selection changed.",
@@ -3729,7 +3834,7 @@
                     : command.type === "thread.settle"
                       ? "Thread settled."
                       : command.type === "thread.metadata.update"
-                        ? "Workspace changed."
+                        ? WORKSPACE_CHANGED_DETACH_REASON
                         : command.type === "thread.runtime-mode.set"
                           ? "Runtime mode changed."
                           : "Provider or model selection changed.",
@@ -9049,6 +9154,9 @@
             type: "checkpoint.capture",
             runId: run.id,
             scopeId,
+            ...(providerThread?.providerSessionId == null
+              ? {}
+              : { providerSessionId: providerThread.providerSessionId }),
           },
         } satisfies PendingOrchestrationEffectV2,
       ]);
```

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

Status: modified; +119; −2

```diff
@@ -98,6 +98,8 @@
   THREAD_HISTORY_PAGE_POLICY,
   OLDER_THREAD_USER_TURN_LIMIT,
 } from "./threadHistoryPaging.ts";
+
+export const WORKSPACE_CHANGED_DETACH_REASON = "Workspace changed.";
 
 export class ProjectionStoreApplyEventError extends Schema.TaggedError<ProjectionStoreApplyEventError>()(
   "ProjectionStoreApplyEventError",
@@ -404,6 +406,10 @@
     options: ProjectionTimelinePageOptions,
   ) => Effect.Effect<ProjectionTimelinePage, ProjectionStoreV2Error>;
   readonly getMessageCount: (threadId: ThreadId) => Effect.Effect<number, ProjectionStoreV2Error>;
+  readonly hasSiblingThreadWithWorktreePath: (input: {
+    readonly threadId: ThreadId;
+    readonly worktreePath: string;
+  }) => Effect.Effect<boolean, ProjectionStoreV2Error>;
   readonly getNextTurnItemOrdinal: (
     threadId: ThreadId,
   ) => Effect.Effect<number, ProjectionStoreV2Error>;
@@ -526,6 +532,17 @@
     threadId: ThreadId,
     targetInstanceId?: ProviderInstanceId,
   ) => Effect.Effect<ProjectionThreadProviderContext, ProjectionStoreV2Error>;
+  readonly getProviderSession: (
+    threadId: ThreadId,
+    providerSessionId: ProviderSessionId,
+  ) => Effect.Effect<
+    OrchestrationV2ThreadProjection["providerSessions"][number] | undefined,
+    ProjectionStoreV2Error
+  >;
+  readonly isProviderSessionShared: (
+    threadId: ThreadId,
+    providerSessionId: ProviderSessionId,
+  ) => Effect.Effect<boolean, ProjectionStoreV2Error>;
   readonly getRuntimeResponseContext: (
     threadId: ThreadId,
     requestId: RuntimeRequestId,
@@ -940,6 +957,10 @@
  */
 export interface ProjectionReplayState {
   readonly projections: Map<ThreadId, OrchestrationV2ThreadProjection>;
+  readonly providerSessions: Map<
+    ProviderSessionId,
+    OrchestrationV2ThreadProjection["providerSessions"][number]
+  >;
   readonly providerSessionThreadIds: Map<ProviderSessionId, ReadonlySet<ThreadId>>;
   readonly providerThreadInstanceIds: Map<ProviderThreadId, ProviderInstanceId>;
 }
@@ -947,6 +968,7 @@
 function makeProjectionReplayState(): ProjectionReplayState {
   return {
     projections: new Map(),
+    providerSessions: new Map(),
     providerSessionThreadIds: new Map(),
     providerThreadInstanceIds: new Map(),
   };
@@ -974,6 +996,12 @@
     }
   }
   state.projections.set(event.threadId, next);
+  if (
+    event.type === "provider-session.attached" ||
+    (event.type === "provider-session.updated" && state.providerSessions.has(event.payload.id))
+  ) {
+    state.providerSessions.set(event.payload.id, event.payload);
+  }
 
   switch (event.type) {
     case "provider-thread.updated": {
@@ -1030,6 +1058,9 @@
       boundThreadIds.delete(event.threadId);
       if (boundThreadIds.size === 0) {
         state.providerSessionThreadIds.delete(event.payload.providerSessionId);
+        if (event.payload.reason === WORKSPACE_CHANGED_DETACH_REASON) {
+          state.providerSessions.delete(event.payload.providerSessionId);
+        }
       } else {
         state.providerSessionThreadIds.set(event.payload.providerSessionId, boundThreadIds);
       }
@@ -2246,7 +2277,7 @@
                 updated_at,
                 payload_json
               )
-              VALUES (
+              SELECT
                 ${event.payload.id},
                 ${event.threadId},
                 ${event.payload.providerInstanceId},
@@ -2256,7 +2287,11 @@
                 ${event.payload.model},
                 ${stringField(payload, "updatedAt")},
                 ${payloadJson}
-              )
+              WHERE ${event.type === "provider-session.attached" ? 1 : 0} = 1
+                OR EXISTS (
+                  SELECT 1 FROM orchestration_v2_projection_provider_sessions
+                  WHERE provider_session_id = ${event.payload.id}
+                )
               ON CONFLICT(provider_session_id)
               DO UPDATE SET
                 thread_id = excluded.thread_id,
@@ -2285,6 +2320,17 @@
               WHERE provider_session_id = ${event.payload.providerSessionId}
                 AND thread_id = ${event.threadId}
             `;
+              // An explicit handoff revokes the old runtime location; queue promotion retains it.
+              if (event.payload.reason === WORKSPACE_CHANGED_DETACH_REASON) {
+                yield* sql`
+                DELETE FROM orchestration_v2_projection_provider_sessions
+                WHERE provider_session_id = ${event.payload.providerSessionId}
+                  AND NOT EXISTS (
+                    SELECT 1 FROM orchestration_v2_projection_provider_session_bindings
+                    WHERE provider_session_id = ${event.payload.providerSessionId}
+                  )
+              `;
+              }
               break;
             }
             case "provider-thread.updated": {
@@ -4879,6 +4925,35 @@
         )
         .pipe(Effect.mapError(controlReadError(threadId)));
 
+    const getProviderSession: ProjectionStoreV2Shape["getProviderSession"] = Effect.fn(
+      "ProjectionStore.getProviderSession",
+    )(
+      function* (_threadId, providerSessionId) {
+        const rows = yield* sql<PayloadRow>`
+          SELECT payload_json FROM orchestration_v2_projection_provider_sessions
+          WHERE provider_session_id = ${providerSessionId}
+        `;
+        return rows[0] === undefined
+          ? undefined
+          : yield* decodeProviderSessionPayload(rows[0].payload_json);
+      },
+      (effect, threadId) => effect.pipe(Effect.mapError(controlReadError(threadId))),
+    );
+
+    const isProviderSessionShared: ProjectionStoreV2Shape["isProviderSessionShared"] = (
+      threadId,
+      providerSessionId,
+    ) =>
+      sql<{ readonly shared: number }>`
+        SELECT EXISTS (
+          SELECT 1 FROM orchestration_v2_projection_provider_session_bindings
+          WHERE provider_session_id = ${providerSessionId} AND thread_id <> ${threadId}
+        ) AS shared
+      `.pipe(
+        Effect.map((rows) => rows[0]?.shared === 1),
+        Effect.mapError(controlReadError(threadId)),
+      );
+
     const getRuntimeResponseContext: ProjectionStoreV2Shape["getRuntimeResponseContext"] = (
       threadId,
       requestId,
@@ -5184,6 +5259,20 @@
         Effect.map((rows) => rows[0]?.count ?? 0),
         Effect.mapError(controlReadError(threadId)),
       );
+
+    const hasSiblingThreadWithWorktreePath: ProjectionStoreV2Shape["hasSiblingThreadWithWorktreePath"] =
+      (input) =>
+        sql<{ readonly thread_id: string }>`
+          SELECT thread_id FROM orchestration_v2_projection_threads
+          WHERE thread_id != ${input.threadId}
+            AND deleted_at IS NULL
+            AND archived_at IS NULL
+            AND json_extract(payload_json, '$.worktreePath') = ${input.worktreePath}
+          LIMIT 1
+        `.pipe(
+          Effect.map((rows) => rows.length > 0),
+          Effect.mapError(controlReadError(input.threadId)),
+        );
     const getNextTurnItemOrdinal: ProjectionStoreV2Shape["getNextTurnItemOrdinal"] = (threadId) =>
       sql<{ ordinal: number | null }>`SELECT MAX(ordinal) AS ordinal
         FROM orchestration_v2_projection_turn_items WHERE thread_id = ${threadId}`.pipe(
@@ -6495,6 +6584,8 @@
       getRuntimeRecoveryProjection,
       getRunningTurnContext,
       getThreadProviderContext,
+      getProviderSession,
+      isProviderSessionShared,
       getRuntimeResponseContext,
       getCheckpointContext,
       getCheckpointCaptureContext,
@@ -6505,6 +6596,7 @@
       hasUnpairedRunInterruptRequest,
       isNodeStored,
       getMessageCount,
+      hasSiblingThreadWithWorktreePath,
       getNextTurnItemOrdinal,
       getTurnItem,
       getThreadRecords,
@@ -6538,6 +6630,7 @@
           const result = yield* Ref.modify(replayState, (existing) => {
             const next: ProjectionReplayState = {
               projections: new Map(existing.projections),
+              providerSessions: new Map(existing.providerSessions),
               providerSessionThreadIds: new Map(existing.providerSessionThreadIds),
               providerThreadInstanceIds: new Map(existing.providerThreadInstanceIds),
             };
@@ -6792,6 +6885,18 @@
         Ref.get(replayState).pipe(
           Effect.map((state) => state.projections.get(threadId)?.messages.length ?? 0),
         ),
+      hasSiblingThreadWithWorktreePath: (input) =>
+        Ref.get(replayState).pipe(
+          Effect.map((state) =>
+            [...state.projections.values()].some(
+              ({ thread }) =>
+                thread.id !== input.threadId &&
+                thread.worktreePath === input.worktreePath &&
+                thread.deletedAt === null &&
+                thread.archivedAt === null,
+            ),
+          ),
+        ),
       getNextTurnItemOrdinal: (threadId) =>
         Ref.get(replayState).pipe(
           Effect.map(
@@ -6919,6 +7024,18 @@
           };
         },
       ),
+      getProviderSession: (_threadId, providerSessionId) =>
+        Ref.get(replayState).pipe(
+          Effect.map((state) => state.providerSessions.get(providerSessionId)),
+        ),
+      isProviderSessionShared: (threadId, providerSessionId) =>
+        Ref.get(replayState).pipe(
+          Effect.map((state) =>
+            [...(state.providerSessionThreadIds.get(providerSessionId) ?? [])].some(
+              (boundThreadId) => boundThreadId !== threadId,
+            ),
+          ),
+        ),
       getThreadProviderContext: Effect.fn("ProjectionStore.getThreadProviderContext")(
         function* (threadId, targetInstanceId) {
           const projection = (yield* Ref.get(replayState)).projections.get(threadId);
```

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

Status: modified; +76; −0

```diff
@@ -2180,6 +2180,82 @@
   }),
 );
 
+it.effect("ProviderSessionManagerV2 starts turns and releases at the cwd it last reported", () =>
+  Effect.gen(function* () {
+    const state = yield* Ref.make(emptyState);
+    const effect = Effect.gen(function* () {
+      const eventSink = yield* EventSink.EventSinkV2;
+      const idAllocator = yield* IdAllocator.IdAllocatorV2;
+      const manager = yield* ProviderSessionManager.ProviderSessionManagerV2;
+      const projectionStore = yield* ProjectionStore.ProjectionStoreV2;
+      const now = yield* DateTime.now;
+      const projectId = yield* idAllocator.allocate.project({
+        fixtureName: "provider-session-manager-moved-cwd",
+      });
+      const threadId = yield* idAllocator.allocate.thread({
+        fixtureName: "provider-session-manager-moved-cwd",
+        projectId,
+      });
+      const providerSessionId = yield* idAllocator.allocate.providerSession({
+        providerInstanceId: modelSelection.instanceId,
+        threadId,
+      });
+      yield* eventSink.write({
+        events: [yield* makeThreadCreatedEvent({ idAllocator, threadId, now })],
+      });
+      const runtime = yield* manager.open({
+        threadId,
+        providerSessionId,
+        modelSelection,
+        runtimePolicy,
+      });
+      const subscription = yield* runtime.subscribeEvents!;
+      const adapterQueue = (yield* Ref.get(state)).eventQueues.get(String(providerSessionId));
+      assert.isDefined(adapterQueue);
+      yield* Queue.offer(adapterQueue!, {
+        type: "provider_session.updated",
+        driver: CODEX_DRIVER,
+        providerSession: { ...runtime.providerSession, cwd: "/workspace/moved", updatedAt: now },
+      });
+      yield* subscription.events.pipe(Stream.take(1), Stream.runDrain);
+      const providerThread = makeProviderThread({ idAllocator, threadId, providerSessionId, now });
+      const runId = idAllocator.derive.run({ threadId, ordinal: 2 });
+      yield* runtime.startTurn({
+        appThread: (yield* projectionStore.getThreadProjection(threadId)).thread,
+        threadId,
+        runId,
+        runOrdinal: 2,
+        providerTurnOrdinal: 2,
+        attemptId: idAllocator.derive.runAttempt({ runId, attemptOrdinal: 1 }),
+        rootNodeId: idAllocator.derive.rootNode({ runId }),
+        providerThread,
+        message: {
+          createdBy: "user",
+          creationSource: "web",
+          messageId: yield* idAllocator.allocate.message({ threadId, ordinal: 2 }),
+          text: "Continue in the worktree.",
+          attachments: [],
+        },
+        modelSelection,
+        runtimePolicy: { ...runtimePolicy, cwd: "/workspace/moved" },
+      });
+      assert.equal(runtime.providerSession.cwd, "/workspace/moved");
+      assert.equal(
+        (yield* projectionStore.getThreadProjection(threadId)).providerSessions.at(-1)?.cwd,
+        "/workspace/moved",
+      );
+
+      yield* manager.close(providerSessionId);
+
+      const projection = yield* projectionStore.getThreadProjection(threadId);
+      assert.equal(projection.providerSessions.at(-1)?.status, "stopped");
+      assert.equal(projection.providerSessions.at(-1)?.cwd, "/workspace/moved");
+    });
+
+    yield* effect.pipe(Effect.provide(layerTest({ state, idleTimeoutMs: 60_000 })));
+  }),
+);
+
 const openSessionWithBackgroundCommand = (fixtureName: string) =>
   Effect.gen(function* () {
     const eventSink = yield* EventSink.EventSinkV2;
```

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

Status: modified; +12; −3

```diff
@@ -199,6 +199,8 @@
   readonly mcpCredentialIdByThread: ReadonlyMap<ThreadId, string>;
   readonly supportsMultipleProviderThreads: boolean;
   readonly runtime: ProviderAdapter.ProviderAdapterV2SessionRuntime;
+  // Runtime updates must survive attach, turn start, and release.
+  readonly providerSession: Ref.Ref<OrchestrationV2ProviderSession>;
   readonly exposedRuntime: ProviderAdapter.ProviderAdapterV2SessionRuntime;
   readonly eventSubscribers: Ref.Ref<
     ReadonlyMap<number, Queue.Queue<ProviderSessionEventSignal, Cause.Done>>
@@ -613,7 +615,7 @@
       }) {
         const now = yield* DateTime.now;
         const payload: OrchestrationV2ProviderSession = {
-          ...input.entry.runtime.providerSession,
+          ...(yield* Ref.get(input.entry.providerSession)),
           status: releaseStatusFor(input.reason),
           updatedAt: now,
           lastError:
@@ -1396,7 +1398,7 @@
                     runtime: entry.runtime,
                     threadIds: [input.threadId],
                     type: "provider-session.attached",
-                    payload: entry.runtime.providerSession,
+                    payload: yield* Ref.get(entry.providerSession),
                   }),
                 );
               }
@@ -1717,6 +1719,7 @@
         eventSubscribers: Ref.Ref<
           ReadonlyMap<number, Queue.Queue<ProviderSessionEventSignal, Cause.Done>>
         >,
+        providerSession: Ref.Ref<OrchestrationV2ProviderSession>,
       ): ProviderAdapter.ProviderAdapterV2SessionRuntime => {
         const providerSessionId = runtime.providerSessionId;
         const subscribeEvents = makeEventSubscription(eventSubscribers);
@@ -1732,6 +1735,9 @@
           });
         return {
           ...runtime,
+          get providerSession() {
+            return Ref.getUnsafe(providerSession);
+          },
           subscribeEvents,
           events: Stream.unwrap(
             subscribeEvents.pipe(Effect.map((subscription) => subscription.events)),
@@ -1908,6 +1914,7 @@
           if (current?.runtime !== entry.runtime) {
             return;
           }
+          yield* Ref.set(entry.providerSession, event.providerSession);
           yield* writeProviderSessionEvents({
             runtime: entry.runtime,
             threadIds: current.attachedThreadIds,
@@ -2218,7 +2225,8 @@
               const eventSubscribers = yield* Ref.make<
                 ReadonlyMap<number, Queue.Queue<ProviderSessionEventSignal, Cause.Done>>
               >(new Map());
+              const providerSession = yield* Ref.make(runtime.providerSession);
-              const exposedRuntime = decorateRuntime(runtime, eventSubscribers);
+              const exposedRuntime = decorateRuntime(runtime, eventSubscribers, providerSession);
               const now = yield* Clock.currentTimeMillis;
               const entry: LiveSessionEntry = {
                 attachedThreadIds: new Set([input.threadId]),
@@ -2231,6 +2239,7 @@
                   runtime.providerSession.capabilities.sessions
                     .supportsMultipleProviderThreadsPerSession,
                 runtime,
+                providerSession,
                 exposedRuntime,
                 eventSubscribers,
                 requestEventPermit: yield* Semaphore.make(1),
```

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

Status: modified; +4; −0

```diff
@@ -229,12 +229,16 @@
           getThreadHistoryPage: () => Effect.die("unused"),
           getTimelinePage: () => Effect.die("Unused timeline read"),
           getMessageCount: () => Effect.die("unused message count"),
+          hasSiblingThreadWithWorktreePath: () =>
+            Effect.die("unused hasSiblingThreadWithWorktreePath"),
           getNextTurnItemOrdinal: () => Effect.die("unused ordinal read"),
           getTurnItem: () => Effect.die("unused turn item read"),
           getThreadRecords: () => Effect.die("unused record read"),
           getRuntimeRequest: () => Effect.die("unused getRuntimeRequest"),
           getRunningTurnContext: () => Effect.die("unused getRunningTurnContext"),
           getThreadProviderContext: () => Effect.die("unused getThreadProviderContext"),
+          getProviderSession: () => Effect.die("unused getProviderSession"),
+          isProviderSessionShared: () => Effect.die("unused isProviderSessionShared"),
           getRuntimeResponseContext: () => Effect.die("unused getRuntimeResponseContext"),
           getPendingNativeUserInputs: () => Effect.die("unused getPendingNativeUserInputs"),
           getProviderControlContext: (_threadId, target) =>
```

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

Status: modified; +15; −6

```diff
@@ -1271,8 +1271,7 @@
                           }),
                         ),
                 }),
-            Layer.succeed(RunFinalizationService.RunFinalizationObserver, {
+            Layer.succeed(RunFinalizationService.RunTurnObserver, {
-              refresh: () => Effect.void,
               refreshAfterTurn: () => Ref.update(refreshes, (count) => count + 1),
             }),
           ),
@@ -2417,9 +2416,19 @@
       ["run_interrupt_result"],
     );
     assert.deepEqual(observed, ["run:interrupted", "pull-requests-refreshed"]);
+    // Finalization follows the session this run used, even if promotion replaces it.
     assert.deepEqual(
+      committedEffects.map((effect) => ({
+        type: effect.request.type,
+        providerSessionId:
+          effect.request.type === "checkpoint.capture" ? effect.request.providerSessionId : null,
+      })),
+      [
+        {
+          type: "checkpoint.capture",
+          providerSessionId: ProviderSessionId.make("session:hard-stop"),
+        },
-      committedEffects.map((effect) => effect.request.type),
-      ["checkpoint.capture"],
+      ],
     );
   }),
 );
@@ -2801,8 +2810,7 @@
             ingestNormalized: input.ingestNormalized ?? (() => Effect.succeed([])),
           }),
           ServerSettings.layerTest(),
+          Layer.succeed(RunFinalizationService.RunTurnObserver, {
-          Layer.succeed(RunFinalizationService.RunFinalizationObserver, {
-            refresh: () => Effect.void,
             refreshAfterTurn: () =>
               Ref.update(observed, (current) => [...current, "pull-requests-refreshed"]).pipe(
                 Effect.andThen(input.refreshAfterTurn ?? Effect.void),
@@ -2870,6 +2878,7 @@
         providerThread: {
           id: ids.providerThreadId,
           driver,
+          providerSessionId: ProviderSessionId.make(`session:${input.key}`),
         } as OrchestrationV2ProviderThread,
         attempt: {
           id: ids.attemptId,
```

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

Status: modified; +5; −2

```diff
@@ -610,7 +610,7 @@
     const serverSettings = yield* ServerSettings.ServerSettingsService;
     const mcpAppModelContext = yield* McpAppModelContext.McpAppModelContext;
     const turnItemPositions = yield* TurnItemPositionStore.TurnItemPositionStoreV2;
-    const finalizationObserver = yield* RunFinalizationService.RunFinalizationObserver;
+    const turnObserver = yield* RunFinalizationService.RunTurnObserver;
 
     const writeFinalRunEvents = Effect.fn("RunExecutionService.writeFinalRunEvents")(
       function* (input: {
@@ -738,6 +738,9 @@
                       type: "checkpoint.capture" as const,
                       runId: input.run.id,
                       scopeId: input.checkpointScope.id,
+                      ...(input.providerThread.providerSessionId === null
+                        ? {}
+                        : { providerSessionId: input.providerThread.providerSessionId }),
                     },
                   },
                 ]
@@ -873,7 +876,7 @@
       startRootRun: Effect.fn("RunExecutionService.startRootRun")(function* (input) {
         // Startup failure and stream shutdown can report the same attempt.
         const refreshAfterTurn = yield* Effect.cached(
+          turnObserver.refreshAfterTurn(input.appThread.projectId).pipe(
-          finalizationObserver.refreshAfterTurn(input.appThread.projectId).pipe(
             Effect.catchCause((cause) =>
               Effect.logWarning("failed to refresh pull requests after run termination", {
                 threadId: input.run.threadId,
```

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

Status: modified; +8; −0

```diff
@@ -1,12 +1,15 @@
 import { assert, it } from "@effect/vitest";
+import * as NodeServices from "@effect/platform-node/NodeServices";
 import { CheckpointScopeId, CommandId, GitCommandError, RunId, ThreadId } from "@t3tools/contracts";
 import * as Effect from "effect/Effect";
 import * as Layer from "effect/Layer";
 import * as Option from "effect/Option";
 
 import * as SqlitePersistence from "../persistence/Sqlite.ts";
+import * as ProjectService from "../project/ProjectService.ts";
 import * as PullRequestService from "../pullRequest/PullRequestService.ts";
 import * as ServerSettings from "../serverSettings.ts";
+import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
 import * as VcsStatusBroadcaster from "../vcs/VcsStatusBroadcaster.ts";
 import * as WorkspaceEntries from "../workspace/WorkspaceEntries.ts";
 import * as CheckpointCapture from "./CheckpointCaptureService.ts";
@@ -37,6 +40,7 @@
     cause: "checkpoint storage unavailable",
   });
   const projections = Layer.mock(ProjectionStore.ProjectionStoreV2)({
+    getThreadShell: () => Effect.succeed(null),
     getCheckpointContext: () =>
       Effect.succeed({
         runs: [],
@@ -50,6 +54,10 @@
         projections,
         Layer.mock(WorkspaceEntries.WorkspaceEntries)({ refresh: () => Effect.void }),
         Layer.mock(PullRequestService.PullRequestService)({ refreshAfterTurn: () => Effect.void }),
+        Layer.mock(ProjectService.ProjectService)({}),
+        Layer.mock(ThreadManagement.ThreadManagementService)({}),
+        Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({}),
+        NodeServices.layer,
         Layer.mock(VcsStatusBroadcaster.VcsStatusBroadcaster)({
           refreshLocalStatus: () =>
             Effect.gen(function* () {
```

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

Status: modified; +16; −1

```diff
@@ -1,4 +1,5 @@
 import { assert, it } from "@effect/vitest";
+import * as NodeServices from "@effect/platform-node/NodeServices";
 import {
   CheckpointScopeId,
   RunId,
@@ -8,13 +9,20 @@
 import * as Effect from "effect/Effect";
 import * as Layer from "effect/Layer";
 
+import * as ProjectService from "../project/ProjectService.ts";
 import * as PullRequestService from "../pullRequest/PullRequestService.ts";
 import * as VcsStatusBroadcaster from "../vcs/VcsStatusBroadcaster.ts";
+import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
 import * as WorkspaceEntries from "../workspace/WorkspaceEntries.ts";
 import * as CheckpointCapture from "./CheckpointCaptureService.ts";
 import * as ProjectionStore from "./ProjectionStore.ts";
 import * as RunFinalization from "./RunFinalizationService.ts";
+import * as ThreadManagementService from "./ThreadManagementService.ts";
 
+const unusedProjectService = Layer.mock(ProjectService.ProjectService)({
+  getById: () => Effect.die("not exercised by this scenario"),
+});
+
 it.effect("completes finalization when the post-checkpoint workspace refresh fails", () => {
   const threadId = ThreadId.make("thread_refresh_failure");
   const runId = RunId.make("run_refresh_failure");
@@ -46,7 +54,7 @@
                 cause: new Error("Git command timed out"),
               });
             }),
+          followWorktreeMove: () => Effect.void,
-          refreshAfterTurn: () => Effect.void,
         }),
       ),
     ),
@@ -125,9 +133,16 @@
             Effect.succeed({
               id: threadId,
               branch: scenario.branch,
+              worktreePath: null,
               activeRunId: scenario.activeRun === null ? null : RunId.make(scenario.activeRun),
             } as OrchestrationV2ThreadShell),
         }),
+        Layer.mock(ThreadManagementService.ThreadManagementService)({
+          dispatch: () => Effect.die("not exercised by this scenario"),
+        }),
+        unusedProjectService,
+        Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({}),
+        NodeServices.layer,
       ),
     ),
   );
```

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

Status: modified; +319; −9

```diff
@@ -1,15 +1,52 @@
+import { isTemporaryWorktreeBranch } from "@t3tools/shared/git";
+import {
+  CheckpointScopeId,
+  CommandId,
+  type OrchestrationV2ThreadShell,
+  ProjectId,
+  type ProviderSessionId,
+  RunId,
+  ThreadId,
+} from "@t3tools/contracts";
+import * as Cause from "effect/Cause";
-import { CheckpointScopeId, ProjectId, RunId, ThreadId } from "@t3tools/contracts";
 import * as Context from "effect/Context";
 import * as Effect from "effect/Effect";
+import * as FileSystem from "effect/FileSystem";
 import * as Layer from "effect/Layer";
+import * as Option from "effect/Option";
+import * as Path from "effect/Path";
 import * as Schema from "effect/Schema";
 
+import * as ProjectService from "../project/ProjectService.ts";
 import * as PullRequestService from "../pullRequest/PullRequestService.ts";
+import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
 import * as VcsStatusBroadcaster from "../vcs/VcsStatusBroadcaster.ts";
 import * as WorkspaceEntries from "../workspace/WorkspaceEntries.ts";
 import * as CheckpointCapture from "./CheckpointCaptureService.ts";
 import * as ProjectionStore from "./ProjectionStore.ts";
+import * as ThreadManagementService from "./ThreadManagementService.ts";
 
+function failureCategory(error: unknown): string {
+  if (
+    typeof error === "object" &&
+    error !== null &&
+    "_tag" in error &&
+    typeof error._tag === "string"
+  ) {
+    return error._tag;
+  }
+  if (error instanceof Error) return error.name;
+  return typeof error;
+}
+
+function causeCategory(cause: Cause.Cause<unknown>): string {
+  for (const reason of cause.reasons) {
+    if (Cause.isFailReason(reason)) return failureCategory(reason.error);
+    if (Cause.isDieReason(reason)) return failureCategory(reason.defect);
+  }
+  return "Interrupted";
+}
+
 export class RunFinalizationError extends Schema.TaggedError<RunFinalizationError>()(
   "RunFinalizationError",
   {
@@ -26,15 +63,55 @@
   { cwd: Schema.String, cause: Schema.Defect() },
 ) {}
 
-export class RunFinalizationObserver extends Context.Reference<{
+// Run execution is built before the finalization observer, which depends on the orchestrator.
+export class RunTurnObserver extends Context.Reference<{
   readonly refreshAfterTurn: (projectId: ProjectId) => Effect.Effect<void>;
+}>("t3/orchestration-v2/RunTurnObserver", {
+  defaultValue: () => ({ refreshAfterTurn: () => Effect.void }),
+}) {}
+
+export const layerTurnObserver = Layer.effect(
+  RunTurnObserver,
+  Effect.gen(function* () {
+    const pullRequests = yield* PullRequestService.PullRequestService;
+    return { refreshAfterTurn: pullRequests.refreshAfterTurn };
+  }),
+);
+
+export class RunFinalizationFollowError extends Schema.TaggedError<RunFinalizationFollowError>()(
+  "RunFinalizationFollowError",
+  {
+    threadId: ThreadId,
+    projectId: Schema.optional(ProjectId),
+    operation: Schema.Literals([
+      "read-thread",
+      "read-run",
+      "read-provider-session",
+      "read-session-sharing",
+      "read-project",
+      "detect-repository",
+      "resolve-repository-metadata",
+    ]),
+    cause: Schema.Defect(),
+  },
+) {}
+
+export class RunFinalizationObserver extends Context.Reference<{
   readonly refresh: (input: {
     readonly cwd: string;
     readonly threadId: ThreadId;
     readonly runId: RunId;
   }) => Effect.Effect<void, RunFinalizationRefreshError>;
+  readonly followWorktreeMove: (input: {
+    readonly threadId: ThreadId;
+    readonly runId: RunId;
+    readonly providerSessionId?: ProviderSessionId | undefined;
+  }) => Effect.Effect<void, RunFinalizationFollowError>;
 }>("t3/orchestration-v2/RunFinalizationObserver", {
+  defaultValue: () => ({
+    refresh: () => Effect.void,
-  defaultValue: () => ({ refresh: () => Effect.void, refreshAfterTurn: () => Effect.void }),
+    followWorktreeMove: () => Effect.void,
+  }),
 }) {}
 
 export class RunFinalizationService extends Context.Service<
@@ -44,6 +121,7 @@
       readonly threadId: ThreadId;
       readonly runId: RunId;
       readonly scopeId: CheckpointScopeId;
+      readonly providerSessionId?: ProviderSessionId | undefined;
     }) => Effect.Effect<void, RunFinalizationError>;
   }
 >()("t3/orchestration-v2/RunFinalizationService") {}
@@ -63,6 +141,15 @@
           (cause) => new RunFinalizationError({ ...input, operation: "capture-checkpoint", cause }),
         ),
       );
+    yield* observer.followWorktreeMove(input).pipe(
+      Effect.catchCause((cause) =>
+        Effect.logWarning("worktree-location follow failed", {
+          threadId: input.threadId,
+          runId: input.runId,
+          category: causeCategory(cause),
+        }),
+      ),
+    );
     const projection = yield* projections
       .getCheckpointContext(input.threadId)
       .pipe(
@@ -95,25 +182,248 @@
     const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
     const vcsStatus = yield* VcsStatusBroadcaster.VcsStatusBroadcaster;
     const projections = yield* ProjectionStore.ProjectionStoreV2;
+    const threads = yield* ThreadManagementService.ThreadManagementService;
+    const projects = yield* ProjectService.ProjectService;
+    const vcsDrivers = yield* VcsDriverRegistry.VcsDriverRegistry;
+    const fileSystem = yield* FileSystem.FileSystem;
+    const path = yield* Path.Path;
+
+    const followBranchDrift = Effect.fn("RunFinalizationService.followBranchDrift")(function* (
+      thread: OrchestrationV2ThreadShell,
+      cwd: string,
+      refName: string,
+      runId: RunId,
+    ) {
+      // Ignore the old checkpoint checkout after a location move.
-    const pullRequests = yield* PullRequestService.PullRequestService;
+      if (
+        thread.worktreePath === null ||
+        thread.worktreePath !== cwd ||
+        isTemporaryWorktreeBranch(refName)
+      ) {
+        return false;
+      }
+      return yield* threads
+        .dispatch({
+          type: "thread.metadata.update",
+          commandId: CommandId.make(`command:effect:worktree-branch-drift:${runId}`),
+          finalizedRunId: runId,
+          threadId: thread.id,
+          branch: refName,
+          expectedActiveRunId: thread.activeRunId,
+          expectedBranch: thread.branch,
+          expectedWorktreePath: thread.worktreePath,
+          requireExclusiveWorktree: true,
+        })
+        .pipe(
+          Effect.as(true),
+          Effect.catch((error) =>
+            Effect.logWarning("failed to follow worktree branch drift after run completion", {
+              threadId: thread.id,
+              previousBranch: thread.branch,
+              branch: refName,
+              category: failureCategory(error),
+            }).pipe(Effect.as(false)),
+          ),
+        );
+    });
+
+    const followWorktreeMove = Effect.fn("RunFinalizationService.followWorktreeMove")(function* ({
+      threadId,
+      runId,
+      providerSessionId,
+    }: {
+      readonly threadId: ThreadId;
+      readonly runId: RunId;
+      readonly providerSessionId?: ProviderSessionId | undefined;
+    }) {
+      // Queue promotion can replace the provider thread's session, so the capture
+      // effect carries the one this run used.
+      if (providerSessionId === undefined) return;
+      const thread = yield* projections
+        .getThreadShell(threadId)
+        .pipe(
+          Effect.mapError(
+            (cause) =>
+              new RunFinalizationFollowError({ threadId, operation: "read-thread", cause }),
+          ),
+        );
+      if (!thread) return;
+      const { runs } = yield* projections
+        .getThreadRecords(threadId, ["runs"], {
+          runIds:
+            thread.activeRunId === null || thread.activeRunId === runId
+              ? [runId]
+              : [runId, thread.activeRunId],
+        })
+        .pipe(
+          Effect.mapError(
+            (cause) => new RunFinalizationFollowError({ threadId, operation: "read-run", cause }),
+          ),
+        );
+      const run = runs.find((candidate) => candidate.id === runId);
+      if (run?.startedAt == null) return;
+      if (thread.activeRunId !== null && thread.activeRunId !== runId) {
+        const active = runs.find((candidate) => candidate.id === thread.activeRunId);
+        if (active?.status !== "starting" || active.startedAt !== null) return;
+      }
+      // Queue promotion can also detach the binding before this run follows its session.
+      const session = yield* projections.getProviderSession(threadId, providerSessionId).pipe(
+        Effect.mapError(
+          (cause) =>
+            new RunFinalizationFollowError({
+              threadId,
+              operation: "read-provider-session",
+              cause,
+            }),
+        ),
+      );
+      // An instance-wide session's cwd is where it opened, not where this thread is.
+      if (
+        session === undefined ||
+        session.capabilities.sessions.supportsMultipleProviderThreadsPerSession
+      ) {
+        return;
+      }
+      const liveCwd = session.cwd;
+      if (liveCwd === undefined) return;
+      // A shared session’s cwd does not identify which thread moved.
+      const shared = yield* projections.isProviderSessionShared(threadId, providerSessionId).pipe(
+        Effect.mapError(
+          (cause) =>
+            new RunFinalizationFollowError({
+              threadId,
+              operation: "read-session-sharing",
+              cause,
+            }),
+        ),
+      );
+      if (shared) return;
+      const project = yield* projects.getById(thread.projectId).pipe(
+        Effect.mapError(
+          (cause) =>
+            new RunFinalizationFollowError({
+              threadId,
+              projectId: thread.projectId,
+              operation: "read-project",
+              cause,
+            }),
+        ),
+      );
+      if (Option.isNone(project)) return;
+      const recordedCwd = thread.worktreePath ?? project.value.workspaceRoot;
+      // A failed turn can retain a native move. Only follow while its requested
+      // source is still recorded; a project repoint must invalidate that observation.
+      if (session.cwdChangeSource !== recordedCwd || liveCwd === recordedCwd) return;
+
+      const [observedRepository, projectRepository] = yield* Effect.all([
+        vcsDrivers.detect({ cwd: liveCwd }),
+        vcsDrivers.detect({ cwd: project.value.workspaceRoot }),
+      ]).pipe(
+        Effect.mapError(
+          (cause) =>
+            new RunFinalizationFollowError({ threadId, operation: "detect-repository", cause }),
+        ),
+      );
+      if (
+        observedRepository?.kind !== "git" ||
+        projectRepository?.kind !== "git" ||
+        !observedRepository.repository.metadataPath ||
+        !projectRepository.repository.metadataPath
+      ) {
+        return;
+      }
+      // Git common dirs identify a repository across worktrees and are relative to detection cwd.
+      const [observedCommonDir, projectCommonDir] = yield* Effect.all([
+        fileSystem.realPath(path.resolve(liveCwd, observedRepository.repository.metadataPath)),
+        fileSystem.realPath(
+          path.resolve(project.value.workspaceRoot, projectRepository.repository.metadataPath),
+        ),
+      ]).pipe(
+        Effect.mapError(
+          (cause) =>
+            new RunFinalizationFollowError({
+              threadId,
+              operation: "resolve-repository-metadata",
+              cause,
+            }),
+        ),
+      );
+      if (observedCommonDir !== projectCommonDir) return;
+      // Projects may select a repository subdirectory; sessions keep their factual cwd.
+      const location =
+        observedRepository.repository.rootPath === projectRepository.repository.rootPath
+          ? project.value.workspaceRoot
+          : observedRepository.repository.rootPath;
+      if (location === recordedCwd) return;
+      const newWorktreePath = location === project.value.workspaceRoot ? null : location;
+      const local = yield* vcsStatus.refreshLocalStatus(location).pipe(
+        Effect.catch((error) =>
+          Effect.logWarning("failed to read git status after a worktree move", {
+            threadId,
+            cwd: location,
+            category: failureCategory(error),
+          }).pipe(Effect.as(null)),
+        ),
+      );
+      const checkedOutBranch =
+        newWorktreePath !== null &&
+        local !== null &&
+        local.refName !== null &&
+        !isTemporaryWorktreeBranch(local.refName)
+          ? local.refName
+          : null;
+
+      yield* threads
+        .dispatch({
+          type: "thread.metadata.update",
+          commandId: CommandId.make(`command:effect:worktree-location-follow:${runId}`),
+          finalizedRunId: runId,
+          threadId,
+          worktreePath: newWorktreePath,
+          expectedWorktreePath: thread.worktreePath,
+          expectedBranch: thread.branch,
+          expectedActiveRunId: thread.activeRunId,
+          branch: checkedOutBranch,
+          // The provider already moved; an explicit client handoff still detaches.
+          preserveProviderSession: true,
+          ...(checkedOutBranch !== null ? { requireExclusiveWorktree: true } : {}),
+        })
+        .pipe(
+          Effect.catch((error) =>
+            Effect.logWarning("failed to follow the provider session into its new worktree", {
+              threadId,
+              worktreePath: newWorktreePath,
+              category: failureCategory(error),
+            }),
+          ),
+        );
+    });
+
     return {
-      refreshAfterTurn: pullRequests.refreshAfterTurn,
+      followWorktreeMove,
       refresh: Effect.fn("RunFinalizationService.refresh")(
         function* ({ cwd, threadId, runId }) {
           const [, local] = yield* Effect.all(
             [workspaceEntries.refresh(cwd), vcsStatus.refreshLocalStatus(cwd)],
             { concurrency: "unbounded" },
           );
-          if (local.refName === null || local.isDefaultRef) return;
+          if (local.refName === null) return;
           const thread = yield* projections.getThreadShell(threadId);
+          if (!thread) return;
-          if (!thread || thread.branch !== local.refName) return;
-          if (thread.activeRunId !== null && thread.activeRunId !== runId) return;
+          if (thread.branch !== local.refName) {
+            if (!(yield* followBranchDrift(thread, cwd, local.refName, runId))) return;
+          } else if (
+            local.isDefaultRef ||
+            (thread.activeRunId !== null && thread.activeRunId !== runId)
+          ) {
+            return;
+          }
           yield* vcsStatus.refreshPullRequestStatus(cwd).pipe(
             Effect.catch((error) =>
               Effect.logWarning("failed to refresh pull request status after run completion", {
                 threadId,
                 cwd,
-                detail: error.message,
+                category: failureCategory(error),
               }),
             ),
           );
```

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

Status: modified; +76; −5

```diff
@@ -31,6 +31,7 @@
 import * as Duration from "effect/Duration";
 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 Option from "effect/Option";
 import * as Ref from "effect/Ref";
@@ -145,12 +146,22 @@
     Layer.provide(layerReceipts),
   );
   const layerOutbox = EffectOutbox.layer.pipe(Layer.provide(layerDatabase));
+  // Model the checkout for tests that create a generated temporary branch.
+  let liveBranch = "t3/abcd1234";
   const createWorktree = vi.fn(
+    (...args: Parameters<GitWorkflow.GitWorkflowService["Service"]["createWorktree"]>) => {
+      const input = args[0];
+      liveBranch = input.newRefName ?? liveBranch;
+      return options.createWorktree
+        ? options.createWorktree(...args)
+        : Effect.succeed({
+            worktree: {
+              path: "/repo-worktrees/feature",
-    options.createWorktree ??
-      ((input) =>
-        Effect.succeed({
-          worktree: { path: "/repo-worktrees/feature", refName: input.newRefName, headSha: "abc" },
-        } as never)),
+              refName: input.newRefName,
+              headSha: "abc",
+            },
+          } as never);
+    },
   );
   const renameBranch = vi.fn(
     options.renameBranch ?? ((input) => Effect.succeed({ branch: input.newBranch })),
@@ -194,6 +205,16 @@
     Layer.mock(GitWorkflow.GitWorkflowService)({
       createWorktree,
       renameBranch,
+      localStatus: () =>
+        Effect.sync(() => ({
+          isRepo: true,
+          hasPrimaryRemote: true,
+          isDefaultRef: false,
+          refName: liveBranch,
+          hasWorkingTreeChanges: false,
+          workingTree: { files: [], insertions: 0, deletions: 0 },
+        })),
+      invalidateLocalStatus: () => Effect.void,
       fetchWorktreeBaseCommit:
         options.fetchWorktreeBaseCommit ??
         (() => Effect.succeed({ commitSha: "remote-main-sha", remoteRefName: "origin/main" })),
@@ -1107,6 +1128,56 @@
     ),
 );
 
+it.effect("preserves an observed agent branch switch during background naming", () =>
+  Effect.gen(function* () {
+    const namingStarted = yield* Deferred.make<Fiber.Fiber<unknown, unknown>>();
+    const continueNaming = yield* Deferred.make<void>();
+    let cachedBranch = "t3/abcd1234";
+    const harness = makeHarness({
+      generateBranchName: () =>
+        Effect.withFiber((fiber) => Deferred.succeed(namingStarted, fiber)).pipe(
+          Effect.andThen(Deferred.await(continueNaming)),
+          Effect.as({ branch: "generated-branch" }),
+        ),
+      git: {
+        invalidateLocalStatus: () =>
+          Effect.sync(() => {
+            cachedBranch = "feature/agent-chosen";
+          }),
+        localStatus: () =>
+          Effect.sync(() => ({
+            isRepo: true,
+            hasPrimaryRemote: true,
+            isDefaultRef: false,
+            refName: cachedBranch,
+            hasWorkingTreeChanges: false,
+            workingTree: { files: [], insertions: 0, deletions: 0 },
+          })),
+      },
+    });
+    yield* Effect.gen(function* () {
+      const launches = yield* ThreadLaunch.ThreadLaunchService;
+      const threads = yield* ThreadManagement.ThreadManagementService;
+      const launched = yield* launches.launch(
+        launchInput({
+          command: "command:launch:agent-switch",
+          thread: "thread:launch:agent-switch",
+          message: "Build the feature",
+          workspace: { type: "worktree", baseRef: "main", branch: "t3/abcd1234" },
+        }),
+      );
+      const naming = yield* Deferred.await(namingStarted);
+      yield* Deferred.succeed(continueNaming, undefined);
+      assert.isTrue(Exit.isSuccess(yield* Fiber.await(naming)));
+      assert.equal(harness.renameBranch.mock.calls.length, 0);
+      assert.equal(
+        (yield* threads.getThreadProjection(launched.threadId)).thread.branch,
+        "t3/abcd1234",
+      );
+    }).pipe(Effect.provide(harness.layer));
+  }),
+);
+
 it.effect("shows the fetch diagnosis when preparing a worktree from origin fails", () => {
   const detail =
     "Git could not authenticate with the remote. Check Git credentials or SSH access on the server, then retry.";
```

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

Status: modified; +54; −51

```diff
@@ -479,70 +479,73 @@
       ) {
         const oldBranch = branch;
         const worktreeCwd = worktreePath;
+        branchRename = yield* Effect.gen(function* () {
+          const { branch: newBranch, exactName } = yield* generateBranchNameFor(
+            worktreeCwd,
+            initialMessage,
+          );
+          // Naming can finish after the agent has checked out another branch.
+          yield* git.invalidateLocalStatus(worktreeCwd);
+          const local = yield* git.localStatus({ cwd: worktreeCwd });
+          if (local.refName !== oldBranch) return;
+          const renamed = yield* git.renameBranch({
+            cwd: worktreeCwd,
+            oldBranch,
+            newBranch,
+            ...(exactName ? { exactName: true } : {}),
+          });
+          yield* threads
+            .dispatch({
+              type: "thread.metadata.update",
+              commandId: CommandId.make(`${input.commandId}:branch-rename`),
+              threadId,
+              branch: renamed.branch,
+              worktreePath: worktreeCwd,
+              expectedBranch: oldBranch,
+              expectedWorktreePath: worktreeCwd,
+            })
+            .pipe(
+              Effect.catchCause((cause) =>
+                Effect.logWarning("Parent thread worktree branch update failed", {
+                  threadId,
+                  cause,
+                }),
+              ),
+            );
+          // Updating the parent first makes later subagents inherit the new name.
-        branchRename = yield* generateBranchNameFor(worktreeCwd, initialMessage).pipe(
-          Effect.flatMap(({ branch: newBranch, exactName }) =>
-            git.renameBranch({
-              cwd: worktreeCwd,
-              oldBranch,
-              newBranch,
+          // Re-read after each pass for children of subagents updated in that pass.
+          const visited = new Set([threadId]);
+          while (true) {
+            const snapshot = yield* threads.getShellSnapshot();
+            const sharing = [...snapshot.threads, ...snapshot.archivedThreads].filter(
+              (thread) =>
+                !visited.has(thread.id) &&
+                thread.worktreePath === worktreeCwd &&
+                thread.branch === oldBranch,
+            );
+            if (sharing.length === 0) break;
+            for (const thread of sharing) {
+              visited.add(thread.id);
-              ...(exactName ? { exactName: true } : {}),
-            }),
-          ),
-          Effect.flatMap((renamed) =>
-            Effect.gen(function* () {
               yield* threads
                 .dispatch({
                   type: "thread.metadata.update",
+                  commandId: CommandId.make(`${input.commandId}:branch-rename:${thread.id}`),
+                  threadId: thread.id,
-                  commandId: CommandId.make(`${input.commandId}:branch-rename`),
-                  threadId,
                   branch: renamed.branch,
-                  worktreePath: worktreeCwd,
                   expectedBranch: oldBranch,
                   expectedWorktreePath: worktreeCwd,
                 })
                 .pipe(
                   Effect.catchCause((cause) =>
-                    Effect.logWarning("Parent thread worktree branch update failed", {
+                    Effect.logWarning("Shared thread worktree branch update failed", {
-                      threadId,
+                      threadId: thread.id,
                       cause,
                     }),
                   ),
                 );
-              // Updating the parent first makes later subagents inherit the new name.
-              // Re-read after each pass for children of subagents updated in that pass.
-              const visited = new Set([threadId]);
-              while (true) {
-                const snapshot = yield* threads.getShellSnapshot();
-                const sharing = [...snapshot.threads, ...snapshot.archivedThreads].filter(
-                  (thread) =>
-                    !visited.has(thread.id) &&
-                    thread.worktreePath === worktreeCwd &&
-                    thread.branch === oldBranch,
-                );
-                if (sharing.length === 0) break;
-                for (const thread of sharing) {
-                  visited.add(thread.id);
-                  yield* threads
-                    .dispatch({
-                      type: "thread.metadata.update",
-                      commandId: CommandId.make(`${input.commandId}:branch-rename:${thread.id}`),
-                      threadId: thread.id,
-                      branch: renamed.branch,
-                      expectedBranch: oldBranch,
-                      expectedWorktreePath: worktreeCwd,
-                    })
-                    .pipe(
-                      Effect.catchCause((cause) =>
-                        Effect.logWarning("Shared thread worktree branch update failed", {
+            }
+          }
+        }).pipe(
-                          threadId: thread.id,
-                          cause,
-                        }),
-                      ),
-                    );
-                }
-              }
-            }),
-          ),
           Effect.catchCause((cause) =>
             Effect.logWarning("Thread worktree branch rename failed", {
               commandId: input.commandId,
```

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

Status: modified; +11; −1

```diff
@@ -54,9 +54,19 @@
 });
 
 export const dispatchCommand = Effect.fn("ThreadMessageIntake.dispatchCommand")(function* (
-  command: OrchestrationV2Command,
+  input: OrchestrationV2Command,
 ) {
   const threads = yield* ThreadManagement.ThreadManagementService;
+  // Ignore server-only metadata options even for callers that bypass schema decoding.
+  const command =
+    input.type === "thread.metadata.update"
+      ? {
+          ...input,
+          preserveProviderSession: undefined,
+          expectedActiveRunId: undefined,
+          finalizedRunId: undefined,
+        }
+      : input;
   if (command.type === "runtime-request.respond" && command.attachmentsByQuestionId) {
     const config = yield* ServerConfig.ServerConfig;
     const incomingByQuestionId = command.attachmentsByQuestionId;
```

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

Status: modified; +1505; −3

```diff
@@ -1,13 +1,29 @@
+import * as FileSystem from "effect/FileSystem";
+import * as CheckpointService from "./CheckpointService.ts";
+import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
+import * as RunFinalization from "./RunFinalizationService.ts";
+import * as RunExecution from "./RunExecutionService.ts";
+import * as ProviderEventIngestor from "./ProviderEventIngestor.ts";
+import * as TurnItemPositionStore from "./TurnItemPositionStore.ts";
+import * as McpAppModelContext from "../mcpApps/McpAppModelContext.ts";
+import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
+import { makeProviderFailure } from "@t3tools/provider-core/server/failure";
+import * as CheckpointCapture from "./CheckpointCaptureService.ts";
+import * as WorkspaceEntries from "../workspace/WorkspaceEntries.ts";
+import * as VcsStatusBroadcaster from "../vcs/VcsStatusBroadcaster.ts";
+import { dispatchCommand } from "./ThreadMessageIntake.ts";
 import { SourceControlProviderKind } from "@t3tools/contracts";
 import { limitRecoveryCommand } from "./UsageLimitRecoveryWorker.ts";
 import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
 import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts";
 import * as NodeServices from "@effect/platform-node/NodeServices";
 import { assert, it } from "@effect/vitest";
-import { vi } from "vite-plus/test";
+import { expect, vi } from "vite-plus/test";
 import {
   type ApplicationStoredEvent,
   CheckpointId,
+  CheckpointScopeId,
+  type Project,
   CheckpointRef,
   CommandId,
   ContextTransferId,
@@ -72,8 +88,10 @@
 import * as ProjectStore from "./ProjectStore.ts";
 import * as ProviderSessionManager from "./ProviderSessionManager.ts";
 import * as RuntimeLayer from "./runtimeLayer.ts";
+import * as RuntimePolicy from "./RuntimePolicy.ts";
 import { shellStreamItemFromThreadShell } from "./ShellStream.ts";
 import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";
+import { ClaudeProviderCapabilitiesV2 } from "./Adapters/ClaudeAdapterV2.ts";
 import * as ThreadManagementService from "./ThreadManagementService.ts";
 import * as ManagedProjectFolders from "../project/ManagedProjectFolders.ts";
 import * as ScheduledTaskService from "../scheduledTasks/ScheduledTaskService.ts";
@@ -155,16 +173,45 @@
   },
 } satisfies ProviderInstance;
 
+const claudeProviderInstance = {
+  ...providerInstance,
+  instanceId: ProviderInstanceId.make("claudeAgent"),
+  displayName: "Claude test",
+  driverKind: ProviderDriverKind.make("claudeAgent"),
+  continuationIdentity: {
+    driverKind: ProviderDriverKind.make("claudeAgent"),
+    continuationKey: "claude:test",
+  },
+  orchestrationAdapter: {
+    ...orchestrationAdapter,
+    instanceId: ProviderInstanceId.make("claudeAgent"),
+    driver: ProviderDriverKind.make("claudeAgent"),
+    getCapabilities: () => Effect.succeed(ClaudeProviderCapabilitiesV2),
+  },
+} satisfies ProviderInstance;
+const alternateClaudeProviderInstance = {
+  ...claudeProviderInstance,
+  instanceId: ProviderInstanceId.make("claude_alternate"),
+  orchestrationAdapter: {
+    ...claudeProviderInstance.orchestrationAdapter,
+    instanceId: ProviderInstanceId.make("claude_alternate"),
+  },
+} satisfies ProviderInstance;
+
 const layerTestProviderInstanceRegistry = Layer.succeed(
   ProviderInstanceRegistry.ProviderInstanceRegistry,
   {
     getInstance: (instanceId) =>
       Effect.succeed(
-        [providerInstance, alternateProviderInstance].find(
+        [providerInstance, alternateProviderInstance, claudeProviderInstance].find(
           (instance) => instanceId === instance.instanceId,
         ),
       ),
+    listInstances: Effect.succeed([
+      providerInstance,
+      alternateProviderInstance,
-    listInstances: Effect.succeed([providerInstance, alternateProviderInstance]),
+      claudeProviderInstance,
+    ]),
     listUnavailable: Effect.succeed([]),
     streamChanges: Stream.empty,
     subscribeChanges: Effect.never,
@@ -2029,6 +2076,493 @@
     }),
   );
 
+  it.effect(
+    "follows the root session through worktree moves while preserving explicit handoff",
+    () =>
+      Effect.forEach(["root", "subdirectory"] as const, (projectLayout) =>
+        Effect.gen(function* () {
+          const modelSelection = {
+            instanceId: claudeProviderInstance.instanceId,
+            model: "claude-sonnet-4-6",
+          };
+          const driver = claudeProviderInstance.driverKind;
+          const orchestrator = yield* Orchestrator.OrchestratorV2;
+          const eventSink = yield* EventSink.EventSinkV2;
+          const projections = yield* ProjectionStore.ProjectionStoreV2;
+          const projectId = ProjectId.make("follow-project");
+          const threadId = ThreadId.make("follow-thread");
+          const siblingId = ThreadId.make("follow-sibling");
+          const fs = yield* FileSystem.FileSystem;
+          const root = yield* fs.makeTempDirectoryScoped();
+          const projectRoot = projectLayout === "root" ? root : `${root}/src`;
+          const worktree = `${root}/.claude/worktrees/feature`;
+          const shared = `${root}/shared`;
+          const vcsProcess = yield* VcsProcess.VcsProcess;
+          const git = (args: ReadonlyArray<string>) =>
+            vcsProcess.run({ operation: "test.fixture", command: "git", args, cwd: root });
+          yield* git(["init", "-b", "main"]);
+          yield* git([
+            "-c",
+            "user.name=Test",
+            "-c",
+            "user.email=test@example.com",
+            "commit",
+            "--allow-empty",
+            "-m",
+            "fixture",
+          ]);
+          yield* git(["worktree", "add", "-b", "feature", worktree]);
+          yield* git(["worktree", "add", "-b", "sibling-branch", shared]);
+          for (const cwd of [root, projectRoot, worktree, shared]) {
+            yield* fs.makeDirectory(`${cwd}/src`, { recursive: true });
+          }
+          const now = yield* DateTime.now;
+          yield* seedProject({
+            projectId,
+            title: "Follow",
+            workspaceRoot: projectRoot,
+            defaultModelSelection: null,
+            createdAt: "2026-09-07T00:00:00.000Z",
+          });
+          let ordinal = 0;
+          const nextId = () => `follow-${++ordinal}`;
+          const create = (id: ThreadId, worktreePath: string | null) =>
+            orchestrator.dispatch({
+              type: "thread.create",
+              commandId: CommandId.make(nextId()),
+              threadId: id,
+              projectId,
+              createdBy: "user",
+              creationSource: "web",
+              title: "Follow worktree",
+              modelSelection,
+              runtimeMode: "full-access",
+              interactionMode: "default",
+              branch: null,
+              worktreePath,
+            });
+          yield* create(threadId, null);
+          yield* orchestrator.dispatch({
+            type: "message.dispatch",
+            commandId: CommandId.make(nextId()),
+            threadId,
+            createdBy: "user",
+            creationSource: "web",
+            messageId: MessageId.make(nextId()),
+            text: "Move worktree",
+            attachments: [],
+            dispatchMode: { type: "start_immediately" },
+          });
+          const initial = yield* orchestrator.getThreadProjection(threadId);
+          const run = initial.runs[0]!;
+          const providerThread = initial.providerThreads[0]!;
+          yield* TestClock.adjust("1 millis");
+          const observedAt = yield* DateTime.now;
+          const session = {
+            id: providerThread.providerSessionId!,
+            driver,
+            providerInstanceId: modelSelection.instanceId,
+            status: "ready" as const,
+            cwd: worktree,
+            model: modelSelection.model,
+            capabilities: ClaudeProviderCapabilitiesV2,
+            createdAt: now,
+            updatedAt: observedAt,
+            cwdChangeSource: projectRoot,
+            lastError: null,
+          };
+          const reportCwd = (cwd: string) =>
+            Effect.gen(function* () {
+              yield* TestClock.adjust("1 millis");
+              const updatedAt = yield* DateTime.now;
+              yield* eventSink.write({
+                events: [
+                  {
+                    id: EventId.make(nextId()),
+                    type: "provider-session.updated",
+                    threadId,
+                    occurredAt: updatedAt,
+                    payload: {
+                      ...session,
+                      cwd,
+                      updatedAt,
+                      cwdChangeSource:
+                        (yield* orchestrator.getThreadProjection(threadId)).thread.worktreePath ??
+                        projectRoot,
+                    },
+                  },
+                ],
+              });
+            });
+          yield* eventSink.write({
+            events: [
+              {
+                id: EventId.make(nextId()),
+                type: "provider-session.attached",
+                threadId,
+                occurredAt: observedAt,
+                payload: session,
+              },
+              {
+                id: EventId.make(nextId()),
+                type: "run.updated",
+                threadId,
+                runId: run.id,
+                occurredAt: observedAt,
+                payload: { ...run, status: "completed", startedAt: now, completedAt: observedAt },
+              },
+            ],
+          });
+          const otherSession = {
+            ...session,
+            id: ProviderSessionId.make("follow-other-session"),
+            cwd: `${root}/unrelated`,
+          };
+          yield* eventSink.write({
+            events: [
+              {
+                id: EventId.make(nextId()),
+                type: "provider-session.attached",
+                threadId,
+                occurredAt: now,
+                payload: otherSession,
+              },
+            ],
+          });
+          const refs = new Map<string, string | null>([
+            [root, "main"],
+            [worktree, "feature"],
+          ]);
+          const refreshed: string[] = [];
+          const observerLayer = RunFinalization.layerObserver.pipe(
+            Layer.provide(
+              Layer.mergeAll(
+                layerVcsDriverRegistryTest,
+                Layer.mock(WorkspaceEntries.WorkspaceEntries)({ refresh: () => Effect.void }),
+                Layer.mock(PullRequestService.PullRequestService)({
+                  refreshAfterTurn: () => Effect.void,
+                }),
+                Layer.mock(ProjectService.ProjectService)({
+                  getById: () =>
+                    Effect.succeed(
+                      Option.some({
+                        id: projectId,
+                        workspaceRoot: projectRoot,
+                      } as Project),
+                    ),
+                }),
+                Layer.mock(VcsStatusBroadcaster.VcsStatusBroadcaster)({
+                  refreshLocalStatus: (cwd) =>
+                    Effect.succeed({
+                      isRepo: true,
+                      hasPrimaryRemote: true,
+                      isDefaultRef:
+                        refs.get(cwd.endsWith("/src") ? cwd.slice(0, -4) : cwd) === "main",
+                      refName: refs.get(cwd.endsWith("/src") ? cwd.slice(0, -4) : cwd) ?? null,
+                      hasWorkingTreeChanges: false,
+                      workingTree: { files: [], insertions: 0, deletions: 0 },
+                    }),
+                  refreshPullRequestStatus: (cwd) =>
+                    Effect.sync(() => {
+                      refreshed.push(cwd);
+                      return null;
+                    }),
+                }),
+              ),
+            ),
+          );
+          const scopeId = CheckpointScopeId.make("follow-scope");
+          let checkpointCwd = projectRoot;
+          const finalizationLayer = RunFinalization.layer.pipe(
+            Layer.provide(observerLayer),
+            Layer.provide(
+              Layer.mock(CheckpointCapture.CheckpointCaptureServiceV2)({
+                execute: () => Effect.void,
+              }),
+            ),
+            Layer.provide(
+              Layer.succeed(ProjectionStore.ProjectionStoreV2, {
+                ...projections,
+                getCheckpointContext: () =>
+                  Effect.succeed({
+                    runs: [],
+                    checkpoints: [],
+                    checkpointScopes: [
+                      {
+                        id: scopeId,
+                        runId: run.id,
+                        kind: "root_run",
+                        cwd: checkpointCwd,
+                      },
+                    ],
+                  } as never),
+              }),
+            ),
+          );
+          yield* Effect.gen(function* () {
+            const finalization = yield* RunFinalization.RunFinalizationService;
+            const finish = () =>
+              Effect.gen(function* () {
+                const runId = RunId.make(nextId());
+                const completedAt = yield* DateTime.now;
+                yield* eventSink.write({
+                  events: [
+                    {
+                      id: EventId.make(nextId()),
+                      type: "run.updated",
+                      threadId,
+                      runId,
+                      occurredAt: completedAt,
+                      payload: {
+                        ...run,
+                        id: runId,
+                        ordinal,
+                        status: "completed",
+                        startedAt: now,
+                        completedAt,
+                      },
+                    },
+                  ],
+                });
+                yield* finalization.finalize({
+                  threadId,
+                  runId,
+                  scopeId,
+                  providerSessionId: providerThread.providerSessionId!,
+                });
+              });
+            const location = Effect.map(
+              orchestrator.getThreadProjection(threadId),
+              (projection) => ({
+                worktreePath: projection.thread.worktreePath,
+                branch: projection.thread.branch,
+                sessions: projection.providerSessions.map((value) => value.id),
+              }),
+            );
+            yield* finish();
+            yield* eventSink.write({
+              events: [
+                {
+                  id: EventId.make(nextId()),
+                  type: "provider-session.detached",
+                  threadId,
+                  occurredAt: now,
+                  payload: {
+                    providerSessionId: otherSession.id,
+                    detachedAt: now,
+                    reason: "Other session closed.",
+                  },
+                },
+              ],
+            });
+            expect.soft(yield* location, projectLayout).toEqual({
+              worktreePath: worktree,
+              branch: "feature",
+              sessions: [session.id],
+            });
+
+            checkpointCwd = worktree;
+            refs.set(worktree, "main");
+            yield* finish();
+            assert.equal((yield* location).branch, "main");
+            assert.deepEqual(refreshed, [worktree]);
+
+            yield* create(siblingId, null);
+            yield* reportCwd(`${projectRoot}/src`);
+            yield* finish();
+            expect.soft(yield* location, projectLayout).toEqual({
+              worktreePath: null,
+              branch: null,
+              sessions: [session.id],
+            });
+
+            assert.equal(
+              (yield* orchestrator.getThreadProjection(threadId)).providerSessions[0]?.cwd,
+              `${projectRoot}/src`,
+            );
+
+            expect
+              .soft(
+                (yield* orchestrator.getThreadProjection(threadId)).checkpointScopes.find(
+                  (scope) => scope.kind === "root_run",
+                )?.cwd,
+                projectLayout,
+              )
+              .toBe(projectRoot);
+
+            checkpointCwd = projectRoot;
+            refs.set(worktree, null);
+            yield* reportCwd(worktree);
+            yield* finish();
+            assert.isNull((yield* location).branch);
+            checkpointCwd = worktree;
+            refs.set(worktree, "t3code/0a1b2c3d");
+            yield* finish();
+            assert.isNull((yield* location).branch);
+            refs.set(worktree, "feature-again");
+            yield* finish();
+            assert.equal((yield* location).branch, "feature-again");
+
+            yield* dispatchCommand({
+              type: "thread.metadata.update",
+              commandId: CommandId.make(nextId()),
+              threadId: siblingId,
+              worktreePath: shared,
+            });
+            refs.set(shared, "sibling-branch");
+            yield* reportCwd(`${shared}/src`);
+            yield* finish();
+            expect.soft(yield* location, projectLayout).toEqual({
+              worktreePath: shared,
+              branch: null,
+              sessions: [session.id],
+            });
+            assert.equal(
+              (yield* orchestrator.getThreadProjection(threadId)).providerSessions[0]?.cwd,
+              `${shared}/src`,
+            );
+            checkpointCwd = shared;
+            yield* finish();
+            expect.soft((yield* location).branch, projectLayout).toBeNull();
+
+            yield* eventSink.write({
+              events: [
+                {
+                  id: EventId.make(nextId()),
+                  type: "provider-session.attached",
+                  threadId: siblingId,
+                  occurredAt: now,
+                  payload: { ...session, cwd: worktree, cwdChangeSource: shared },
+                },
+              ],
+            });
+            yield* finish();
+            expect.soft((yield* location).worktreePath, projectLayout).toBe(shared);
+
+            yield* dispatchCommand({
+              type: "thread.metadata.update",
+              commandId: CommandId.make(nextId()),
+              threadId: siblingId,
+              worktreePath: null,
+            });
+            yield* orchestrator.dispatch({
+              type: "message.dispatch",
+              commandId: CommandId.make(nextId()),
+              threadId,
+              createdBy: "user",
+              creationSource: "web",
+              messageId: MessageId.make(nextId()),
+              text: "Next turn",
+              attachments: [],
+              dispatchMode: { type: "start_immediately" },
+            });
+            yield* finish();
+            expect.soft((yield* location).worktreePath, projectLayout).toBe(shared);
+
+            const handoff = {
+              type: "thread.metadata.update" as const,
+              commandId: CommandId.make(nextId()),
+              threadId,
+              worktreePath: `${root}/handoff`,
+              preserveProviderSession: true,
+              expectedActiveRunId: RunId.make("forged-run"),
+              finalizedRunId: RunId.make("forged-finalized-run"),
+            };
+            yield* dispatchCommand(handoff);
+            expect.soft(yield* location, projectLayout).toEqual({
+              worktreePath: `${root}/handoff`,
+              branch: null,
+              sessions: [],
+            });
+          }).pipe(Effect.provide(Layer.fresh(finalizationLayer)));
+        }).pipe(
+          Effect.provide(
+            Layer.mergeAll(Layer.fresh(layerTest), VcsProcess.layer).pipe(
+              Layer.provideMerge(layerServerConfig.pipe(Layer.provideMerge(layerPlatformTest))),
+            ),
+          ),
+        ),
+      ),
+  );
+
+  it.effect("rejects stale branch observations and worktrees that become shared", () =>
+    Effect.gen(function* () {
+      const orchestrator = yield* Orchestrator.OrchestratorV2;
+      const projectId = ProjectId.make("follow-guards-project");
+      const threadId = ThreadId.make("follow-guards-thread");
+      const worktreePath = "/repo/guards";
+      // The sibling belongs to another project; sharing is per checkout, not per project.
+      for (const id of [threadId, ThreadId.make("follow-guards-sibling")]) {
+        yield* orchestrator.dispatch({
+          type: "thread.create",
+          commandId: CommandId.make(`create-${id}`),
+          threadId: id,
+          projectId: id === threadId ? projectId : ProjectId.make("follow-guards-other-project"),
+          createdBy: "user",
+          creationSource: "web",
+          title: "Guard",
+          modelSelection,
+          runtimeMode: "full-access",
+          interactionMode: "default",
+          branch: "feature",
+          worktreePath,
+        });
+        const failure = yield* orchestrator
+          .dispatch({
+            type: "thread.metadata.update",
+            commandId: CommandId.make(`update-${id}`),
+            threadId,
+            branch: "stale",
+            expectedBranch: id === threadId ? "old" : "feature",
+            expectedWorktreePath: worktreePath,
+            requireExclusiveWorktree: true,
+          })
+          .pipe(Effect.match({ onFailure: (error) => error, onSuccess: () => null }));
+        assert.instanceOf(failure, Orchestrator.OrchestratorDispatchError);
+        assert.equal((yield* orchestrator.getThreadProjection(threadId)).thread.branch, "feature");
+      }
+      for (const observation of [
+        { expectedActiveRunId: RunId.make("superseded-run") },
+        { expectedWorktreePath: "/repo/previous-worktree" },
+      ]) {
+        const staleRun = yield* orchestrator
+          .dispatch({
+            type: "thread.metadata.update",
+            commandId: CommandId.make(`stale-finalization-${Object.keys(observation)[0]}`),
+            threadId,
+            branch: "stale",
+            ...observation,
+          })
+          .pipe(Effect.match({ onFailure: (error) => error, onSuccess: () => null }));
+        assert.instanceOf(staleRun, Orchestrator.OrchestratorDispatchError);
+        assert.equal((yield* orchestrator.getThreadProjection(threadId)).thread.branch, "feature");
+      }
+      // A checkout holding another project's root keeps the move but not the branch.
+      yield* seedProject({
+        projectId: ProjectId.make("follow-guards-rooted-project"),
+        title: "Rooted",
+        workspaceRoot: "/repo/rooted/src",
+        defaultModelSelection: null,
+        createdAt: "2026-09-17T00:00:00.000Z",
+      });
+      yield* orchestrator.dispatch({
+        type: "thread.metadata.update",
+        commandId: CommandId.make("follow-guards-rooted"),
+        threadId,
+        worktreePath: "/repo/rooted",
+        branch: "rooted",
+        expectedBranch: "feature",
+        expectedWorktreePath: worktreePath,
+        requireExclusiveWorktree: true,
+      });
+      const rooted = (yield* orchestrator.getThreadProjection(threadId)).thread;
+      assert.deepEqual(
+        { worktreePath: rooted.worktreePath, branch: rooted.branch },
+        { worktreePath: "/repo/rooted", branch: null },
+      );
+    }),
+  );
+
   it.effect("keeps the branch pull request when linking another pull request", () =>
     Effect.gen(function* () {
       const orchestrator = yield* Orchestrator.OrchestratorV2;
@@ -6316,3 +6850,971 @@
     );
   },
 );
+
+const queueWorktreeInstances = [
+  claudeProviderInstance,
+  alternateClaudeProviderInstance,
+  providerInstance,
+] satisfies ProviderInstance[];
+const layerQueueWorktreeTest = Layer.mergeAll(
+  RunExecution.layer.pipe(
+    Layer.provide(
+      Layer.mergeAll(
+        McpAppModelContext.layerEmpty,
+        CheckpointService.layer.pipe(Layer.provide(IdAllocator.layer)),
+        RuntimeLayer.layerEventSink,
+        IdAllocator.layer,
+        TurnItemPositionStore.layer,
+        ProviderEventIngestor.layer.pipe(
+          Layer.provide(
+            Layer.mergeAll(
+              RuntimeLayer.layerEventSink,
+              IdAllocator.layer,
+              ProjectionStore.layer,
+              ThreadCommandExecutor.layer,
+              GitVcsDriver.layer,
+            ),
+          ),
+        ),
+      ),
+    ),
+  ),
+  RuntimeLayer.layer.pipe(Layer.provide(RuntimeLayer.layerProjectService)),
+  RuntimeLayer.layerProjectService,
+  RuntimeLayer.layerEventSink,
+  ProjectStore.layer,
+  ProjectionStore.layer,
+  EffectOutbox.layer,
+  ThreadCommandExecutor.layer,
+  RuntimePolicy.layerFromProjectStore.pipe(Layer.provide(ProjectStore.layer)),
+).pipe(
+  Layer.provide(McpSessionRegistryTestkit.layer),
+  Layer.provide(McpProviderSessions.layer),
+  Layer.provide(SqlitePersistence.layerMemory),
+  Layer.provide(layerCheckpointStoreTest),
+  Layer.provide(layerServerConfig),
+  Layer.provide(ServerSettings.layerTest()),
+  Layer.provide(
+    Layer.succeed(ProviderInstanceRegistry.ProviderInstanceRegistry, {
+      getInstance: (instanceId) =>
+        Effect.succeed(
+          queueWorktreeInstances.find((instance) => instance.instanceId === instanceId),
+        ),
+      listInstances: Effect.succeed(queueWorktreeInstances),
+      listUnavailable: Effect.succeed([]),
+      streamChanges: Stream.empty,
+      subscribeChanges: Effect.never,
+    }),
+  ),
+  Layer.provide(layerGitWorkflowTest),
+  Layer.provide(WorkspacePaths.layer),
+  Layer.provide(
+    Layer.mock(ProjectEnrichmentService.ProjectEnrichmentService)({
+      getAvailable: () =>
+        Effect.succeed({
+          repositoryIdentity: null,
+          faviconPath: null,
+          repositoryIdentityResolved: false,
+        }),
+      invalidate: () => Effect.void,
+    }),
+  ),
+  Layer.provide(layerPlatformTest),
+);
+const queueWorktreeVariants = (move: "enter" | "exit-remove") => [
+  ...(["waiting", "interrupted"] as const).flatMap((terminalStatus) =>
+    queueWorktreeInstances.map((instance) => ({ terminalStatus, instance, move })),
+  ),
+  ...(move === "enter"
+    ? [
+        {
+          move,
+          terminalStatus: "waiting" as const,
+          instance: claudeProviderInstance,
+          afterFailure: true,
+        },
+        ...(["root-repoint", "root-repoint-non-git", "root-repoint-linked"] as const).map(
+          (move) => ({
+            move,
+            terminalStatus: "waiting" as const,
+            instance: claudeProviderInstance,
+          }),
+        ),
+        {
+          move: "multiplex-project" as const,
+          terminalStatus: "waiting" as const,
+          instance: providerInstance,
+        },
+        {
+          move: "multiplex-worktree" as const,
+          terminalStatus: "waiting" as const,
+          instance: providerInstance,
+        },
+        {
+          move: "branch-switch" as const,
+          terminalStatus: "waiting" as const,
+          instance: providerInstance,
+        },
+        ...queueWorktreeInstances.slice(0, 2).map((instance) => ({
+          move: "handoff" as const,
+          terminalStatus: "waiting" as const,
+          instance,
+        })),
+      ]
+    : [
+        {
+          move,
+          terminalStatus: "waiting" as const,
+          instance: claudeProviderInstance,
+          modelSwitch: true,
+        },
+      ]),
+];
+
+it.layer(Layer.fresh(layerQueueWorktreeTest))("worktree moves with queued messages", (it) => {
+  it.effect.each(["enter", "exit-remove"] as const)(
+    "starts the queued successor at the destination after %s",
+    (direction) =>
+      Effect.forEach(queueWorktreeVariants(direction), (variant) =>
+        Effect.gen(function* () {
+          const { move, terminalStatus, instance } = variant;
+          const afterFailure = "afterFailure" in variant;
+          // Promotion replaces the moved session when the queued message changes model.
+          const modelSwitch = "modelSwitch" in variant;
+          const rootRepoint =
+            move === "root-repoint" ||
+            move === "root-repoint-non-git" ||
+            move === "root-repoint-linked";
+          const multiplex = move === "multiplex-project" || move === "multiplex-worktree";
+          const sourceInstance =
+            multiplex || move === "branch-switch" ? providerInstance : claudeProviderInstance;
+          const modelSelection = {
+            instanceId: sourceInstance.instanceId,
+            model: sourceInstance === providerInstance ? "gpt-5.4" : "claude-sonnet-4-6",
+          };
+          const driver = sourceInstance.driverKind;
+          const orchestrator = yield* Orchestrator.OrchestratorV2;
+          const eventSink = yield* EventSink.EventSinkV2;
+          const outbox = yield* EffectOutbox.EffectOutboxV2;
+          const runtimePolicy = yield* RuntimePolicy.RuntimePolicyV2;
+          const projections = yield* ProjectionStore.ProjectionStoreV2;
+          const projects = yield* ProjectService.ProjectService;
+          const fs = yield* FileSystem.FileSystem;
+          const root = yield* fs.makeTempDirectoryScoped();
+          const worktree = `${root}/.claude/worktrees/feature`;
+          const vcsProcess = yield* VcsProcess.VcsProcess;
+          const git = (args: ReadonlyArray<string>, allowNonZeroExit = false) =>
+            vcsProcess.run({
+              operation: "test.fixture",
+              command: "git",
+              args,
+              cwd: root,
+              allowNonZeroExit,
+            });
+          yield* git(["init", "-b", "main"]);
+          yield* git([
+            "-c",
+            "user.name=Test",
+            "-c",
+            "user.email=test@example.com",
+            "commit",
+            "--allow-empty",
+            "-m",
+            "fixture",
+          ]);
+          yield* git(["worktree", "add", "-b", "feature", worktree]);
+          const source =
+            move === "exit-remove" ||
+            move === "branch-switch" ||
+            move === "multiplex-worktree" ||
+            move === "root-repoint-linked"
+              ? worktree
+              : root;
+          const destination =
+            multiplex || rootRepoint ? source : move === "exit-remove" ? root : worktree;
+          const openerCwd =
+            move === "multiplex-project" || (rootRepoint && move !== "root-repoint-linked")
+              ? yield* fs.makeTempDirectoryScoped()
+              : root;
+          if (move === "multiplex-project" || move === "root-repoint")
+            yield* git(["-C", openerCwd, "init", "-b", "opener-branch"]);
+          const threadId = ThreadId.make(`queue-worktree-${move}`);
+          const projectId = ProjectId.make(`queue-worktree-project-${move}`);
+          const now = yield* DateTime.now;
+          let ordinal = 0;
+          const nextId = () => `${threadId}:${++ordinal}`;
+          yield* seedProject({
+            projectId,
+            title: "Queued worktree move",
+            workspaceRoot: rootRepoint ? openerCwd : root,
+            defaultModelSelection: null,
+            createdAt: DateTime.formatIso(now),
+          });
+          yield* orchestrator.dispatch({
+            type: "thread.create",
+            commandId: CommandId.make(nextId()),
+            threadId,
+            projectId,
+            createdBy: "user",
+            creationSource: "web",
+            title: "Queued worktree move",
+            modelSelection,
+            runtimeMode: "full-access",
+            interactionMode: "default",
+            branch: rootRepoint || source === root ? null : "feature",
+            worktreePath: rootRepoint || source === root ? null : worktree,
+          });
+          const send = (text: string) =>
+            orchestrator.dispatch({
+              type: "message.dispatch",
+              commandId: CommandId.make(nextId()),
+              threadId,
+              createdBy: "user",
+              creationSource: "web",
+              messageId: MessageId.make(nextId()),
+              text,
+              ...(text === "Queued successor"
+                ? {
+                    modelSelection: {
+                      instanceId: instance.instanceId,
+                      model:
+                        instance.driverKind === claudeProviderInstance.driverKind
+                          ? modelSwitch
+                            ? "claude-opus-4-6"
+                            : "claude-sonnet-4-6"
+                          : "gpt-5.4",
+                    },
+                  }
+                : {}),
+              attachments: [],
+              dispatchMode: { type: "queue_after_active" },
+            });
+          if (rootRepoint) {
+            yield* send("Open retained session");
+            const opened = yield* orchestrator.getThreadProjection(threadId);
+            const openerRun = opened.runs[0]!;
+            const openerThread = opened.providerThreads[0]!;
+            yield* eventSink.write({
+              events: [
+                {
+                  id: EventId.make(nextId()),
+                  type: "provider-session.attached",
+                  threadId,
+                  occurredAt: now,
+                  payload: {
+                    id: openerThread.providerSessionId!,
+                    driver,
+                    providerInstanceId: modelSelection.instanceId,
+                    status: "ready",
+                    cwd: openerCwd,
+                    ...(move === "root-repoint-linked" ? { cwdChangeSource: openerCwd } : {}),
+                    model: modelSelection.model,
+                    capabilities: yield* sourceInstance.orchestrationAdapter.getCapabilities(),
+                    createdAt: now,
+                    updatedAt: now,
+                    lastError: null,
+                  },
+                },
+                {
+                  id: EventId.make(nextId()),
+                  type: "run.updated",
+                  threadId,
+                  runId: openerRun.id,
+                  occurredAt: now,
+                  payload: { ...openerRun, status: "completed", startedAt: now, completedAt: now },
+                },
+              ],
+            });
+            yield* TestClock.adjust("1 millis");
+            const updated = yield* projects.update({
+              projectId,
+              commandId: CommandId.make(nextId()),
+              workspaceRoot: source,
+            });
+            assert.equal(updated.workspaceRoot, source);
+            assert.equal(
+              (yield* orchestrator.getThreadProjection(threadId)).providerSessions[0]?.cwd,
+              openerCwd,
+            );
+          }
+          let failedMoveAt: DateTime.Utc | undefined;
+          if (afterFailure) {
+            yield* send("Move then fail");
+            const opening = yield* orchestrator.getThreadProjection(threadId);
+            const run = { ...opening.runs[0]!, status: "running" as const, startedAt: now };
+            const providerThread = {
+              ...opening.providerThreads[0]!,
+              nativeThreadRef: {
+                driver,
+                nativeId: `${threadId}:native`,
+                strength: "strong" as const,
+              },
+            };
+            const node = opening.nodes.find((value) => value.id === run.rootNodeId)!;
+            const scope = opening.checkpointScopes.find(
+              (value) => value.id === node.checkpointScopeId,
+            )!;
+            const attempt = {
+              ...opening.attempts.find((value) => value.id === run.activeAttemptId)!,
+              providerTurnId: ProviderTurnId.make(nextId()),
+              startedAt: now,
+            };
+            yield* TestClock.adjust("1 millis");
+            failedMoveAt = yield* DateTime.now;
+            const session = {
+              id: providerThread.providerSessionId!,
+              driver,
+              providerInstanceId: modelSelection.instanceId,
+              status: "ready" as const,
+              cwd: destination,
+              cwdChangeSource: source,
+              model: modelSelection.model,
+              capabilities: ClaudeProviderCapabilitiesV2,
+              createdAt: now,
+              updatedAt: failedMoveAt,
+              lastError: null,
+            };
+            yield* eventSink.write({
+              events: [
+                {
+                  id: EventId.make(nextId()),
+                  type: "run.updated",
+                  threadId,
+                  runId: run.id,
+                  occurredAt: now,
+                  payload: run,
+                },
+                {
+                  id: EventId.make(nextId()),
+                  type: "provider-session.attached",
+                  threadId,
+                  occurredAt: now,
+                  payload: {
+                    ...session,
+                    cwd: source,
+                    cwdChangeSource: undefined,
+                    updatedAt: now,
+                  },
+                },
+              ],
+            });
+            const events = yield* Queue.unbounded<ProviderAdapter.ProviderAdapterV2Event>();
+            const closed = yield* Deferred.make<void>();
+            const execution = yield* RunExecution.RunExecutionServiceV2;
+            yield* execution.startRootRun({
+              commandId: CommandId.make(nextId()),
+              appThread: opening.thread,
+              providerSessionId: session.id,
+              run,
+              rootNode: node,
+              checkpointScope: scope,
+              providerThread,
+              attempt,
+              attemptId: attempt.id,
+              providerTurnOrdinal: 1,
+              modelSelection,
+              runtimePolicy: yield* runtimePolicy.resolve({
+                thread: opening.thread,
+                modelSelection,
+              }),
+              message: {
+                messageId: opening.messages[0]!.id,
+                createdBy: "user",
+                creationSource: "web",
+                text: "Move then fail",
+                attachments: [],
+              },
+              session: {
+                instanceId: modelSelection.instanceId,
+                driver,
+                providerSessionId: session.id,
+                providerSession: session,
+                events: Stream.fromQueue(events),
+                subscribeEvents: Effect.succeed({
+                  events: Stream.fromQueue(events),
+                  close: Deferred.succeed(closed, undefined).pipe(Effect.asVoid),
+                }),
+                ensureThread: () => Effect.succeed(providerThread),
+                resumeThread: () => Effect.succeed(providerThread),
+                startTurn: () =>
+                  Effect.gen(function* () {
+                    // The manager persists native cwd before delivering the terminal to the run.
+                    yield* eventSink
+                      .write({
+                        events: [
+                          {
+                            id: EventId.make(nextId()),
+                            type: "provider-session.updated",
+                            threadId,
+                            occurredAt: session.updatedAt,
+                            payload: session,
+                          },
+                        ],
+                      })
+                      .pipe(Effect.orDie);
+                    yield* Queue.offer(events, {
+                      type: "turn.terminal",
+                      driver,
+                      providerThreadId: providerThread.id,
+                      providerTurnId: attempt.providerTurnId,
+                      runOrdinal: run.ordinal,
+                      failureItemOrdinal: 101,
+                      status: "failed",
+                      failure: makeProviderFailure({ class: "provider_error" }),
+                      threadDisposition: "reusable",
+                    });
+                  }),
+                steerTurn: () => Effect.die("unused steer"),
+                interruptTurn: () => Effect.die("unused interrupt"),
+                respondToRuntimeRequest: () => Effect.die("unused request"),
+                readThreadSnapshot: () => Effect.die("unused snapshot"),
+                rollbackThread: () => Effect.die("unused rollback"),
+                forkThread: () => Effect.die("unused fork"),
+              },
+            });
+            yield* Deferred.await(closed);
+            const failed = yield* orchestrator.getThreadProjection(threadId);
+            assert.equal(failed.runs[0]?.status, "failed");
+            assert.equal(failed.providerThreads[0]?.status, "idle");
+            assert.equal(failed.providerSessions[0]?.id, session.id);
+            assert.isEmpty(
+              yield* outbox.listByCommandId(
+                CommandId.make(`command:effect:checkpoint.capture:${run.id}`),
+              ),
+            );
+            assert.isFalse(failed.checkpoints.some((checkpoint) => checkpoint.runId === run.id));
+            assert.equal(failed.thread.worktreePath, opening.thread.worktreePath);
+          }
+          yield* TestClock.adjust("1 millis");
+          const startedAt = yield* DateTime.now;
+          yield* send("Move worktree");
+          if (move !== "handoff") yield* send("Queued successor");
+          const before = yield* orchestrator.getThreadProjection(threadId);
+          const run = before.runs[rootRepoint || afterFailure ? 1 : 0]!;
+          const node = before.nodes.find((value) => value.id === run.rootNodeId)!;
+          const providerThread = before.providerThreads.find(
+            (value) => value.id === run.providerThreadId,
+          )!;
+          const scope = before.checkpointScopes.find(
+            (value) => value.id === node.checkpointScopeId,
+          )!;
+          assert.equal(scope.cwd, source);
+          yield* TestClock.adjust("1 millis");
+          const observedAt = yield* DateTime.now;
+          const sessionEvent = {
+            id: EventId.make(nextId()),
+            type: "provider-session.attached" as const,
+            threadId,
+            occurredAt: observedAt,
+            payload: {
+              id: providerThread.providerSessionId!,
+              driver,
+              providerInstanceId: modelSelection.instanceId,
+              status: "ready" as const,
+              cwd: multiplex || rootRepoint ? openerCwd : move === "handoff" ? source : destination,
+              model: modelSelection.model,
+              capabilities: yield* sourceInstance.orchestrationAdapter.getCapabilities(),
+              createdAt: now,
+              // Only native movement refreshes the adapter session observation.
+              updatedAt:
+                failedMoveAt ?? (rootRepoint || multiplex || move === "handoff" ? now : observedAt),
+              ...(move === "root-repoint-linked"
+                ? { cwdChangeSource: openerCwd }
+                : rootRepoint || multiplex || move === "handoff"
+                  ? {}
+                  : { cwdChangeSource: source }),
+              lastError: null,
+            },
+          };
+          if (multiplex) {
+            const openerProjectId = ProjectId.make(`${threadId}:opener-project`);
+            const openerThreadId = ThreadId.make(`${threadId}:opener`);
+            yield* seedProject({
+              projectId: openerProjectId,
+              title: "First session opener",
+              workspaceRoot: openerCwd,
+              defaultModelSelection: null,
+              createdAt: DateTime.formatIso(now),
+            });
+            yield* orchestrator.dispatch({
+              type: "thread.create",
+              commandId: CommandId.make(nextId()),
+              threadId: openerThreadId,
+              projectId: openerProjectId,
+              createdBy: "user",
+              creationSource: "web",
+              title: "First session opener",
+              modelSelection,
+              runtimeMode: "full-access",
+              interactionMode: "default",
+              branch: null,
+              worktreePath: null,
+            });
+            // The instance-wide session keeps its original cwd after the opener detaches.
+            yield* eventSink.write({
+              events: [
+                { ...sessionEvent, id: EventId.make(nextId()), threadId: openerThreadId },
+                {
+                  id: EventId.make(nextId()),
+                  type: "provider-session.detached",
+                  threadId: openerThreadId,
+                  occurredAt: now,
+                  payload: {
+                    providerSessionId: providerThread.providerSessionId!,
+                    detachedAt: now,
+                    reason: "First thread settled.",
+                  },
+                },
+              ],
+            });
+            assert.isEmpty(
+              (yield* orchestrator.getThreadProjection(openerThreadId)).providerSessions,
+            );
+          }
+          if (move === "handoff") {
+            yield* eventSink.write({
+              events: [
+                sessionEvent,
+                {
+                  id: EventId.make(nextId()),
+                  type: "run.updated",
+                  threadId,
+                  runId: run.id,
+                  occurredAt: now,
+                  payload: { ...run, status: "running", startedAt },
+                },
+              ],
+            });
+            yield* dispatchCommand({
+              type: "thread.metadata.update",
+              commandId: CommandId.make(nextId()),
+              threadId,
+              worktreePath: worktree,
+              branch: "feature",
+              expectedWorktreePath: null,
+            });
+            const handedOff = yield* orchestrator.getThreadProjection(threadId);
+            assert.equal(handedOff.thread.worktreePath, worktree);
+            assert.equal(handedOff.runs.find((value) => value.id === run.id)?.status, "running");
+            assert.isEmpty(handedOff.providerSessions);
+            yield* send("Queued successor");
+          }
+          const queued = (yield* orchestrator.getThreadProjection(threadId)).runs[
+            rootRepoint || afterFailure ? 2 : 1
+          ]!;
+          assert.equal(queued.status, "queued");
+          yield* outbox.cancelUnsettled({
+            threadId,
+            effectTypes: ["provider-turn.start"],
+            reason: "The fixture supplies the first provider turn's terminal events.",
+          });
+          if (move === "exit-remove") yield* git(["worktree", "remove", worktree]);
+          if (move === "branch-switch") {
+            yield* git(["-C", worktree, "switch", "-c", "feature-2"]);
+          }
+          const promoted = yield* Deferred.make<void>();
+          const afterSequence = yield* orchestrator.getThreadEventSequence(threadId);
+          yield* eventSink.stream({ threadId, afterSequence }).pipe(
+            Stream.runForEach((stored) =>
+              stored.event.type === "run.updated" &&
+              stored.event.payload.id === queued.id &&
+              stored.event.payload.status === "starting"
+                ? Deferred.succeed(promoted, undefined)
+                : Effect.void,
+            ),
+            Effect.forkScoped,
+          );
+          yield* eventSink.writeWithEffects({
+            events: [
+              ...(afterFailure
+                ? []
+                : move === "handoff" || rootRepoint
+                  ? [
+                      {
+                        ...sessionEvent,
+                        id: EventId.make(nextId()),
+                        type: "provider-session.updated" as const,
+                      },
+                    ]
+                  : [sessionEvent]),
+              {
+                id: EventId.make(nextId()),
+                type: "provider-thread.updated",
+                threadId,
+                occurredAt: observedAt,
+                payload: {
+                  ...providerThread,
+                  nativeThreadRef: { driver, nativeId: `${threadId}:native`, strength: "strong" },
+                  status: "idle",
+                },
+              },
+              {
+                id: EventId.make(nextId()),
+                type: "run-attempt.updated",
+                threadId,
+                runId: run.id,
+                occurredAt: observedAt,
+                payload: {
+                  ...before.attempts.find((value) => value.id === run.activeAttemptId)!,
+                  nativeThreadId: `${threadId}:native`,
+                  startedAt,
+                  status: terminalStatus === "waiting" ? "completed" : "interrupted",
+                  completedAt: observedAt,
+                },
+              },
+              {
+                id: EventId.make(nextId()),
+                type: "run.updated",
+                threadId,
+                runId: run.id,
+                occurredAt: observedAt,
+                payload: {
+                  ...run,
+                  status: terminalStatus,
+                  startedAt,
+                  ...(terminalStatus === "interrupted" ? { completedAt: observedAt } : {}),
+                },
+              },
+              {
+                id: EventId.make(nextId()),
+                type: "node.updated",
+                threadId,
+                runId: run.id,
+                occurredAt: observedAt,
+                payload: { ...node, status: terminalStatus },
+              },
+            ],
+            effects: [
+              {
+                id: `effect:checkpoint.capture:${run.id}`,
+                commandId: CommandId.make(`command:effect:checkpoint.capture:${run.id}`),
+                threadId,
+                request: {
+                  type: "checkpoint.capture",
+                  runId: run.id,
+                  scopeId: scope.id,
+                  providerSessionId: providerThread.providerSessionId!,
+                },
+              },
+            ],
+          });
+          const checkpointCwds: string[] = [];
+          const captureLayer = CheckpointCapture.layer.pipe(
+            Layer.provide(
+              CheckpointService.layer.pipe(
+                Layer.provide(
+                  Layer.mock(CheckpointStore.CheckpointStore)({
+                    isGitRepository: (cwd) =>
+                      Effect.sync(() => checkpointCwds.push(cwd)).pipe(
+                        Effect.andThen(fs.exists(cwd)),
+                        Effect.orDie,
+                      ),
+                    hasCheckpointRef: () => Effect.succeed(false),
+                    captureCheckpoint: () => Effect.void,
+                  }),
+                ),
+              ),
+            ),
+            Layer.provide(IdAllocator.layer),
+          );
+          const pullRequestRefreshes: Array<{
+            cwd: string;
+            refName: string;
+            branch: string | null;
+          }> = [];
+          const finalizationLayer = RunFinalization.layer.pipe(
+            Layer.provide(
+              Layer.effect(
+                CheckpointCapture.CheckpointCaptureServiceV2,
+                Effect.gen(function* () {
+                  const capture = yield* CheckpointCapture.CheckpointCaptureServiceV2;
+                  return CheckpointCapture.CheckpointCaptureServiceV2.of({
+                    // Let the real promotion commit before capture returns to its caller.
+                    execute: (input) =>
+                      capture.execute(input).pipe(Effect.andThen(Deferred.await(promoted))),
+                  });
+                }),
+              ).pipe(Layer.provide(captureLayer)),
+            ),
+            Layer.provide(
+              RunFinalization.layerObserver.pipe(
+                Layer.provide(
+                  Layer.mergeAll(
+                    layerVcsDriverRegistryTest,
+                    Layer.mock(WorkspaceEntries.WorkspaceEntries)({ refresh: () => Effect.void }),
+                    Layer.mock(PullRequestService.PullRequestService)({
+                      refreshAfterTurn: () => Effect.void,
+                    }),
+                    Layer.succeed(ProjectService.ProjectService, projects),
+                    Layer.mock(VcsStatusBroadcaster.VcsStatusBroadcaster)({
+                      refreshLocalStatus: (cwd) =>
+                        Effect.gen(function* () {
+                          const ref = yield* git(
+                            ["-C", cwd, "symbolic-ref", "--short", "HEAD"],
+                            true,
+                          );
+                          return {
+                            isRepo: ref.exitCode === 0,
+                            hasPrimaryRemote: true,
+                            isDefaultRef: ref.stdout.trim() === "main",
+                            refName: ref.exitCode === 0 ? ref.stdout.trim() : null,
+                            hasWorkingTreeChanges: false,
+                            workingTree: { files: [], insertions: 0, deletions: 0 },
+                          };
+                        }).pipe(Effect.orDie),
+                      refreshPullRequestStatus: (cwd) =>
+                        Effect.gen(function* () {
+                          const ref = yield* git(["-C", cwd, "symbolic-ref", "--short", "HEAD"]);
+                          const projection = yield* orchestrator.getThreadProjection(threadId);
+                          pullRequestRefreshes.push({
+                            cwd,
+                            refName: ref.stdout.trim(),
+                            branch: projection.thread.branch,
+                          });
+                          return null;
+                        }).pipe(Effect.orDie),
+                    }),
+                  ),
+                ),
+              ),
+            ),
+          );
+          yield* Effect.gen(function* () {
+            const finalization = yield* RunFinalization.RunFinalizationService;
+            const starts: Array<{
+              worktreePath: string | null;
+              branch: string | null;
+              checkpointCwd: string | undefined;
+              startCwd: string | null | undefined;
+            }> = [];
+            const executor = Layer.succeed(EffectWorker.OrchestrationEffectExecutorV2, {
+              execute: (effect) =>
+                Effect.gen(function* () {
+                  if (effect.request.type === "checkpoint.capture") {
+                    if (multiplex || rootRepoint || afterFailure) {
+                      const projection = yield* orchestrator.getThreadProjection(threadId);
+                      assert.equal(projection.providerSessions.length, 1);
+                      assert.equal(
+                        projection.providerSessions[0]?.cwd,
+                        afterFailure ? destination : openerCwd,
+                      );
+                      if (afterFailure || move === "root-repoint-linked") {
+                        assert.equal(
+                          providerThread.providerSessionId,
+                          before.providerSessions[0]?.id,
+                        );
+                        assert.equal(run.providerThreadId, before.runs[0]?.providerThreadId);
+                      }
+                      if (rootRepoint) {
+                        assert.equal(
+                          DateTime.toEpochMillis(projection.providerSessions[0]!.updatedAt),
+                          DateTime.toEpochMillis(now),
+                        );
+                        assert.isBelow(
+                          DateTime.toEpochMillis(projection.providerSessions[0]!.updatedAt),
+                          DateTime.toEpochMillis(
+                            projection.runs.find((value) => value.id === run.id)!.startedAt!,
+                          ),
+                        );
+                        assert.equal(
+                          projection.providerSessions[0]?.id,
+                          before.providerSessions[0]?.id,
+                        );
+                        assert.isFalse(
+                          projection.providerSessions[0]!.capabilities.sessions
+                            .supportsMultipleProviderThreadsPerSession,
+                        );
+                      }
+                      assert.isFalse(
+                        yield* projections.isProviderSessionShared(
+                          threadId,
+                          providerThread.providerSessionId!,
+                        ),
+                      );
+                      assert.equal(
+                        (yield* runtimePolicy.resolve({
+                          thread: projection.thread,
+                          modelSelection,
+                        })).cwd,
+                        source,
+                      );
+                    }
+                    yield* finalization.finalize({ threadId, ...effect.request });
+                  } else if (effect.request.type === "provider-turn.start") {
+                    assert.equal(effect.request.runId, queued.id);
+                    const projection = yield* orchestrator.getThreadProjection(threadId);
+                    if (move === "branch-switch") {
+                      assert.equal(
+                        (yield* git([
+                          "-C",
+                          worktree,
+                          "symbolic-ref",
+                          "--short",
+                          "HEAD",
+                        ])).stdout.trim(),
+                        "feature-2",
+                      );
+                      expect
+                        .soft(pullRequestRefreshes, "PR refresh before successor start")
+                        .toEqual([{ cwd: worktree, refName: "feature-2", branch: "feature-2" }]);
+                    }
+                    const queuedNode = projection.nodes.find(
+                      (value) => value.id === queued.rootNodeId,
+                    )!;
+                    const policy = yield* runtimePolicy.resolve({
+                      thread: projection.thread,
+                      modelSelection: queued.modelSelection,
+                    });
+                    starts.push({
+                      startCwd: policy.cwd,
+                      worktreePath: projection.thread.worktreePath,
+                      branch: projection.thread.branch,
+                      checkpointCwd: projection.checkpointScopes.find(
+                        (value) => value.id === queuedNode.checkpointScopeId,
+                      )?.cwd,
+                    });
+                  } else if (effect.request.type === "provider-session.detach") {
+                    assert.equal(
+                      effect.request.providerSessionId,
+                      providerThread.providerSessionId,
+                    );
+                    if (move === "handoff") {
+                      // The provider completed before the queued release effect executed.
+                      const projection = yield* orchestrator.getThreadProjection(threadId);
+                      assert.equal(
+                        projection.attempts.find((value) => value.id === run.activeAttemptId)
+                          ?.status,
+                        "completed",
+                      );
+                    }
+                  } else {
+                    return yield* Effect.die(`Unexpected effect: ${effect.request.type}`);
+                  }
+                }).pipe(
+                  Effect.mapError(
+                    (cause) =>
+                      new EffectWorker.OrchestrationEffectExecutionError({
+                        effectId: effect.id,
+                        effectType: effect.request.type,
+                        cause,
+                      }),
+                  ),
+                ),
+            });
+            yield* Effect.gen(function* () {
+              const worker = yield* EffectWorker.OrchestrationEffectWorkerV2;
+              yield* worker.drain();
+            }).pipe(Effect.provide(EffectWorker.layer.pipe(Layer.provide(executor))));
+            const after = yield* orchestrator.getThreadProjection(threadId);
+            if (terminalStatus === "waiting") {
+              assert.equal(after.runs.find((value) => value.id === run.id)?.status, "completed");
+            } else {
+              assert.equal(after.runs.find((value) => value.id === run.id)?.status, "interrupted");
+            }
+            assert.equal(
+              after.checkpoints.find((value) => value.runId === run.id)?.status,
+              move === "exit-remove" ? "missing" : "ready",
+            );
+            assert.isTrue(
+              checkpointCwds.length > 0 &&
+                checkpointCwds.every((cwd) => cwd === (move === "handoff" ? destination : source)),
+            );
+            assert.equal(after.thread.providerInstanceId, instance.instanceId);
+            if (instance.instanceId !== modelSelection.instanceId) {
+              assert.notEqual(queued.providerThreadId, providerThread.id);
+              assert.equal(
+                after.providerThreads.find((value) => value.id === run.providerThreadId)
+                  ?.providerSessionId,
+                providerThread.providerSessionId,
+              );
+              assert.notInclude(
+                after.providerSessions.map((value) => value.id),
+                providerThread.providerSessionId,
+              );
+              const successor = after.providerThreads.find(
+                (value) => value.id === queued.providerThreadId,
+              )!;
+              assert.equal(
+                successor.nativeThreadRef?.nativeId ?? null,
+                instance.driverKind === driver ? `${threadId}:native` : null,
+              );
+            }
+            expect
+              .soft(
+                starts,
+                `${move}/${terminalStatus}/${instance.instanceId}${afterFailure ? "/after-failure" : ""}${modelSwitch ? "/model-switch" : ""}`,
+              )
+              .toEqual([
+                {
+                  worktreePath:
+                    move === "exit-remove" || move === "multiplex-project" || rootRepoint
+                      ? null
+                      : worktree,
+                  branch:
+                    move === "exit-remove" || move === "multiplex-project" || rootRepoint
+                      ? null
+                      : move === "branch-switch"
+                        ? "feature-2"
+                        : "feature",
+                  checkpointCwd: destination,
+                  startCwd: destination,
+                },
+              ]);
+            const staleFollow = yield* orchestrator
+              .dispatch({
+                type: "thread.metadata.update",
+                commandId: CommandId.make(nextId()),
+                threadId,
+                worktreePath: source,
+                expectedActiveRunId: queued.id,
+                finalizedRunId: RunId.make("superseded-finalization"),
+              })
+              .pipe(Effect.result);
+            assert.equal(staleFollow._tag, "Failure");
+            if (move === "branch-switch") {
+              yield* eventSink.write({
+                events: [
+                  {
+                    id: EventId.make(nextId()),
+                    type: "run.updated",
+                    threadId,
+                    runId: queued.id,
+                    occurredAt: now,
+                    payload: {
+                      ...after.runs.find((value) => value.id === queued.id)!,
+                      status: "running",
+                      startedAt: now,
+                    },
+                  },
+                ],
+              });
+              const lateFollow = yield* orchestrator
+                .dispatch({
+                  type: "thread.metadata.update",
+                  commandId: CommandId.make(nextId()),
+                  threadId,
+                  branch: "stale",
+                  expectedBranch: after.thread.branch,
+                  expectedWorktreePath: worktree,
+                  expectedActiveRunId: queued.id,
+                  finalizedRunId: run.id,
+                  requireExclusiveWorktree: true,
+                })
+                .pipe(Effect.result);
+              assert.equal(lateFollow._tag, "Failure");
+              assert.equal(
+                (yield* orchestrator.getThreadProjection(threadId)).thread.branch,
+                after.thread.branch,
+              );
+            }
+          }).pipe(Effect.provide(finalizationLayer));
+        }).pipe(
+          Effect.provide(
+            Layer.mergeAll(Layer.fresh(layerQueueWorktreeTest), VcsProcess.layer).pipe(
+              Layer.provideMerge(layerServerConfig.pipe(Layer.provideMerge(layerPlatformTest))),
+            ),
+          ),
+        ),
+      ),
+  );
+});
```

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

Status: modified; +7; −0

```diff
@@ -381,6 +381,13 @@
   EffectOutbox.layerPruneWorker.pipe(Layer.provide(EffectOutbox.layer)),
 ).pipe(
   Layer.provide(ResourceCleanupService.layer.pipe(Layer.provide(layerScheduledTaskProvided))),
+  Layer.provide(
+    RunFinalizationService.layerObserver.pipe(
+      Layer.provide(
+        Layer.mergeAll(ProjectionStore.layer, layerProjectService, layerThreadManagementProvided),
+      ),
+    ),
+  ),
   Layer.provide(Scheduler.layer),
   Layer.provideMerge(layerEventInfrastructure),
 );
```

## apps/server/src/server.ts

Status: modified; +1; −2

```diff
@@ -501,8 +501,7 @@
   Layer.provide(layerCheckpointStore),
   Layer.provide(layerGitWorkflow),
   Layer.provide(
-    RunFinalizationService.layerObserver.pipe(
+    RunFinalizationService.layerTurnObserver.pipe(
-      Layer.provide(ProjectionStoreV2.layer),
       Layer.provide(layerPullRequestService),
       Layer.provide(RuntimeLayer.layerProjectService),
     ),
```

[Next page](https://git.cafe/pingdotgg/t3code/pull/15989/diffs?after=ImFwcHMvc2VydmVyL3NyYy9zZXJ2ZXIudHMi&expectedVersion=36&format=markdown)
