{"record":{"id":"a132bf2ee08fb482","repo":"charmbracelet/crush","slug":"get-tool-usage-w","errorCode":null,"errorMessage":"get tool usage: %w","messagePattern":"get tool usage: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/cmd/stats.go","lineNumber":642,"sourceCode":"\t\tstats.RecentActivity = append(stats.RecentActivity, DailyActivity{\n\t\t\tDay:          fmt.Sprintf(\"%v\", r.Day),\n\t\t\tSessionCount: r.SessionCount,\n\t\t\tTotalTokens:  nullFloat64ToInt64(r.TotalTokens),\n\t\t\tCost:         r.Cost.Float64,\n\t\t})\n\t}\n\n\t// Average response time.\n\tavgResp, err := queries.GetAverageResponseTime(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get average response time: %w\", err)\n\t}\n\tstats.AvgResponseTimeMs = toFloat64(avgResp) * 1000\n\n\t// Tool usage.\n\ttoolUsage, err := queries.GetToolUsage(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get tool usage: %w\", err)\n\t}\n\tfor _, t := range toolUsage {\n\t\tif name, ok := t.ToolName.(string); ok && name != \"\" {\n\t\t\tstats.ToolUsage = append(stats.ToolUsage, ToolUsage{\n\t\t\t\tToolName:  name,\n\t\t\t\tCallCount: t.CallCount,\n\t\t\t})\n\t\t}\n\t}\n\n\t// Hour/day heatmap.\n\theatmap, err := queries.GetHourDayHeatmap(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get hour day heatmap: %w\", err)\n\t}\n\tfor _, h := range heatmap {\n\t\tstats.HourDayHeatmap = append(stats.HourDayHeatmap, HourDayHeatmapPt{\n\t\t\tDayOfWeek:    int(h.DayOfWeek),","sourceCodeStart":624,"sourceCodeEnd":660,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/cmd/stats.go#L624-L660","documentation":"gatherStats in internal/cmd/stats.go wraps a failure from the sqlc-generated query queries.GetToolUsage(ctx), which aggregates per-tool call counts from the SQLite session/message tables. The %w wrapping preserves the underlying database error. It is thrown while building the stats report for `crush stats`.","triggerScenarios":"Running `crush stats` when the SQLite query for tool usage fails: DB file locked/corrupt, disk I/O error, missing schema (unmigrated DB), or a query timeout.","commonSituations":"Corrupt or partially migrated SQLite database in the config/data directory; another crush process holding a write lock; read-only filesystem or full disk.","solutions":["Read the wrapped cause (%w) to identify the exact SQLite failure (locked, corrupt, no such table).","Close other crush instances holding a lock on the database, then retry `crush stats`.","If the DB is corrupt, restore from a backup or move it aside and let crush recreate it (loses history).","Ensure the DB schema is current: run crush once normally so migrations apply, then rerun stats."],"exampleFix":"// before: opaque failure\nstats, err := gatherStats(ctx, queries)\n// after: inspect wrapped cause\nif err != nil {\n    var sqliteErr sqlite.Error\n    if errors.As(err, &sqliteErr) {\n        log.Printf(\"sqlite error code %d: %v\", sqliteErr.Code, sqliteErr)\n    }\n    return err\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"stats, err := gatherStats(ctx, queries)\nif err != nil {\n    var se sqlite.Error\n    if errors.As(err, &se) {\n        switch se.Code {\n        case sqlite.ErrLocked:\n            // retry after closing other crush instances\n        default:\n            return fmt.Errorf(\"stats unavailable: %w\", err)\n        }\n    }\n    return err\n}","preventionTips":["Avoid running multiple crush instances that write the same DB concurrently.","Keep the DB schema migrated before using `crush stats`.","Back up the SQLite file periodically to recover from corruption.","Always inspect the wrapped %w cause rather than the message alone."],"tags":["database","sqlite","cli","stats"],"backgroundTag":"sqlite-database-error","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}