返回 DeepSeek-Reasonix
catalog_snapshot.go
根目录 / internal / skill / catalog_snapshot.go
1 package skill
2
3 import (
4 "context"
5 "fmt"
6 "os"
7 "sort"
8 "strings"
9 )
10
11 func (s *Store) discoverSkillsUncached(ctx context.Context) ([]Skill, map[string]Skill) {
12 if s == nil || s.disableDiscovery {
13 return nil, nil
14 }
15 var out []Skill
16 for _, r := range s.roots() {
17 if ctx.Err() != nil {
18 return nil, nil
19 }
20 if r.Status != StatusOK {
21 continue
22 }
23 for _, sk := range s.discoverRoot(ctx, r) {
24 if s.disabledName(sk.Name) {
25 continue
26 }
27 if len(r.plugins) == 0 {
28 out = append(out, sk)
29 continue
30 }
31 for _, plugin := range r.plugins {
32 owned := sk
33 owned.Plugin = plugin
34 if r.forceSubagent {
35 owned.SlashPrefix = plugin + ":agent"
36 }
37 out = append(out, owned)
38 }
39 }
40 }
41 if !s.disableBuiltins {
42 for _, sk := range builtinSkills() {
43 if !s.disabledName(sk.Name) {
44 out = append(out, skillCandidate(sk))
45 }
46 }
47 }
48 builtins := map[string]Skill{}
49 if !s.disableBuiltins {
50 for _, sk := range builtinSkills() {
51 if !s.disabledName(sk.Name) {
52 builtins[sk.Name] = sk
53 }
54 }
55 }
56 return out, builtins
57 }
58
59 func skillCandidate(skill Skill) Skill {
60 skill.Body = ""
61 return skill
62 }
63
64 func enabledFromDiscovered(discovered []Skill) ([]Skill, map[string]Skill) {
65 byName := map[string]Skill{}
66 for _, sk := range discovered {
67 if _, dup := byName[sk.Name]; !dup {
68 byName[sk.Name] = sk
69 }
70 }
71 out := make([]Skill, 0, len(byName))
72 for _, sk := range byName {
73 out = append(out, sk)
74 }
75 sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
76 return out, byName
77 }
78
79 func cloneSkills(in []Skill) []Skill {
80 out := make([]Skill, len(in))
81 for i, sk := range in {
82 out[i] = sk
83 out[i].AllowedTools = append([]string(nil), sk.AllowedTools...)
84 out[i].Triggers = append([]string(nil), sk.Triggers...)
85 out[i].NegativeTriggers = append([]string(nil), sk.NegativeTriggers...)
86 out[i].Requires = append([]string(nil), sk.Requires...)
87 out[i].Profiles = append([]string(nil), sk.Profiles...)
88 out[i].InvalidProfiles = append([]string(nil), sk.InvalidProfiles...)
89 }
90 return out
91 }
92
93 func cloneSkill(sk Skill) Skill { return cloneSkills([]Skill{sk})[0] }
94
95 func (s *Store) buildCatalog(ctx context.Context, generation uint64) {
96 discovered, builtins := s.discoverSkillsUncached(ctx)
97 if ctx.Err() != nil {
98 s.catalogMu.Lock()
99 if s.catalogFlight != nil && s.catalogFlight.generation == generation {
100 close(s.catalogFlight.done)
101 s.catalogFlight = nil
102 }
103 s.catalogMu.Unlock()
104 return
105 }
106 enabled, byName := enabledFromDiscovered(discovered)
107 built := &catalogSnapshot{
108 version: generation, rootSig: s.rootSignature(), discovered: cloneSkills(discovered), enabled: cloneSkills(enabled),
109 byName: byName, slash: VisibleSlashSkills(discovered), builtins: builtins,
110 }
111 s.catalogMu.Lock()
112 if s.catalogGen == generation {
113 s.catalog = built
114 }
115 if s.catalogFlight != nil && s.catalogFlight.generation == generation {
116 close(s.catalogFlight.done)
117 s.catalogFlight = nil
118 }
119 s.catalogMu.Unlock()
120 }
121
122 func (s *Store) rootSignature() string {
123 if s == nil {
124 return ""
125 }
126 var b strings.Builder
127 for _, root := range s.roots() {
128 fmt.Fprintf(&b, "%s\x00%s\x00", root.Dir, root.Status)
129 if info, err := os.Stat(root.Dir); err == nil {
130 fmt.Fprintf(&b, "%d\x00%d\x00", info.ModTime().UnixNano(), info.Size())
131 }
132 }
133 return b.String()
134 }
135
136 func (s *Store) invalidateChangedRoots() {
137 if s == nil {
138 return
139 }
140 sig := s.rootSignature()
141 s.catalogMu.Lock()
142 if s.catalog != nil && s.catalog.rootSig != sig {
143 s.catalogGen++
144 }
145 s.catalogMu.Unlock()
146 }
147
148 // Snapshot returns one immutable catalog generation. Concurrent cold callers
149 // share one scan. A cancelled waiter does not cancel that shared scan; when an
150 // older complete snapshot exists it is returned explicitly marked stale.
151 func (s *Store) Snapshot(ctx context.Context) (CatalogSnapshot, error) {
152 if s == nil {
153 return CatalogSnapshot{Complete: true}, nil
154 }
155 if ctx == nil {
156 ctx = context.Background()
157 }
158 if err := ctx.Err(); err != nil {
159 return CatalogSnapshot{}, err
160 }
161 if s.autoWatch {
162 s.ensureWatcher()
163 }
164 // One initial discovery plus at most two retries when invalidation races
165 // publication. Persistent churn returns the last complete generation.
166 for range 3 {
167 s.catalogMu.Lock()
168 generation := s.catalogGen
169 if s.catalog != nil && s.catalog.version == generation {
170 snapshot := CatalogSnapshot{Version: generation, Complete: true, Candidates: cloneSkills(s.catalog.enabled)}
171 s.catalogMu.Unlock()
172 return snapshot, nil
173 }
174 stale := s.catalog
175 flight := s.catalogFlight
176 if flight == nil {
177 scanCtx, cancel := context.WithCancel(context.Background())
178 flight = &catalogFlight{generation: generation, done: make(chan struct{}), cancel: cancel}
179 s.catalogFlight = flight
180 s.discoveryScans++
181 go s.buildCatalog(scanCtx, generation)
182 }
183 done := flight.done
184 s.catalogMu.Unlock()
185 select {
186 case <-ctx.Done():
187 if stale != nil {
188 return CatalogSnapshot{Version: stale.version, Complete: false, Stale: true, Candidates: cloneSkills(stale.enabled)}, nil
189 }
190 return CatalogSnapshot{}, ctx.Err()
191 case <-done:
192 }
193 }
194 s.catalogMu.Lock()
195 defer s.catalogMu.Unlock()
196 if s.catalog != nil {
197 return CatalogSnapshot{Version: s.catalog.version, Complete: false, Stale: true, Candidates: cloneSkills(s.catalog.enabled)}, nil
198 }
199 return CatalogSnapshot{}, fmt.Errorf("skill catalog changed during all three discovery attempts")
200 }
201
202 // Close releases this store's directory subscriptions. It is idempotent; a
203 // closed store remains readable from its last complete snapshot but performs
204 // no further automatic invalidation.
205 func (s *Store) Invalidate(_ string) {
206 if s == nil {
207 return
208 }
209 s.catalogMu.Lock()
210 s.catalogGen++
211 s.catalogMu.Unlock()
212 }
213
214 // DiscoveryScans exposes the deterministic scan count for diagnostics and
215 // complexity tests. Warm reads leave it unchanged.
216 func (s *Store) DiscoveryScans() uint64 {
217 if s == nil {
218 return 0
219 }
220 s.catalogMu.Lock()
221 defer s.catalogMu.Unlock()
222 return s.discoveryScans
223 }
224
225 func (s *Store) catalogSnapshot() *catalogSnapshot {
226 _, _ = s.Snapshot(context.Background())
227 s.catalogMu.Lock()
228 defer s.catalogMu.Unlock()
229 return s.catalog
230 }
231
232 func (s *Store) discoveredSkills() []Skill {
233 s.invalidateChangedRoots()
234 snapshot := s.catalogSnapshot()
235 if snapshot == nil {
236 return nil
237 }
238 return cloneSkills(snapshot.discovered)
239 }
240
241 func (s *Store) enabledSkills() []Skill {
242 s.invalidateChangedRoots()
243 snapshot := s.catalogSnapshot()
244 if snapshot == nil {
245 return nil
246 }
247 return cloneSkills(snapshot.enabled)
248 }
249
250 // List returns every model-visible skill, deduped by its bare internal name
251 // (first/highest-priority root wins) and sorted for a cache-stable index.
252 // Role-setting profiles do not filter this surface.
253
253 lines GO