// internal/session/session.go package session import ( "bufio" "encoding/json" "errors" "fmt" "io/fs" "os" "path/filepath" "time" ) // Entry is one skill invocation record, appended to the session JSONL log. type Entry struct { SessionID string `json:"session_id"` Timestamp time.Time `json:"timestamp"` Skill string `json:"skill"` Phase string `json:"phase,omitempty"` ProjectRoot string `json:"project_root,omitempty"` Input json.RawMessage `json:"input,omitempty"` Attempts []Attempt `json:"attempts,omitempty"` FinalStatus string `json:"final_status"` FilePath string `json:"file_path,omitempty"` ModelUsed string `json:"model_used,omitempty"` DurationMs int64 `json:"duration_ms,omitempty"` Message string `json:"message,omitempty"` } // Attempt represents one subprocess invocation within a skill call. type Attempt struct { Attempt int `json:"attempt"` Model string `json:"model"` OutputSummary string `json:"output_summary,omitempty"` RunnerOutput string `json:"runner_output,omitempty"` Verified bool `json:"verified"` } // Append writes entry as a single JSON line to sessionsDir/{sessionID}.jsonl. func Append(sessionsDir, sessionID string, entry Entry) error { if err := os.MkdirAll(sessionsDir, 0o755); err != nil { return fmt.Errorf("create sessions dir: %w", err) } path := filepath.Join(sessionsDir, sessionID+".jsonl") f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) if err != nil { return fmt.Errorf("open session log: %w", err) } line, err := json.Marshal(entry) if err != nil { _ = f.Close() return fmt.Errorf("marshal entry: %w", err) } if _, err = fmt.Fprintf(f, "%s\n", line); err != nil { _ = f.Close() return fmt.Errorf("write entry: %w", err) } if err = f.Close(); err != nil { return fmt.Errorf("close session log: %w", err) } return nil } // Read returns all entries for sessionID. Returns empty slice if no log exists. func Read(sessionsDir, sessionID string) ([]Entry, error) { path := filepath.Join(sessionsDir, sessionID+".jsonl") f, err := os.Open(path) if errors.Is(err, fs.ErrNotExist) { return []Entry{}, nil } if err != nil { return nil, fmt.Errorf("open session log: %w", err) } defer f.Close() var entries []Entry scanner := bufio.NewScanner(f) scanner.Buffer(make([]byte, 0, 256*1024), 1<<20) // up to 1 MB per line for scanner.Scan() { line := scanner.Bytes() if len(line) == 0 { continue } var e Entry if err := json.Unmarshal(line, &e); err != nil { return nil, fmt.Errorf("parse entry: %w", err) } entries = append(entries, e) } return entries, scanner.Err() }