M1: raw per-session focus log with retention prune

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 12:41:08 -04:00
parent d780643015
commit 5ee34a350d
2 changed files with 169 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
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/<sessionID>.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
}