refactor(session): share session state locks · Entire

refactor(session): share session state locks

3d3df24→main·

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

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-- { releasesi } } 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) }