fix(picker): snapshot session fields under the session lock before sorting
- commit
- 1521b5b10f7facc5fa9b2cb78a65fdd84c86c1c1
- author
- brice · · 2026-07-29 15:35 UTC
- parent
- d2bbb2e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +19 -8
M internal/usecase/daemon/picker.go +19 -8
| @@ -87,17 +87,28 @@ func (d *Daemon) pickerViews(cur *session) ([]picker.SessionView, picker.SourceF | |||
| 87 | 87 | } | |
| 88 | 88 | } | |
| 89 | 89 | d.mu.Unlock() | |
| 90 | - | // Snapshot mruAt once: comparators must not observe concurrent touchMRU | |
| 91 | - | // updates mid-sort. | |
| 92 | - | mru := make(map[*session]uint64, len(sessions)) | |
| 90 | + | // Snapshot mruAt, name, and ephemeral once: comparators must not observe | |
| 91 | + | // concurrent touchMRU updates or a renameSession mutation (which mutates | |
| 92 | + | // name/ephemeral under sess.mu) mid-sort. | |
| 93 | + | type pickerSortSnapshot struct { | |
| 94 | + | mruAt uint64 | |
| 95 | + | name string | |
| 96 | + | ephemeral bool | |
| 97 | + | } | |
| 98 | + | snap := make(map[*session]pickerSortSnapshot, len(sessions)) | |
| 93 | 99 | for _, s := range sessions { | |
| 94 | - | mru[s] = s.mruAt.Load() | |
| 100 | + | mruAt := s.mruAt.Load() | |
| 101 | + | s.mu.Lock() | |
| 102 | + | name := s.name | |
| 103 | + | ephemeral := s.ephemeral | |
| 104 | + | s.mu.Unlock() | |
| 105 | + | snap[s] = pickerSortSnapshot{mruAt: mruAt, name: name, ephemeral: ephemeral} | |
| 95 | 106 | } | |
| 96 | 107 | sort.Slice(sessions, func(i, j int) bool { | |
| 97 | - | if mru[sessions[i]] != mru[sessions[j]] { | |
| 98 | - | return mru[sessions[i]] > mru[sessions[j]] | |
| 108 | + | if snap[sessions[i]].mruAt != snap[sessions[j]].mruAt { | |
| 109 | + | return snap[sessions[i]].mruAt > snap[sessions[j]].mruAt | |
| 99 | 110 | } | |
| 100 | - | return sessions[i].name < sessions[j].name | |
| 111 | + | return snap[sessions[i]].name < snap[sessions[j]].name | |
| 101 | 112 | }) | |
| 102 | 113 | sort.Slice(stopped, func(i, j int) bool { | |
| 103 | 114 | if stopped[i].lastUsedSeq != stopped[j].lastUsedSeq { | |
| @@ -109,7 +120,7 @@ func (d *Daemon) pickerViews(cur *session) ([]picker.SessionView, picker.SourceF | |||
| 109 | 120 | // Stable partition: named sessions first, ephemeral after, each keeping | |
| 110 | 121 | // its MRU order from the sort above. | |
| 111 | 122 | sort.SliceStable(sessions, func(i, j int) bool { | |
| 112 | - | return !sessions[i].ephemeral && sessions[j].ephemeral | |
| 123 | + | return !snap[sessions[i]].ephemeral && snap[sessions[j]].ephemeral | |
| 113 | 124 | }) | |
| 114 | 125 | } | |
| 115 | 126 | ||
| @@ -87,17 +87,28 @@ func (d *Daemon) pickerViews(cur *session) ([]picker.SessionView, picker.SourceF | |||||
| 87 | } | 87 | } | ||
| 88 | } | 88 | } | ||
| 89 | d.mu.Unlock() | 89 | d.mu.Unlock() | ||
| 90 | - | // Snapshot mruAt once: comparators must not observe concurrent touchMRU | 90 | + | // Snapshot mruAt, name, and ephemeral once: comparators must not observe |
| 91 | - | // updates mid-sort. | 91 | + | // concurrent touchMRU updates or a renameSession mutation (which mutates |
| 92 | - | mru := make(map[*session]uint64, len(sessions)) | 92 | + | // name/ephemeral under sess.mu) mid-sort. |
| 93 | + | type pickerSortSnapshot struct { | |||
| 94 | + | mruAt uint64 | |||
| 95 | + | name string | |||
| 96 | + | ephemeral bool | |||
| 97 | + | } | |||
| 98 | + | snap := make(map[*session]pickerSortSnapshot, len(sessions)) | |||
| 93 | for _, s := range sessions { | 99 | for _, s := range sessions { | ||
| 94 | - | mru[s] = s.mruAt.Load() | 100 | + | mruAt := s.mruAt.Load() |
| 101 | + | s.mu.Lock() | |||
| 102 | + | name := s.name | |||
| 103 | + | ephemeral := s.ephemeral | |||
| 104 | + | s.mu.Unlock() | |||
| 105 | + | snap[s] = pickerSortSnapshot{mruAt: mruAt, name: name, ephemeral: ephemeral} | |||
| 95 | } | 106 | } | ||
| 96 | sort.Slice(sessions, func(i, j int) bool { | 107 | sort.Slice(sessions, func(i, j int) bool { | ||
| 97 | - | if mru[sessions[i]] != mru[sessions[j]] { | 108 | + | if snap[sessions[i]].mruAt != snap[sessions[j]].mruAt { |
| 98 | - | return mru[sessions[i]] > mru[sessions[j]] | 109 | + | return snap[sessions[i]].mruAt > snap[sessions[j]].mruAt |
| 99 | } | 110 | } | ||
| 100 | - | return sessions[i].name < sessions[j].name | 111 | + | return snap[sessions[i]].name < snap[sessions[j]].name |
| 101 | }) | 112 | }) | ||
| 102 | sort.Slice(stopped, func(i, j int) bool { | 113 | sort.Slice(stopped, func(i, j int) bool { | ||
| 103 | if stopped[i].lastUsedSeq != stopped[j].lastUsedSeq { | 114 | if stopped[i].lastUsedSeq != stopped[j].lastUsedSeq { | ||
| @@ -109,7 +120,7 @@ func (d *Daemon) pickerViews(cur *session) ([]picker.SessionView, picker.SourceF | |||||
| 109 | // Stable partition: named sessions first, ephemeral after, each keeping | 120 | // Stable partition: named sessions first, ephemeral after, each keeping | ||
| 110 | // its MRU order from the sort above. | 121 | // its MRU order from the sort above. | ||
| 111 | sort.SliceStable(sessions, func(i, j int) bool { | 122 | sort.SliceStable(sessions, func(i, j int) bool { | ||
| 112 | - | return !sessions[i].ephemeral && sessions[j].ephemeral | 123 | + | return !snap[sessions[i]].ephemeral && snap[sessions[j]].ephemeral |
| 113 | }) | 124 | }) | ||
| 114 | } | 125 | } | ||
| 115 | 126 | ||||