Go · 7711 bytes Raw Blame History
1 package llm
2
3 import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "sort"
8 "sync"
9 "time"
10 )
11
12 // UserFailureHistory tracks persistent failure patterns across sessions
13 type UserFailureHistory struct {
14 TotalFailures int `json:"total_failures"`
15 CurrentStreak int `json:"current_streak"`
16 LongestStreak int `json:"longest_streak"`
17 LastFailureTime time.Time `json:"last_failure_time"`
18 CommandFrequency map[string]int `json:"command_frequency"`
19 HourlyDistribution map[int]int `json:"hourly_distribution"`
20 BranchDisasters map[string]int `json:"branch_disasters"`
21 ProjectFailures map[string]int `json:"project_failures"`
22 ExitCodeFrequency map[int]int `json:"exit_code_frequency"`
23 WorstDay string `json:"worst_day"`
24 DailyFailures map[string]int `json:"daily_failures"`
25
26 mu sync.Mutex `json:"-"` // Mutex for thread-safe operations
27 }
28
29 var (
30 userHistory *UserFailureHistory
31 historyFile string
32 historyLoadOnce sync.Once
33 )
34
35 // getHistoryFilePath returns the path to the failure history file
36 func getHistoryFilePath() string {
37 home, err := os.UserHomeDir()
38 if err != nil {
39 return ""
40 }
41
42 parrotDir := filepath.Join(home, ".parrot")
43 os.MkdirAll(parrotDir, 0755)
44
45 return filepath.Join(parrotDir, "failures.json")
46 }
47
48 // LoadUserHistory loads failure history from disk
49 func LoadUserHistory() *UserFailureHistory {
50 historyLoadOnce.Do(func() {
51 historyFile = getHistoryFilePath()
52 if historyFile == "" {
53 userHistory = newUserHistory()
54 return
55 }
56
57 data, err := os.ReadFile(historyFile)
58 if err != nil {
59 // File doesn't exist yet, create new history
60 userHistory = newUserHistory()
61 return
62 }
63
64 var history UserFailureHistory
65 if err := json.Unmarshal(data, &history); err != nil {
66 // Corrupted file, start fresh
67 userHistory = newUserHistory()
68 return
69 }
70
71 userHistory = &history
72 if userHistory.CommandFrequency == nil {
73 userHistory.CommandFrequency = make(map[string]int)
74 }
75 if userHistory.HourlyDistribution == nil {
76 userHistory.HourlyDistribution = make(map[int]int)
77 }
78 if userHistory.BranchDisasters == nil {
79 userHistory.BranchDisasters = make(map[string]int)
80 }
81 if userHistory.ProjectFailures == nil {
82 userHistory.ProjectFailures = make(map[string]int)
83 }
84 if userHistory.ExitCodeFrequency == nil {
85 userHistory.ExitCodeFrequency = make(map[int]int)
86 }
87 if userHistory.DailyFailures == nil {
88 userHistory.DailyFailures = make(map[string]int)
89 }
90
91 // Check if streak should be reset (more than 1 hour since last failure)
92 if time.Since(userHistory.LastFailureTime) > time.Hour {
93 userHistory.CurrentStreak = 0
94 }
95 })
96
97 return userHistory
98 }
99
100 // newUserHistory creates a new empty history
101 func newUserHistory() *UserFailureHistory {
102 return &UserFailureHistory{
103 CommandFrequency: make(map[string]int),
104 HourlyDistribution: make(map[int]int),
105 BranchDisasters: make(map[string]int),
106 ProjectFailures: make(map[string]int),
107 ExitCodeFrequency: make(map[int]int),
108 DailyFailures: make(map[string]int),
109 }
110 }
111
112 // RecordFailure records a new failure in the history
113 func (h *UserFailureHistory) RecordFailure(ctx SmartFallbackContext) {
114 if h == nil {
115 return
116 }
117
118 h.mu.Lock()
119 defer h.mu.Unlock()
120
121 // Update counters
122 h.TotalFailures++
123
124 // Update streak
125 if time.Since(h.LastFailureTime) <= time.Hour {
126 h.CurrentStreak++
127 } else {
128 h.CurrentStreak = 1
129 }
130
131 if h.CurrentStreak > h.LongestStreak {
132 h.LongestStreak = h.CurrentStreak
133 }
134
135 h.LastFailureTime = time.Now()
136
137 // Track command frequency
138 if ctx.Command != "" {
139 h.CommandFrequency[ctx.Command]++
140 }
141
142 // Track hourly distribution
143 hour := time.Now().Hour()
144 h.HourlyDistribution[hour]++
145
146 // Track branch disasters
147 if ctx.GitBranch != "" {
148 h.BranchDisasters[ctx.GitBranch]++
149 }
150
151 // Track project failures
152 if ctx.ProjectType != "" {
153 h.ProjectFailures[ctx.ProjectType]++
154 }
155
156 // Track exit codes
157 if ctx.ExitCode > 0 {
158 h.ExitCodeFrequency[ctx.ExitCode]++
159 }
160
161 // Track daily failures
162 today := time.Now().Format("2006-01-02")
163 h.DailyFailures[today]++
164
165 // Determine worst day
166 maxFailures := 0
167 for day, count := range h.DailyFailures {
168 if count > maxFailures {
169 maxFailures = count
170 h.WorstDay = day
171 }
172 }
173
174 // Save to disk
175 h.Save()
176 }
177
178 // Save writes the history to disk
179 func (h *UserFailureHistory) Save() error {
180 if h == nil || historyFile == "" {
181 return nil
182 }
183
184 data, err := json.MarshalIndent(h, "", " ")
185 if err != nil {
186 return err
187 }
188
189 return os.WriteFile(historyFile, data, 0644)
190 }
191
192 // GetMostFailedCommand returns the command with the most failures
193 func (h *UserFailureHistory) GetMostFailedCommand() (string, int) {
194 if h == nil {
195 return "", 0
196 }
197
198 h.mu.Lock()
199 defer h.mu.Unlock()
200
201 maxCmd := ""
202 maxCount := 0
203
204 for cmd, count := range h.CommandFrequency {
205 if count > maxCount {
206 maxCount = count
207 maxCmd = cmd
208 }
209 }
210
211 return maxCmd, maxCount
212 }
213
214 // GetWorstHour returns the hour with the most failures
215 func (h *UserFailureHistory) GetWorstHour() int {
216 if h == nil {
217 return -1
218 }
219
220 h.mu.Lock()
221 defer h.mu.Unlock()
222
223 maxHour := -1
224 maxCount := 0
225
226 for hour, count := range h.HourlyDistribution {
227 if count > maxCount {
228 maxCount = count
229 maxHour = hour
230 }
231 }
232
233 return maxHour
234 }
235
236 // GetMostDisastrousBranch returns the branch with the most failures
237 func (h *UserFailureHistory) GetMostDisastrousBranch() (string, int) {
238 if h == nil {
239 return "", 0
240 }
241
242 h.mu.Lock()
243 defer h.mu.Unlock()
244
245 maxBranch := ""
246 maxCount := 0
247
248 for branch, count := range h.BranchDisasters {
249 if count > maxCount {
250 maxCount = count
251 maxBranch = branch
252 }
253 }
254
255 return maxBranch, maxCount
256 }
257
258 // GetFailureStats returns a summary of failure statistics
259 type FailureStats struct {
260 TotalFailures int
261 CurrentStreak int
262 LongestStreak int
263 WorstCommand string
264 WorstCommandCount int
265 WorstHour int
266 WorstBranch string
267 WorstBranchCount int
268 TodayFailures int
269 WorstDay string
270 WorstDayCount int
271 }
272
273 func (h *UserFailureHistory) GetStats() FailureStats {
274 if h == nil {
275 return FailureStats{}
276 }
277
278 h.mu.Lock()
279 defer h.mu.Unlock()
280
281 stats := FailureStats{
282 TotalFailures: h.TotalFailures,
283 CurrentStreak: h.CurrentStreak,
284 LongestStreak: h.LongestStreak,
285 }
286
287 // Find worst command
288 for cmd, count := range h.CommandFrequency {
289 if count > stats.WorstCommandCount {
290 stats.WorstCommandCount = count
291 stats.WorstCommand = cmd
292 }
293 }
294
295 // Find worst hour
296 maxHourCount := 0
297 for hour, count := range h.HourlyDistribution {
298 if count > maxHourCount {
299 maxHourCount = count
300 stats.WorstHour = hour
301 }
302 }
303
304 // Find worst branch
305 for branch, count := range h.BranchDisasters {
306 if count > stats.WorstBranchCount {
307 stats.WorstBranchCount = count
308 stats.WorstBranch = branch
309 }
310 }
311
312 // Today's failures
313 today := time.Now().Format("2006-01-02")
314 stats.TodayFailures = h.DailyFailures[today]
315
316 // Worst day
317 stats.WorstDay = h.WorstDay
318 if h.WorstDay != "" {
319 stats.WorstDayCount = h.DailyFailures[h.WorstDay]
320 }
321
322 return stats
323 }
324
325 // GetTopFailedCommands returns the top N most failed commands
326 func (h *UserFailureHistory) GetTopFailedCommands(n int) []string {
327 if h == nil {
328 return nil
329 }
330
331 h.mu.Lock()
332 defer h.mu.Unlock()
333
334 type cmdCount struct {
335 cmd string
336 count int
337 }
338
339 var commands []cmdCount
340 for cmd, count := range h.CommandFrequency {
341 commands = append(commands, cmdCount{cmd, count})
342 }
343
344 sort.Slice(commands, func(i, j int) bool {
345 return commands[i].count > commands[j].count
346 })
347
348 var result []string
349 for i := 0; i < n && i < len(commands); i++ {
350 result = append(result, commands[i].cmd)
351 }
352
353 return result
354 }
355