diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts index 8072e88b58f6..44bbd41352bc 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts @@ -73,6 +73,7 @@ import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; import { ProviderCommandReactor } from "../Services/ProviderCommandReactor.ts"; import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts"; import * as NodeServices from "@effect/platform-node/NodeServices"; +import { withWorkspaceLease } from "../../workspace/workspaceLease.ts"; import * as Clock from "effect/Clock"; import { ServerSettingsService } from "../../serverSettings.ts"; import { ServerActivation } from "../../serverActivation.ts"; @@ -432,7 +433,13 @@ describe("ProviderCommandReactor", () => { }, }), ); - const runSetupScript = vi.fn(() => Effect.succeed({ status: "no-script" as const })); + // The real runner opens a terminal, which takes the worktree's workspace lease. + const runSetupScript = vi.fn((input: { readonly worktreePath: string }) => + withWorkspaceLease( + NodePath.resolve(input.worktreePath), + Effect.succeed({ status: "no-script" as const }), + ), + ); const pruneWorktrees = vi.fn((_: { readonly cwd: string }) => Effect.void); const refreshStatus = vi.fn((_: string) => Effect.succeed({ diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index fa95d2f765ac..5da70c802710 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -668,12 +668,28 @@ const make = Effect.gen(function* () { cwd: managedWorktree.projectCwd, ...recoveryPlan, }); - yield* projectSetupScriptRunner.runForThread({ - threadId: input.thread.id, - projectId: input.thread.projectId, - projectCwd: managedWorktree.projectCwd, - worktreePath: managedWorktree.path, - }); + // Opening the setup terminal takes this worktree's workspace lease, which + // the turn start already holds. Fork so it queues behind the turn start + // instead of deadlocking on it. + yield* projectSetupScriptRunner + .runForThread({ + threadId: input.thread.id, + projectId: input.thread.projectId, + projectCwd: managedWorktree.projectCwd, + worktreePath: managedWorktree.path, + }) + .pipe( + Effect.catchCause((cause) => + Cause.hasInterruptsOnly(cause) + ? Effect.void + : Effect.logWarning("failed to run setup script for recreated worktree", { + threadId: input.thread.id, + worktreePath: managedWorktree.path, + cause: Cause.pretty(cause), + }), + ), + Effect.forkScoped, + ); yield* Effect.logInfo("recreated missing managed thread worktree", { threadId: input.thread.id, branch: input.thread.branch,