charmbracelet/crush · error

get tool usage: %w

Error message

get tool usage: %w

What it means

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`.

Source

Thrown at internal/cmd/stats.go:642

		stats.RecentActivity = append(stats.RecentActivity, DailyActivity{
			Day:          fmt.Sprintf("%v", r.Day),
			SessionCount: r.SessionCount,
			TotalTokens:  nullFloat64ToInt64(r.TotalTokens),
			Cost:         r.Cost.Float64,
		})
	}

	// Average response time.
	avgResp, err := queries.GetAverageResponseTime(ctx)
	if err != nil {
		return nil, fmt.Errorf("get average response time: %w", err)
	}
	stats.AvgResponseTimeMs = toFloat64(avgResp) * 1000

	// Tool usage.
	toolUsage, err := queries.GetToolUsage(ctx)
	if err != nil {
		return nil, fmt.Errorf("get tool usage: %w", err)
	}
	for _, t := range toolUsage {
		if name, ok := t.ToolName.(string); ok && name != "" {
			stats.ToolUsage = append(stats.ToolUsage, ToolUsage{
				ToolName:  name,
				CallCount: t.CallCount,
			})
		}
	}

	// Hour/day heatmap.
	heatmap, err := queries.GetHourDayHeatmap(ctx)
	if err != nil {
		return nil, fmt.Errorf("get hour day heatmap: %w", err)
	}
	for _, h := range heatmap {
		stats.HourDayHeatmap = append(stats.HourDayHeatmap, HourDayHeatmapPt{
			DayOfWeek:    int(h.DayOfWeek),

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Read the wrapped cause (%w) to identify the exact SQLite failure (locked, corrupt, no such table).
  2. Close other crush instances holding a lock on the database, then retry `crush stats`.
  3. If the DB is corrupt, restore from a backup or move it aside and let crush recreate it (loses history).
  4. Ensure the DB schema is current: run crush once normally so migrations apply, then rerun stats.

Example fix

// before: opaque failure
stats, err := gatherStats(ctx, queries)
// after: inspect wrapped cause
if err != nil {
    var sqliteErr sqlite.Error
    if errors.As(err, &sqliteErr) {
        log.Printf("sqlite error code %d: %v", sqliteErr.Code, sqliteErr)
    }
    return err
}
Defensive patterns

Strategy: try-catch

Try / catch

stats, err := gatherStats(ctx, queries)
if err != nil {
    var se sqlite.Error
    if errors.As(err, &se) {
        switch se.Code {
        case sqlite.ErrLocked:
            // retry after closing other crush instances
        default:
            return fmt.Errorf("stats unavailable: %w", err)
        }
    }
    return err
}

Prevention

When it happens

Trigger: 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.

Common situations: Corrupt or partially migrated SQLite database in the config/data directory; another crush process holding a write lock; read-only filesystem or full disk.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/a132bf2ee08fb482. Report an issue: GitHub.