From 42b745aeb30737a29da430346efc911b3776ff33 Mon Sep 17 00:00:00 2001 From: Junjun Zhang Date: Fri, 2 Oct 2026 23:23:01 +0800 Subject: [PATCH] fix(im): daemonSessionTurnIndex reads UsageHistory/Metrics via snapshot accessors (#3091) #3086/#3087 introduced the lock-guarded snapshot accessors for cross-goroutine reads of the shared slices, but this helper kept two bare slice reads (len + last element). The sole caller runs once at bridge construction before the collectors register, so the window is near zero (low, consistency cleanup, not a reproducible fault) - but the accessor comments name exactly this use. Replaced both reads with UsageHistorySnapshot()/MetricsSnapshot(). Verified: go build + internal/im tests green under -race; new zz_issue3091 probe (nil/empty/max-from-usage/max-from-metric). --- internal/im/daemon_bridge.go | 14 ++++++++----- internal/im/zz_issue3091_test.go | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 internal/im/zz_issue3091_test.go 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) + } +}