Why: Linear transcripts cannot fork, and compaction rewrote history, so resume and branch navigation had nothing to point at. Evidence: go test -count=1 ./runtime/session/ Impact: Load still returns the model-visible path, including old one-message-per-line files. Compaction appends a summary entry instead of truncating the file. Refs: feature/pi-headless N1 Co-authored-by: Cursor <cursoragent@cursor.com>
106 lines
2.2 KiB
Go
106 lines
2.2 KiB
Go
package session
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Info describes one session file in a directory.
|
|
type Info struct {
|
|
ID string
|
|
Path string
|
|
Name string
|
|
CWD string
|
|
Leaf string
|
|
Updated string
|
|
}
|
|
|
|
// List returns session files in dir, newest first.
|
|
func List(dir string) ([]Info, error) {
|
|
ents, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
var out []Info
|
|
for _, ent := range ents {
|
|
name := ent.Name()
|
|
if ent.IsDir() || !strings.HasSuffix(name, ".jsonl") {
|
|
continue
|
|
}
|
|
path := filepath.Join(dir, name)
|
|
st, err := Open(path)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
info, err := st.Info()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
out = append(out, info)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Updated > out[j].Updated })
|
|
return out, nil
|
|
}
|
|
|
|
// Continue opens the newest session whose cwd matches. A second return
|
|
// value of false means there is nothing to continue.
|
|
func Continue(dir, cwd string) (*Store, bool, error) {
|
|
list, err := List(dir)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
for _, info := range list {
|
|
if cwd == "" || info.CWD == cwd {
|
|
st, err := Open(info.Path)
|
|
return st, err == nil, err
|
|
}
|
|
}
|
|
return nil, false, nil
|
|
}
|
|
|
|
// Resume opens a session by id (filename stem) or by name.
|
|
func Resume(dir, idOrName string) (*Store, error) {
|
|
list, err := List(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, info := range list {
|
|
if info.ID == idOrName || info.Name == idOrName {
|
|
return Open(info.Path)
|
|
}
|
|
}
|
|
return nil, os.ErrNotExist
|
|
}
|
|
|
|
// SetCWD records the working directory used by Continue.
|
|
func (s *Store) SetCWD(cwd string) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.writeMetaKeyLocked("cwd", cwd)
|
|
}
|
|
|
|
// Info reads the catalog fields for this file.
|
|
func (s *Store) Info() (Info, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
entries, err := s.readEntriesLocked()
|
|
if err != nil {
|
|
return Info{}, err
|
|
}
|
|
meta := s.readMetaLocked()
|
|
updated := ""
|
|
if len(entries) > 0 {
|
|
updated = entries[len(entries)-1].Timestamp
|
|
}
|
|
id := strings.TrimSuffix(filepath.Base(s.path), ".jsonl")
|
|
return Info{
|
|
ID: id, Path: s.path, Name: meta["name"], CWD: meta["cwd"],
|
|
Leaf: s.leafLocked(entries), Updated: updated,
|
|
}, nil
|
|
}
|