Skip to content

Commit 2af22bf

Browse files
committed
fix(opencode): coordinate worktree bootstrap cleanup
1 parent dffa3bb commit 2af22bf

2 files changed

Lines changed: 280 additions & 19 deletions

File tree

packages/opencode/src/worktree/index.ts

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
22
import { path } from "@opencode-ai/core/effect/layer-node-platform"
3+
import { KeyedMutex } from "@opencode-ai/core/effect/keyed-mutex"
34
import { Global } from "@opencode-ai/core/global"
45
import { InstanceLayer } from "@/project/instance-layer"
56
import { InstanceStore } from "@/project/instance-store"
@@ -12,7 +13,7 @@ import { Slug } from "@opencode-ai/core/util/slug"
1213
import { errorMessage } from "../util/error"
1314
import { GlobalBus } from "@/bus/global"
1415
import { Git } from "@/git"
15-
import { Effect, Layer, Path, Schema, Scope, Context } from "effect"
16+
import { Context, Effect, FiberMap, Layer, Path, Schema } from "effect"
1617
import { ChildProcess } from "effect/unstable/process"
1718
import { NodePath } from "@effect/platform-node"
1819
import { FSUtil } from "@opencode-ai/core/fs-util"
@@ -146,7 +147,8 @@ export const layer: Layer.Layer<
146147
> = Layer.effect(
147148
Service,
148149
Effect.gen(function* () {
149-
const scope = yield* Scope.Scope
150+
const bootFibers = yield* FiberMap.make<string, void, never>()
151+
const lifecycle = KeyedMutex.makeUnsafe<string>()
150152
const fs = yield* FSUtil.Service
151153
const pathSvc = yield* Path.Path
152154
const appProcess = yield* AppProcess.Service
@@ -287,20 +289,26 @@ export const layer: Layer.Layer<
287289
{ event: "CwdChanged", oldCwd: ctx.directory, newCwd: info.directory },
288290
{ sessionID: "", transcriptPath: "" },
289291
)
290-
.pipe(
291-
Effect.catch(() => Effect.succeed({ additionalContexts: [], systemMessages: [] })),
292-
)
292+
.pipe(Effect.catch(() => Effect.succeed({ additionalContexts: [], systemMessages: [] })))
293293
yield* SettingsHook.landSystemMessages(cwdResult, { sessionID: "" })
294294
}
295295

296296
yield* runStartScripts(info.directory, { projectID, extra })
297297
})
298298

299299
const createFromInfo = Effect.fn("Worktree.createFromInfo")(function* (info: Info, startCommand?: string) {
300-
yield* setup(info)
301-
yield* boot(info, startCommand).pipe(
302-
Effect.catchCause((cause) => Effect.logError("worktree bootstrap failed", { cause })),
303-
Effect.forkIn(scope),
300+
const directory = yield* canonical(info.directory)
301+
yield* lifecycle.withLock(directory)(
302+
Effect.gen(function* () {
303+
yield* setup(info)
304+
yield* FiberMap.run(
305+
bootFibers,
306+
directory,
307+
boot(info, startCommand).pipe(
308+
Effect.catchCause((cause) => Effect.logError("worktree bootstrap failed", { cause })),
309+
),
310+
)
311+
}),
304312
)
305313
})
306314

@@ -321,7 +329,14 @@ export const layer: Layer.Layer<
321329

322330
const canonical = Effect.fnUntraced(function* (input: string) {
323331
const abs = pathSvc.resolve(input)
324-
const real = yield* fs.realPath(abs).pipe(Effect.catch(() => Effect.succeed(abs)))
332+
const real = yield* fs.realPath(abs).pipe(
333+
Effect.catch(() =>
334+
fs.realPath(pathSvc.dirname(abs)).pipe(
335+
Effect.map((parent) => pathSvc.join(parent, pathSvc.basename(abs))),
336+
Effect.catch(() => Effect.succeed(abs)),
337+
),
338+
),
339+
)
325340
const normalized = pathSvc.normalize(real)
326341
return process.platform === "win32" ? normalized.toLowerCase() : normalized
327342
})
@@ -412,13 +427,13 @@ export const layer: Layer.Layer<
412427
})
413428
}
414429

415-
const remove = Effect.fn("Worktree.remove")(function* (input: RemoveInput) {
430+
const removeLocked = Effect.fnUntraced(function* (input: RemoveInput, directory: string) {
416431
const ctx = yield* InstanceState.context
417432
if (ctx.project.vcs !== "git") {
418433
return yield* new NotGitError({ message: "Worktrees are only supported for git projects" })
419434
}
420435

421-
const directory = yield* canonical(input.directory)
436+
yield* FiberMap.remove(bootFibers, directory)
422437

423438
if (settingsHook) {
424439
const wrResult = yield* settingsHook
@@ -485,6 +500,11 @@ export const layer: Layer.Layer<
485500
return true
486501
})
487502

503+
const remove = Effect.fn("Worktree.remove")(function* (input: RemoveInput) {
504+
const directory = yield* canonical(input.directory)
505+
return yield* lifecycle.withLock(directory)(removeLocked(input, directory))
506+
})
507+
488508
const gitExpect = Effect.fnUntraced(function* (
489509
args: string[],
490510
opts: { cwd: string },
@@ -559,17 +579,17 @@ export const layer: Layer.Layer<
559579
return yield* git(["clean", "-ffdx"], { cwd: root })
560580
})
561581

562-
const reset = Effect.fn("Worktree.reset")(function* (input: ResetInput) {
582+
const resetLocked = Effect.fnUntraced(function* (input: ResetInput, directory: string) {
563583
const ctx = yield* InstanceState.context
564584
if (ctx.project.vcs !== "git") {
565585
return yield* new NotGitError({ message: "Worktrees are only supported for git projects" })
566586
}
567587

568-
const directory = yield* canonical(input.directory)
569588
const primary = yield* canonical(ctx.worktree)
570589
if (directory === primary) {
571590
return yield* new ResetFailedError({ message: "Cannot reset the primary workspace" })
572591
}
592+
yield* FiberMap.remove(bootFibers, directory)
573593

574594
const list = yield* git(["worktree", "list", "--porcelain"], { cwd: ctx.worktree })
575595
if (list.code !== 0) {
@@ -639,14 +659,22 @@ export const layer: Layer.Layer<
639659
return yield* new ResetFailedError({ message: `Worktree reset left local changes:\n${status.text.trim()}` })
640660
}
641661

642-
yield* runStartScripts(worktreePath, { projectID: ctx.project.id }).pipe(
643-
Effect.catchCause((cause) => Effect.logError("worktree start task failed", { cause })),
644-
Effect.forkIn(scope),
662+
yield* FiberMap.run(
663+
bootFibers,
664+
directory,
665+
runStartScripts(worktreePath, { projectID: ctx.project.id }).pipe(
666+
Effect.catchCause((cause) => Effect.logError("worktree start task failed", { cause })),
667+
),
645668
)
646669

647670
return true
648671
})
649672

673+
const reset = Effect.fn("Worktree.reset")(function* (input: ResetInput) {
674+
const directory = yield* canonical(input.directory)
675+
return yield* lifecycle.withLock(directory)(resetLocked(input, directory))
676+
})
677+
650678
return Service.of({ makeWorktreeInfo, createFromInfo, create, list, remove, reset })
651679
}),
652680
)

0 commit comments

Comments
 (0)