mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-26 21:36:31 +02:00
core: drop empty session files instead of writing meta-only stubs
every zot launch was creating a session file with just a meta line, even when the user exited without prompting. /sessions and ls -la ended up showing dozens of empty entries. now Session tracks messagesAppended and freshFile (true for NewSession, false for OpenSession). Close() removes the file when both conditions hold: this process created it AND no messages were ever appended. resumed sessions are never auto-deleted even if the resume run added nothing, since the prior content is real. PruneEmptySessions sweeps existing meta-only stubs from the cwd's session dir on each interactive launch. cheap (only reads enough of each file to find a 'message' line) and fixes the existing backlog automatically the first time you reopen zot in a project.
This commit is contained in:
parent
0a69274cd6
commit
fdef8ac614
2 changed files with 88 additions and 11 deletions
|
|
@ -265,6 +265,10 @@ func openOrCreateSession(args Args, r Resolved, ag *core.Agent, version string)
|
|||
if args.NoSess {
|
||||
return nil, nil
|
||||
}
|
||||
// Sweep meta-only files left over from older zot versions (and from
|
||||
// any session that crashed before its first AppendMessage). Cheap;
|
||||
// reads the first few bytes of each file in the cwd's session dir.
|
||||
core.PruneEmptySessions(ZotHome(), args.CWD)
|
||||
var (
|
||||
s *core.Session
|
||||
msgs []provider.Message
|
||||
|
|
|
|||
|
|
@ -23,6 +23,18 @@ type Session struct {
|
|||
Meta SessionMeta
|
||||
writer *os.File
|
||||
buf *bufio.Writer
|
||||
|
||||
// freshFile is true when the file was created by NewSession (this
|
||||
// process owns it) and false when OpenSession reopened an existing
|
||||
// transcript. Used by Close() to delete the file if the run never
|
||||
// appended any messages — prevents a flood of empty session files
|
||||
// from sessions the user opens then exits without prompting.
|
||||
freshFile bool
|
||||
|
||||
// messagesAppended counts AppendMessage calls. Combined with
|
||||
// freshFile it tells Close() whether the session left any content
|
||||
// worth keeping.
|
||||
messagesAppended int
|
||||
}
|
||||
|
||||
// SessionMeta is written as the first line of every session file.
|
||||
|
|
@ -72,11 +84,12 @@ func NewSession(root, cwd, providerName, model, version string) (*Session, error
|
|||
return nil, err
|
||||
}
|
||||
s := &Session{
|
||||
ID: id,
|
||||
Path: p,
|
||||
Meta: SessionMeta{ID: id, CWD: cwd, Provider: providerName, Model: model, Started: time.Now().UTC(), Version: version},
|
||||
writer: f,
|
||||
buf: bufio.NewWriter(f),
|
||||
ID: id,
|
||||
Path: p,
|
||||
Meta: SessionMeta{ID: id, CWD: cwd, Provider: providerName, Model: model, Started: time.Now().UTC(), Version: version},
|
||||
writer: f,
|
||||
buf: bufio.NewWriter(f),
|
||||
freshFile: true,
|
||||
}
|
||||
if err := s.writeLine(sessionLine{Type: "meta", Meta: &s.Meta}); err != nil {
|
||||
f.Close()
|
||||
|
|
@ -223,6 +236,51 @@ func firstUserText(line []byte) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// PruneEmptySessions deletes session files in cwd's session directory
|
||||
// that contain only a meta line (no messages were ever appended).
|
||||
// Cleans up the backlog of empty stubs created by old zot versions
|
||||
// that wrote a meta line at NewSession time and never followed up.
|
||||
// Errors are swallowed; the caller treats this as best-effort.
|
||||
func PruneEmptySessions(root, cwd string) {
|
||||
dir := SessionsDir(root, cwd)
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".jsonl") {
|
||||
continue
|
||||
}
|
||||
p := filepath.Join(dir, e.Name())
|
||||
if sessionHasNoMessages(p) {
|
||||
_ = os.Remove(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sessionHasNoMessages returns true when the file at path contains
|
||||
// no lines of type "message". Meta-only / usage-only files count as
|
||||
// empty. Used by PruneEmptySessions and the Describe path.
|
||||
func sessionHasNoMessages(path string) bool {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
sc := bufio.NewScanner(f)
|
||||
sc.Buffer(make([]byte, 0, 64*1024), 20*1024*1024)
|
||||
for sc.Scan() {
|
||||
var head sessionLineHead
|
||||
if err := json.Unmarshal(sc.Bytes(), &head); err != nil {
|
||||
continue
|
||||
}
|
||||
if head.Type == "message" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ListSessions returns session file paths for cwd, newest first.
|
||||
func ListSessions(root, cwd string) []string {
|
||||
dir := SessionsDir(root, cwd)
|
||||
|
|
@ -245,7 +303,11 @@ func (s *Session) AppendMessage(m provider.Message) error {
|
|||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return s.writeLine(sessionLine{Type: "message", Message: &m})
|
||||
if err := s.writeLine(sessionLine{Type: "message", Message: &m}); err != nil {
|
||||
return err
|
||||
}
|
||||
s.messagesAppended++
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateModel records a provider/model switch in the session file.
|
||||
|
|
@ -268,16 +330,27 @@ func (s *Session) AppendUsage(u, cum provider.Usage) error {
|
|||
return s.writeLine(sessionLine{Type: "usage", Usage: &u, Cumulative: &cum})
|
||||
}
|
||||
|
||||
// Close flushes and closes the session file.
|
||||
// Close flushes and closes the session file. If the session was
|
||||
// freshly created in this process and never had any messages
|
||||
// appended (the user opened zot, looked around, and exited without
|
||||
// prompting), the file is deleted on close so the sessions list
|
||||
// doesn't fill up with empty meta-only stubs.
|
||||
func (s *Session) Close() error {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
if err := s.buf.Flush(); err != nil {
|
||||
s.writer.Close()
|
||||
return err
|
||||
flushErr := s.buf.Flush()
|
||||
closeErr := s.writer.Close()
|
||||
if s.freshFile && s.messagesAppended == 0 {
|
||||
// Best-effort cleanup. We deliberately don't propagate the
|
||||
// remove error: if it fails (file already gone, perms changed)
|
||||
// the worst case is one stale empty file in the listing.
|
||||
_ = os.Remove(s.Path)
|
||||
}
|
||||
return s.writer.Close()
|
||||
if flushErr != nil {
|
||||
return flushErr
|
||||
}
|
||||
return closeErr
|
||||
}
|
||||
|
||||
func (s *Session) writeLine(row sessionLine) error {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue