返回 DeepSeek-Reasonix
historical_legacy_reconciliation.go
根目录 / desktop / historical_legacy_reconciliation.go
1 package main
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "os"
8 "sort"
9 "strings"
10
11 "reasonix/internal/agent"
12 "reasonix/internal/historywork"
13 "reasonix/internal/sessioncatalog"
14 "reasonix/internal/store"
15 )
16
17 // Reuse catalog metadata instead of enumerating legacy directories a second
18 // time on startup. Only paths with migration evidence require recovery work.
19 func (a *App) addHistoricalCatalogReceipts(ctx context.Context, add func(string, string, string, string, string)) error {
20 catalog := a.sessionCatalog.Load()
21 if catalog == nil {
22 return nil
23 }
24 ledger, err := readDesktopMigrationLedger()
25 if err != nil || len(ledger.Records) == 0 {
26 return err
27 }
28 receipts := historicalCompletedReceiptKeys(ledger)
29 return catalog.WithReadView(ctx, func(ctx context.Context) error {
30 req := sessioncatalog.SessionPageRequest{Scope: "all", Limit: 200}
31 for {
32 page, err := catalog.ListSessions(ctx, req)
33 if err != nil {
34 return err
35 }
36 for _, record := range page.Items {
37 if historicalLegacyHasReceipt(receipts, record.Path, "") {
38 add(record.Path, "legacy", record.Scope, record.WorkspaceRoot, "")
39 }
40 heads, err := catalog.ListHeads(ctx, record.Path)
41 if err != nil {
42 return err
43 }
44 for _, head := range heads {
45 if historicalLegacyHasReceipt(receipts, record.Path, head.ID) {
46 add(record.Path, "legacy", record.Scope, record.WorkspaceRoot, head.ID)
47 }
48 }
49 }
50 if page.NextCursor == "" || page.StaleCursor {
51 return nil
52 }
53 req.Cursor = page.NextCursor
54 }
55 })
56 }
57
58 // Index completion once per discovery, rather than scanning the entire ledger
59 // for each catalog path and head. Reviewed versions retain their base identity.
60 func historicalCompletedReceiptKeys(ledger desktopMigrationLedger) map[string]bool {
61 keys := map[string]bool{}
62 for id, receipt := range ledger.Records {
63 if receipt.Status == "completed" || receipt.PreviousCompletion != nil {
64 key, _, _ := strings.Cut(id, ":review:")
65 keys[key] = true
66 }
67 }
68 return keys
69 }
70
71 func historicalLegacyHasReceipt(receipts map[string]bool, path, head string) bool {
72 key := desktopLegacyMigrationKey(path)
73 if head != "" {
74 key = desktopLegacyHeadKey(path, head)
75 }
76 return receipts[key]
77 }
78
79 // Explicit discovery can run before the metadata catalog exists. A bounded,
80 // current head index supplies receipt identities without replaying the JSONL.
81 func addHistoricalLegacyReceiptHeads(sources map[string]historicalSource, add func(string, string, string, string, string)) error {
82 ledger, err := readDesktopMigrationLedger()
83 if err != nil || len(ledger.Records) == 0 {
84 return err
85 }
86 receipts := historicalCompletedReceiptKeys(ledger)
87 for _, source := range sources {
88 if source.format != "legacy" || source.head != "" {
89 continue
90 }
91 if info, err := os.Stat(store.SessionEventIndex(source.path)); err != nil || info.Size() > historywork.ReadChunk {
92 continue
93 }
94 index, err := agent.ReadSessionHeadIndex(source.path)
95 if err != nil || index == nil || !index.Current(source.path) {
96 continue
97 }
98 for _, head := range index.Heads {
99 if historicalLegacyHasReceipt(receipts, source.path, head.ID) {
100 add(source.path, "legacy", source.scope, source.root, head.ID)
101 }
102 }
103 }
104 return nil
105 }
106
107 func (a *App) reconcileHistoricalLegacyCatalog(ctx context.Context, sources map[string]historicalSource) error {
108 for key, source := range sources {
109 if source.format != "legacy" {
110 continue
111 }
112 release, err := a.historyMaintenance.BackgroundSlice(ctx, false)
113 if err != nil {
114 return err
115 }
116 err = a.reconcileHistoricalCatalogSource(ctx, key, source, "ok")
117 release(historywork.ReadChunk)
118 if errors.Is(err, context.Canceled) {
119 return err
120 }
121 if historicalSourceBusyError(err) || errors.Is(err, os.ErrPermission) {
122 continue
123 }
124 c := &a.historicalImports
125 c.mu.Lock()
126 c.initialize(ctx)
127 c.sources[key] = source
128 wasUnavailable := c.unavailableSources[key]
129 if err != nil {
130 c.unavailableSources[key] = true
131 } else {
132 delete(c.unavailableSources, key)
133 }
134 changed := wasUnavailable != c.unavailableSources[key]
135 if changed {
136 c.catalogRevision++
137 }
138 c.mu.Unlock()
139 if changed {
140 a.emitProjectTreeChangedEvent()
141 }
142 }
143 return ctx.Err()
144 }
145
146 func (a *App) unavailableHistoricalSource(key string) bool {
147 c := &a.historicalImports
148 c.mu.Lock()
149 defer c.mu.Unlock()
150 return c.unavailableSources[key]
151 }
152
153 func (a *App) excludeUnavailableHistoricalSources(existing string) string {
154 keys := []string{}
155 if existing != "" {
156 _ = json.Unmarshal([]byte(existing), &keys)
157 }
158 c := &a.historicalImports
159 c.mu.Lock()
160 paths := []string{}
161 for key := range c.unavailableSources {
162 keys = append(keys, key)
163 if source, ok := c.sources[key]; ok {
164 paths = append(paths, source.path)
165 }
166 }
167 c.mu.Unlock()
168 // The lazy catalog uses path identities for single-head files. Multi-head
169 // workspaces use the materialized adapter and retain per-head exclusions.
170 for _, path := range paths {
171 keys = append(keys, desktopSourceKey(path, ""))
172 }
173 if len(keys) == 0 {
174 return existing
175 }
176 sort.Strings(keys)
177 body, _ := json.Marshal(keys)
178 return string(body)
179 }
180
180 lines GO