# refactor(session): share session state locks

`3d3df24`→[main](/content/gh/entireio/cli/commits/main/index.html)·

peyton-alt·3w ago·3 files·+57 added/-65 removed

## Sessions

03f1aba5f48fView transcript

[?\
Adopt Active Sessions Across Repos and WorktreesCodex·GPT-5.5·1 step](/content/gh/entireio/cli/session/019ef6af-f809-76d3-8c95-7d7f0bc0c5ef#timeline-03f1aba5f48f/index.html)

## Changes

3

- cmd/entire/cli
  
  - Msession_adopt.go+2/-59
  
  - Msession_adopt_test.go+2/-2
  
  - strategy
  
    - Msession_state.go+53/-4

```
6 unmodified lines

7
8
9
10
10
11
12
3 unmodified lines

16
17
18
20
19
20
21
24
22
23
24
114 unmodified lines

139
140
141
145
142
143
144
145
31 unmodified lines

177
178
179
183
180
181
182
183
58 unmodified lines

242
243
244
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
245
246
247

6 unmodified lines

"fmt"
	"io"
	"maps"
	"os"
	"os/exec"
	"path/filepath"
	"slices"
3 unmodified lines

"github.com/entireio/cli/cmd/entire/cli/agent"
	"github.com/entireio/cli/cmd/entire/cli/checkpoint/id"
	"github.com/entireio/cli/cmd/entire/cli/internal/flock"
	"github.com/entireio/cli/cmd/entire/cli/paths"
	"github.com/entireio/cli/cmd/entire/cli/session"
	"github.com/entireio/cli/cmd/entire/cli/strategy"
	"github.com/entireio/cli/cmd/entire/cli/validation"
	"github.com/entireio/cli/cmd/entire/cli/versioninfo"
	"github.com/spf13/cobra"
)
114 unmodified lines

var adopted *session.State
	var filesTouched []string
	err := withAdoptSessionLocks(ctx, sessionID, []string{sourceCommonDir, targetCommonDir}, func() error {
	err := strategy.WithSessionStateLocks(ctx, sessionID, []string{sourceCommonDir, targetCommonDir}, func() error {
		sourceState, err := sourceStore.Load(ctx, sessionID)
		if err != nil {
			return fmt.Errorf("load source session state: %w", err)
		}
31 unmodified lines

return nil
	})
	if err != nil {
		return nil, nil, err
		return nil, nil, fmt.Errorf("adopt external session state: %w", err)
	}
	return adopted, filesTouched, nil
}

func withAdoptSessionLocks(ctx context.Context, sessionID string, commonDirs []string, fn func() error) error {
	lockPaths := make([]string, 0, len(commonDirs))
	seen := make(map[string]struct{}, len(commonDirs))
	for _, commonDir := range commonDirs {
		lockPath, err := adoptSessionLockPath(commonDir, sessionID)
		if err != nil {
			return err
		}
		if _, ok := seen[lockPath]; ok {
			continue
		}
		seen[lockPath] = struct{}{}
		lockPaths = append(lockPaths, lockPath)
	}
	sort.Strings(lockPaths)

releases := make([]func(), 0, len(lockPaths))
	for _, lockPath := range lockPaths {
		if err := ctx.Err(); err != nil {
			releaseAdoptSessionLocks(releases)
			return fmt.Errorf("adopt session lock canceled: %w", err)
		}
		release, err := flock.Acquire(lockPath)
		if err != nil {
			releaseAdoptSessionLocks(releases)
			return fmt.Errorf("acquire session state lock: %w", err)
		}
		releases = append(releases, release)
	}
	defer releaseAdoptSessionLocks(releases)

return fn()
}

func releaseAdoptSessionLocks(releases []func()) {
	for i := len(releases) - 1; i >= 0; i-- {
		releases[i]()
	}
}

func adoptSessionLockPath(commonDir, sessionID string) (string, error) {
	if strings.TrimSpace(commonDir) == "" {
		return "", errors.New("resolve session state lock: empty git common dir")
	}
	if err := validation.ValidateSessionID(sessionID); err != nil {
		return "", fmt.Errorf("invalid session ID: %w", err)
	}
	lockDir := filepath.Join(commonDir, "entire-session-locks")
	if err := os.MkdirAll(lockDir, 0o750); err != nil {
		return "", fmt.Errorf("create session lock directory: %w", err)
	}
	return filepath.Join(lockDir, sessionID+".lock"), nil
}

func stateStoreForWorktree(ctx context.Context, worktreePath string) (*session.StateStore, string, string, error) {
	absWorktree, err := filepath.Abs(worktreePath)
	if err != nil {

Mcmd/entire/cli/session_adopt.go+2/-59

```
263 unmodified lines

264
265
266
267
268
267
268
269
270
271

263 unmodified lines

t.Fatal(err)
	}

lockPath, err := adoptSessionLockPath(targetCommonDir, sessionID)
	if err != nil {
		lockPath := filepath.Join(targetCommonDir, "entire-session-locks", sessionID+".lock")
		if err := os.MkdirAll(filepath.Dir(lockPath), 0o750); err != nil {
		t.Fatal(err)
	}
		release, err := flock.Acquire(lockPath)
```

Mcmd/entire/cli/session_adopt_test.go+2/-2

```
591 unmodified lines

592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
36 unmodified lines

676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
640
641
642
643
693
694
695

591 unmodified lines

}, nil
}

// WithSessionStateLocks acquires the per-session state lock in each git common
// dir, then runs fn. Lock paths are deduplicated and sorted so callers that
// span repositories or worktrees can safely acquire more than one lock.
func WithSessionStateLocks(ctx context.Context, sessionID string, commonDirs []string, fn func() error) error {
	lockPaths := make([]string, 0, len(commonDirs))
	seen := make(map[string]struct{}, len(commonDirs))
	for _, commonDir := range commonDirs {
		lockPath, err := stateLockPathInCommonDir(commonDir, sessionID)
		if err != nil {
			return err
		}
		if _, ok := seen[lockPath]; ok {
			continue
		}
		seen[lockPath] = struct{}{}
		lockPaths = append(lockPaths, lockPath)
	}
	slices.Sort(lockPaths)

releases := make([]func(), 0, len(lockPaths))
	releaseAll := func() {
		for i := len(releases) - 1; i >= 0; i-- {
			releases[i]()
		}
	}
	for _, lockPath := range lockPaths {
		if err := ctx.Err(); err != nil {
			releaseAll()
			return fmt.Errorf("session state lock canceled: %w", err)
		}
		release, err := flock.Acquire(lockPath)
		if err != nil {
			releaseAll()
			return fmt.Errorf("acquire session state lock: %w", err)
		}
		releases = append(releases, release)
	}
	defer releaseAll()

return fn()
}

// ErrMutationSkip signals MutateSessionState to skip the save without
// treating fn's return as an error. Use it when the mutation function
// observes the loaded state and decides no write is needed (for example,
36 unmodified lines

// holder distinct from the data — Save's atomic-rename pattern would
// otherwise unlink the inode the flock is held on.
func stateLockPath(ctx context.Context, sessionID string) (string, error) {
	commonDir, err := GetGitCommonDir(ctx)
	if err != nil {
		return "", err
	}
	return stateLockPathInCommonDir(commonDir, sessionID)
}

func stateLockPathInCommonDir(commonDir, sessionID string) (string, error) {
	if strings.TrimSpace(commonDir) == "" {
		return "", errors.New("empty git common dir")
	}
	if err := validation.ValidateSessionID(sessionID); err != nil {
		return "", fmt.Errorf("invalid session ID: %w", err)
	}
	commonDir, err := GetGitCommonDir(ctx)
	if err != nil {
		return "", err
	}
	lockDir := filepath.Join(commonDir, "entire-session-locks")
	if err := os.MkdirAll(lockDir, 0o750); err != nil {
		return "", fmt.Errorf("create session lock directory: %w", err)
	}
