1package core
2
3import (
4 "errors"
5 "fmt"
6 "io/fs"
7 "os"
8 "path/filepath"
9 "sort"
10 "strings"
11)
12
13// StoredSessionGroup describes sessions whose metadata names the same working
14// directory. Paths can span the default and named-agent session namespaces.
15type StoredSessionGroup struct {
16 CWD string
17 Paths []string
18 SizeBytes int64
19}
20
21// SessionScanIssue describes a stored entry that could not be safely
22// classified. Callers should report these entries and leave them untouched.
23type SessionScanIssue struct {
24 Path string
25 Err error
26}
27
28// ScanStoredSessionGroups finds JSONL sessions below sessionsRoot and groups
29// valid entries by the cwd recorded in their metadata. Symlinks and malformed
30// sessions are reported as issues rather than followed or deleted.
31func ScanStoredSessionGroups(sessionsRoot string) ([]StoredSessionGroup, []SessionScanIssue) {
32 byCWD := make(map[string]*StoredSessionGroup)
33 var issues []SessionScanIssue
34
35 err := filepath.WalkDir(sessionsRoot, func(path string, entry fs.DirEntry, walkErr error) error {
36 if walkErr != nil {
37 if path == sessionsRoot && errors.Is(walkErr, fs.ErrNotExist) {
38 return nil
39 }
40 issues = append(issues, SessionScanIssue{Path: path, Err: walkErr})
41 if entry != nil && entry.IsDir() {
42 return fs.SkipDir
43 }
44 return nil
45 }
46 if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".jsonl") {
47 return nil
48 }
49 if entry.Type()&os.ModeSymlink != 0 {
50 issues = append(issues, SessionScanIssue{Path: path, Err: fmt.Errorf("symbolic link is not a stored session")})
51 return nil
52 }
53
54 meta, err := readSessionMeta(path)
55 if err != nil {
56 issues = append(issues, SessionScanIssue{Path: path, Err: err})
57 return nil
58 }
59 cwd := strings.TrimSpace(meta.CWD)
60 if cwd == "" {
61 issues = append(issues, SessionScanIssue{Path: path, Err: fmt.Errorf("session metadata has an empty cwd")})
62 return nil
63 }
64 info, err := entry.Info()
65 if err != nil {
66 issues = append(issues, SessionScanIssue{Path: path, Err: err})
67 return nil
68 }
69 group := byCWD[cwd]
70 if group == nil {
71 group = &StoredSessionGroup{CWD: cwd}
72 byCWD[cwd] = group
73 }
74 group.Paths = append(group.Paths, path)
75 group.SizeBytes += info.Size()
76 return nil
77 })
78 if err != nil && !os.IsNotExist(err) {
79 issues = append(issues, SessionScanIssue{Path: sessionsRoot, Err: err})
80 }
81
82 groups := make([]StoredSessionGroup, 0, len(byCWD))
83 for _, group := range byCWD {
84 sort.Strings(group.Paths)
85 groups = append(groups, *group)
86 }
87 sort.Slice(groups, func(i, j int) bool { return groups[i].CWD < groups[j].CWD })
88 sort.Slice(issues, func(i, j int) bool { return issues[i].Path < issues[j].Path })
89 return groups, issues
90}