Problem
SessionStore.Snapshot() in internal/metrics/session.go:156 does cp := *s which shallow-copies the Spin field, including the Reasons []string slice. The snapshot and the live SessionMetrics share the same backing array.
Currently safe because SpinDetector.Check() always returns a fresh SpinResult with a newly-allocated Reasons slice. But if Check() is ever changed to accumulate reasons across calls, the background goroutine would mutate the backing array while the TUI goroutine reads it.
Note: FilesChanged (map) IS deep-copied on lines 157-160, but Reasons is not.
Impact
Latent — safe today, one refactor away from a data race.
Suggested Fix
Deep-copy Spin.Reasons in Snapshot():
if len(s.Spin.Reasons) > 0 {
cp.Spin.Reasons = make([]string, len(s.Spin.Reasons))
copy(cp.Spin.Reasons, s.Spin.Reasons)
}
Files
internal/metrics/session.go:150-172
Problem
SessionStore.Snapshot()ininternal/metrics/session.go:156doescp := *swhich shallow-copies theSpinfield, including theReasons []stringslice. The snapshot and the liveSessionMetricsshare the same backing array.Currently safe because
SpinDetector.Check()always returns a freshSpinResultwith a newly-allocatedReasonsslice. But ifCheck()is ever changed to accumulate reasons across calls, the background goroutine would mutate the backing array while the TUI goroutine reads it.Note:
FilesChanged(map) IS deep-copied on lines 157-160, butReasonsis not.Impact
Latent — safe today, one refactor away from a data race.
Suggested Fix
Deep-copy
Spin.ReasonsinSnapshot():Files
internal/metrics/session.go:150-172