返回 DeepSeek-Reasonix
history_search_snapshot_builder.go
根目录 / desktop / history_search_snapshot_builder.go
1 package main
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "os"
8 "path/filepath"
9 "strings"
10 "time"
11
12 "reasonix/internal/agent"
13 "reasonix/internal/history"
14 "reasonix/internal/historycatalog"
15 "reasonix/internal/retrieval"
16 )
17
18 type historySearchSnapshotBuild struct {
19 ctx context.Context
20 app *App
21 store *readSnapshotStore
22 snapshot *readSnapshot
23 catalog *historycatalog.Catalog
24 request HistorySearchRequest
25 active string
26 meta HistorySearchPage
27 fence *readSourceFence
28 overlays map[string]catalogRuntimeOverlay
29 terms []string
30 cutoff time.Time
31 needed map[string]map[historyTextKey]struct{}
32 order []string
33 sources map[string]*historySearchSource
34 }
35
36 // historySearchSource is one session file read once per build: the digest it
37 // had and the snippet of every candidate that points into it.
38 type historySearchSource struct {
39 digest string
40 covered bool
41 intact bool
42 snippets map[historyTextKey]string
43 }
44
45 type historyTextKey struct {
46 message, part int
47 kind string
48 }
49
50 var loadHistorySearchMessages = agent.LoadSessionDisplayMessages
51
52 func (a *App) buildHistorySearchSnapshot(ctx context.Context, store *readSnapshotStore, snap *readSnapshot, req HistorySearchRequest, targetPath, active string) error {
53 status := a.GetHistoryIndexStatus()
54 meta := HistorySearchPage{Status: status, Revision: status.Revision, Partial: status.State != "ready" || status.Pending > 0}
55 catalog := history.SharedCatalog()
56 if catalog == nil || req.Query == "" {
57 snap.metadata, _ = json.Marshal(meta)
58 return nil
59 }
60 candidates := &readSnapshot{}
61 defer store.dispose(candidates)
62 roots := historySearchRootFilter(a, req)
63 if targetPath != "" {
64 roots = nil
65 }
66 search := historycatalog.SearchRequest{Query: req.Query, Scope: req.Scope, WorkspaceRoot: req.WorkspaceRoot, SessionPath: targetPath, Kinds: req.Kinds, ToolName: req.ToolName, Roots: roots}
67 if err := catalog.CaptureSearch(ctx, search, func(row historycatalog.Candidate) error { return store.append(ctx, candidates, row) }); err != nil {
68 return err
69 }
70 terms, err := retrieval.QueryTerms(req.Query)
71 if err != nil {
72 return err
73 }
74 fence, err := a.newReadSourceFence(store, snap)
75 if err != nil {
76 return err
77 }
78 _, overlays := a.catalogRuntimeOverlays()
79 build := &historySearchSnapshotBuild{ctx: ctx, app: a, store: store, snapshot: snap, catalog: catalog, request: req, active: active, meta: meta, fence: fence, overlays: overlays, terms: terms, cutoff: time.Now()}
80 if err := build.run(candidates); err != nil {
81 return err
82 }
83 snap.validate = fence.freeze()
84 snap.metadata, err = json.Marshal(build.meta)
85 return err
86 }
87
88 // run keeps the bm25 order the catalog returned while reading each session file
89 // once: hits interleave sessions, so loading per hit rereads a file per row.
90 func (b *historySearchSnapshotBuild) run(candidates *readSnapshot) error {
91 b.needed, b.sources = map[string]map[historyTextKey]struct{}{}, map[string]*historySearchSource{}
92 if err := candidates.walk(b.ctx, b.collect); err != nil {
93 return err
94 }
95 for _, path := range b.order {
96 if err := b.loadSource(path, b.needed[path]); err != nil {
97 return err
98 }
99 delete(b.needed, path)
100 }
101 return candidates.walk(b.ctx, b.visit)
102 }
103
104 func (b *historySearchSnapshotBuild) candidate(encoded []byte) (historycatalog.Candidate, catalogRuntimeOverlay, bool, error) {
105 if err := b.ctx.Err(); err != nil {
106 return historycatalog.Candidate{}, catalogRuntimeOverlay{}, false, err
107 }
108 var row historycatalog.Candidate
109 if err := json.Unmarshal(encoded, &row); err != nil {
110 return row, catalogRuntimeOverlay{}, false, err
111 }
112 overlay := b.overlays[sessionRuntimeKey(row.SessionPath)]
113 keep := historyStatusMatches(b.request.Status, overlay.open, row.SessionPath == b.active) && historyTimeMatchesAt(row.LastActivityAt, b.request.TimeFilter, b.cutoff)
114 return row, overlay, keep, nil
115 }
116
117 func (b *historySearchSnapshotBuild) collect(encoded []byte) error {
118 row, _, keep, err := b.candidate(encoded)
119 if err != nil || !keep {
120 return err
121 }
122 keys, ok := b.needed[row.SessionPath]
123 if !ok {
124 keys = map[historyTextKey]struct{}{}
125 b.needed[row.SessionPath] = keys
126 b.order = append(b.order, row.SessionPath)
127 }
128 keys[historyTextKey{message: row.MessageIndex, part: row.PartIndex, kind: row.Kind}] = struct{}{}
129 return nil
130 }
131
132 func (b *historySearchSnapshotBuild) visit(encoded []byte) error {
133 row, overlay, keep, err := b.candidate(encoded)
134 if err != nil || !keep {
135 return err
136 }
137 src := b.sources[row.SessionPath]
138 if src.covered {
139 return nil
140 }
141 if !src.intact || row.ContentDigest == "" || src.digest != row.ContentDigest {
142 b.catalog.EnqueueExisting(b.ctx, row.SessionPath)
143 b.meta.Partial = true
144 return nil
145 }
146 snippet, ok := src.snippets[historyTextKey{message: row.MessageIndex, part: row.PartIndex, kind: row.Kind}]
147 if !ok {
148 b.catalog.EnqueueExisting(b.ctx, row.SessionPath)
149 b.meta.Partial = true
150 return nil
151 }
152 hit := HistorySearchHit{SessionPath: row.SessionPath, SessionID: strings.TrimSuffix(filepath.Base(row.SessionPath), filepath.Ext(row.SessionPath)), Source: row.Source, MessageIndex: row.MessageIndex, PartIndex: row.PartIndex, ContentDigest: src.digest, Role: row.Role, Kind: row.Kind, ToolName: row.ToolName, Snippet: snippet, Score: row.Score, SessionTitle: row.SessionTitle, TopicTitle: row.TopicTitle, WorkspaceRoot: row.WorkspaceRoot, LastActivityAt: row.LastActivityAt, Open: overlay.open, Running: overlay.running, Current: row.SessionPath == b.active}
153 return b.store.append(b.ctx, b.snapshot, hit)
154 }
155
156 func (b *historySearchSnapshotBuild) loadSource(path string, keys map[historyTextKey]struct{}) error {
157 ctx := b.ctx
158 src := &historySearchSource{}
159 b.sources[path] = src
160 if sessions := b.app.sessionCatalog.Load(); sessions != nil {
161 record, ok, err := sessions.GetSession(ctx, path)
162 if err != nil {
163 return err
164 }
165 src.covered = ok && record.RecoveryCopy
166 }
167 if src.covered {
168 return nil
169 }
170 if err := b.fence.add(ctx, path); err != nil {
171 if !errors.Is(err, os.ErrNotExist) {
172 return err
173 }
174 b.meta.Partial = true
175 return nil
176 }
177 messages, state, intact, err := loadHistorySearchMessages(path)
178 if errors.Is(err, os.ErrNotExist) {
179 b.meta.Partial = true
180 return nil
181 }
182 if err != nil {
183 return err
184 }
185 src.digest, src.intact = state.DigestHex, intact
186 src.snippets = make(map[historyTextKey]string, len(keys))
187 for key := range keys {
188 text, ok := desktopHistoryText(messages, historycatalog.Candidate{MessageIndex: key.message, PartIndex: key.part, Kind: key.kind})
189 if ok {
190 src.snippets[key] = retrieval.MakeSnippet(text, b.request.Query, b.terms, 240)
191 }
192 }
193 return nil
194 }
195
195 lines GO