11import { LayerNode } from "@opencode-ai/core/effect/layer-node"
22import { path } from "@opencode-ai/core/effect/layer-node-platform"
3+ import { KeyedMutex } from "@opencode-ai/core/effect/keyed-mutex"
34import { Global } from "@opencode-ai/core/global"
45import { InstanceLayer } from "@/project/instance-layer"
56import { InstanceStore } from "@/project/instance-store"
@@ -12,7 +13,7 @@ import { Slug } from "@opencode-ai/core/util/slug"
1213import { errorMessage } from "../util/error"
1314import { GlobalBus } from "@/bus/global"
1415import { Git } from "@/git"
15- import { Effect , Layer , Path , Schema , Scope , Context } from "effect"
16+ import { Context , Effect , FiberMap , Layer , Path , Schema } from "effect"
1617import { ChildProcess } from "effect/unstable/process"
1718import { NodePath } from "@effect/platform-node"
1819import { 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