diff --git a/internal/im/daemon_bridge.go b/internal/im/daemon_bridge.go index dac45bed7..91260f962 100644 --- a/internal/im/daemon_bridge.go +++ b/internal/im/daemon_bridge.go @@ -1234,11 +1234,15 @@ func daemonSessionTurnIndex(ses *session.Session) int { return 0 } last := 0 - if n := len(ses.UsageHistory); n > 0 && ses.UsageHistory[n-1].TurnIndex > last { - last = ses.UsageHistory[n-1].TurnIndex - } - if n := len(ses.Metrics); n > 0 && ses.Metrics[n-1].TurnIndex > last { - last = ses.Metrics[n-1].TurnIndex + // #3091: read through the #3086/#3087 lock-guarded snapshot accessors - + // the bare slice reads raced the append callbacks on the same slices. + // Construction-time-only caller keeps the window near zero (low), but + // the accessor comments name exactly this cross-goroutine use. + if hist := ses.UsageHistorySnapshot(); len(hist) > 0 && hist[len(hist)-1].TurnIndex > last { + last = hist[len(hist)-1].TurnIndex + } + if mets := ses.MetricsSnapshot(); len(mets) > 0 && mets[len(mets)-1].TurnIndex > last { + last = mets[len(mets)-1].TurnIndex } return last } diff --git a/internal/im/zz_issue3091_test.go b/internal/im/zz_issue3091_test.go new file mode 100644 index 000000000..df4c0f7a7 --- /dev/null +++ b/internal/im/zz_issue3091_test.go @@ -0,0 +1,35 @@ +package im + +// Regression probe for #3091: daemonSessionTurnIndex must read the shared +// UsageHistory/Metrics slices through the #3086/#3087 lock-guarded +// snapshot accessors instead of bare slice indexing. + +import ( + "testing" + + "github.com/topcheer/ggcode/internal/metrics" + "github.com/topcheer/ggcode/internal/session" +) + +func TestIssue3091_TurnIndexUsesSnapshots(t *testing.T) { + if got := daemonSessionTurnIndex(nil); got != 0 { + t.Fatalf("nil session: got %d, want 0", got) + } + ses := &session.Session{} + if got := daemonSessionTurnIndex(ses); got != 0 { + t.Fatalf("empty session: got %d, want 0", got) + } + + // TurnIndex from the last usage entry wins over an older metric. + ses.AddUsageHistoryEntry(session.UsageEntry{TurnIndex: 3}) + ses.AppendMetricEvent(metrics.MetricEvent{TurnIndex: 1}) + if got := daemonSessionTurnIndex(ses); got != 3 { + t.Fatalf("usage-max: got %d, want 3", got) + } + + // A newer metric entry wins over the older usage entry. + ses.AppendMetricEvent(metrics.MetricEvent{TurnIndex: 7}) + if got := daemonSessionTurnIndex(ses); got != 7 { + t.Fatalf("metric-max: got %d, want 7", got) + } +}