-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsession.go
More file actions
255 lines (240 loc) · 6.42 KB
/
Copy pathsession.go
File metadata and controls
255 lines (240 loc) · 6.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
// validateJSONL returns nil iff path exists, is non-empty, and the last
// \n-terminated record parses as JSON. A trailing partial line (no
// terminating \n) is allowed — claude often has one in flight.
func validateJSONL(path string) error {
info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("stat %s: %w", path, err)
}
if info.Size() == 0 {
return fmt.Errorf("empty file: %s", path)
}
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
// Assumes no single JSONL record exceeds tailWindow. 64 KiB is much
// larger than Claude Code's per-message records in practice.
const tailWindow = 64 * 1024
start := info.Size() - tailWindow
if start < 0 {
start = 0
}
if _, err := f.Seek(start, io.SeekStart); err != nil {
return err
}
buf, err := io.ReadAll(f)
if err != nil {
return err
}
lastNL := bytes.LastIndexByte(buf, '\n')
if lastNL < 0 {
return fmt.Errorf("no terminated record in tail: %s", path)
}
recStart := bytes.LastIndexByte(buf[:lastNL], '\n') + 1
record := bytes.TrimSpace(buf[recStart:lastNL])
if len(record) == 0 {
return fmt.Errorf("empty last record: %s", path)
}
var v any
if err := json.Unmarshal(record, &v); err != nil {
return fmt.Errorf("invalid JSON in last record of %s: %w", path, err)
}
return nil
}
type sessionInfo struct {
ID string // UUID directory name
Dir string // absolute path to session dir
LiveJSONL string // absolute path to newest *.jsonl in Dir
LastWrite time.Time // mtime of LiveJSONL
}
// listSessions returns all session dirs under <stateDir>/projects/<ns>/,
// sorted newest-first by the mtime of each session's newest *.jsonl.
// Sessions with no .jsonl are skipped. Hidden (dot-prefixed) entries are
// skipped. Missing namespace dir returns (nil, nil).
func listSessions(stateDir, ns string) ([]sessionInfo, error) {
nsDir := filepath.Join(stateDir, "projects", ns)
entries, err := os.ReadDir(nsDir)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
var sessions []sessionInfo
for _, e := range entries {
if !e.IsDir() || strings.HasPrefix(e.Name(), ".") {
continue
}
dir := filepath.Join(nsDir, e.Name())
live, mt, err := newestTopLevelJSONL(dir)
if err != nil || live == "" {
continue
}
sessions = append(sessions, sessionInfo{
ID: e.Name(), Dir: dir, LiveJSONL: live, LastWrite: mt,
})
}
sort.Slice(sessions, func(i, j int) bool {
return sessions[i].LastWrite.After(sessions[j].LastWrite)
})
return sessions, nil
}
// newestTopLevelJSONL returns the path + mtime of the newest *.jsonl
// directly under dir (does not descend into subdirectories — so .snapshots
// content is intentionally excluded).
func newestTopLevelJSONL(dir string) (string, time.Time, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return "", time.Time{}, err
}
var (
bestPath string
bestMt time.Time
)
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".jsonl") {
continue
}
info, err := e.Info()
if err != nil {
continue
}
if info.ModTime().After(bestMt) {
bestMt = info.ModTime()
bestPath = filepath.Join(dir, e.Name())
}
}
return bestPath, bestMt, nil
}
func latestSnapshot(sessionDir string) (string, error) {
snapDir := filepath.Join(sessionDir, ".snapshots")
entries, err := os.ReadDir(snapDir)
if err != nil {
return "", err
}
type cand struct {
path string
mt time.Time
}
var cands []cand
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".jsonl") {
continue
}
info, err := e.Info()
if err != nil {
continue
}
cands = append(cands, cand{filepath.Join(snapDir, e.Name()), info.ModTime()})
}
sort.Slice(cands, func(i, j int) bool { return cands[i].mt.After(cands[j].mt) })
for _, c := range cands {
if validateJSONL(c.path) == nil {
return c.path, nil
}
}
return "", fmt.Errorf("no valid snapshot in %s", snapDir)
}
// restoreFromSnapshot writes the contents of snapPath over the session's
// live JSONL using a tmp+rename so readers never see a partial file. The
// "live JSONL" is the newest top-level *.jsonl in sessionDir.
func restoreFromSnapshot(sessionDir, snapPath string) error {
livePath, _, err := newestTopLevelJSONL(sessionDir)
if err != nil {
return err
}
if livePath == "" {
return fmt.Errorf("no live jsonl in %s", sessionDir)
}
src, err := os.Open(snapPath)
if err != nil {
return err
}
defer src.Close()
tmpPath := livePath + ".restore.tmp"
dst, err := os.Create(tmpPath)
if err != nil {
return err
}
if _, err := io.Copy(dst, src); err != nil {
dst.Close()
os.Remove(tmpPath)
return err
}
if err := dst.Sync(); err != nil {
dst.Close()
os.Remove(tmpPath)
return err
}
if err := dst.Close(); err != nil {
os.Remove(tmpPath)
return err
}
return os.Rename(tmpPath, livePath)
}
var (
errNoSessions = errors.New("no prior sessions for namespace")
errNoValidSession = errors.New("no resumable session (all live JSONLs corrupt and no valid snapshots)")
)
type errSessionNotFound struct {
ID string
Available []sessionInfo
}
func (e *errSessionNotFound) Error() string {
return fmt.Sprintf("session %s not found", e.ID)
}
// pickSession selects a session for resume. If idArg is non-empty, only that
// session is considered; otherwise sessions are tried newest-first. For each
// candidate, the live JSONL is validated; on validation failure we attempt to
// restore from the latest valid snapshot before accepting.
func pickSession(stateDir, ns, idArg string) (sessionInfo, error) {
sessions, err := listSessions(stateDir, ns)
if err != nil {
return sessionInfo{}, err
}
if len(sessions) == 0 {
return sessionInfo{}, errNoSessions
}
tryRecover := func(s sessionInfo) (sessionInfo, error) {
if vErr := validateJSONL(s.LiveJSONL); vErr == nil {
return s, nil
}
snap, sErr := latestSnapshot(s.Dir)
if sErr != nil {
return sessionInfo{}, sErr
}
if rErr := restoreFromSnapshot(s.Dir, snap); rErr != nil {
return sessionInfo{}, rErr
}
return s, nil
}
if idArg != "" {
for _, s := range sessions {
if s.ID == idArg {
return tryRecover(s)
}
}
return sessionInfo{}, &errSessionNotFound{ID: idArg, Available: sessions}
}
for _, s := range sessions {
if got, err := tryRecover(s); err == nil {
return got, nil
}
}
return sessionInfo{}, errNoValidSession
}