package store import ( "bufio" "encoding/json" "os" "path/filepath" "strings" "time" ) // FocusEvent is one raw, disposable observation in a session's firehose log. type FocusEvent struct { AtUnixMillis int64 `json:"at_unix_millis"` Class string `json:"class"` Title string `json:"title"` // full, unscrubbed Available bool `json:"available"` Reason string `json:"reason,omitempty"` } func sessionFile(dir, sessionID string) string { return filepath.Join(dir, sessionID+".jsonl") } // AppendFocus appends one event to dir/.jsonl, creating dir if // needed. It is best-effort detail, not the state of truth. func AppendFocus(dir, sessionID string, e FocusEvent) error { if err := os.MkdirAll(dir, 0o755); err != nil { return err } line, err := json.Marshal(e) if err != nil { return err } f, err := os.OpenFile(sessionFile(dir, sessionID), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return err } defer f.Close() _, err = f.Write(append(line, '\n')) return err } // ReplaySession reads all events for a session in append order. A missing file // yields an empty slice and no error. func ReplaySession(dir, sessionID string) ([]FocusEvent, error) { f, err := os.Open(sessionFile(dir, sessionID)) if os.IsNotExist(err) { return nil, nil } if err != nil { return nil, err } defer f.Close() var out []FocusEvent sc := bufio.NewScanner(f) sc.Buffer(make([]byte, 0, 64*1024), 1024*1024) for sc.Scan() { line := strings.TrimSpace(sc.Text()) if line == "" { continue } var e FocusEvent if err := json.Unmarshal([]byte(line), &e); err != nil { return nil, err } out = append(out, e) } return out, sc.Err() } // PruneOlderThan deletes session files whose modification time precedes // now-age. A missing dir is a no-op. now is injected for testability. func PruneOlderThan(dir string, age time.Duration, now time.Time) error { entries, err := os.ReadDir(dir) if os.IsNotExist(err) { return nil } if err != nil { return err } cutoff := now.Add(-age) for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".jsonl") { continue } info, err := entry.Info() if err != nil { continue } if info.ModTime().Before(cutoff) { _ = os.Remove(filepath.Join(dir, entry.Name())) } } return nil }