返回 DeepSeek-Reasonix
session_v5_migration_legacy.go
根目录 / desktop / session_v5_migration_legacy.go
1 package main
2
3 import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "errors"
8 "os"
9 "path/filepath"
10 "slices"
11
12 "reasonix/internal/agent"
13 "reasonix/internal/fileutil"
14 "reasonix/internal/session"
15 )
16
17 func desktopLegacySourceFiles(path, pairedRoot string) []string {
18 files := legacyMigrationSourceFiles(path)
19 for _, root := range desktopLegacyStoreRoots(pairedRoot) {
20 files = append(files, canonicalMigrationSourceFiles(root, agent.BranchID(path))...)
21 }
22 return files
23 }
24
25 // Before the identity cutover, SessionDirV3 / ProjectSessionDirV3 used this
26 // sibling directory. Its presence is independent of the current v4 root.
27 func desktopLegacyStoreRoots(root string) []string {
28 if root == "" {
29 return nil
30 }
31 parent := filepath.Dir(root)
32 return []string{filepath.Join(parent, "sessions-v4"), filepath.Join(parent, "sessions-v3")}
33 }
34
35 func desktopLegacyPairedRoot(path, root string) (string, error) {
36 for _, candidate := range desktopLegacyStoreRoots(root) {
37 if _, err := os.Stat(filepath.Join(candidate, agent.BranchID(path))); err == nil {
38 return candidate, nil
39 } else if !os.IsNotExist(err) {
40 return "", err
41 }
42 }
43 return root, nil
44 }
45
46 func desktopLegacyHeadKey(path, head string) string {
47 digest := sha256.Sum256([]byte(canonicalRuntimeRoot(path) + "\x00head\x00" + head))
48 return hex.EncodeToString(digest[:])
49 }
50
51 // The cached head list is certified by the same source revision as imports.
52 // Completed DAGs therefore need only stat calls on later startups. The first
53 // head keeps the historical path key even when the old app changes selection.
54 func (a *App) migrateLegacyHeads(ctx context.Context, path string, source desktopMigrationSource) error {
55 key := desktopLegacyMigrationKey(path)
56 files := desktopLegacyMigrationFiles(path, source)
57 cp, err := newDesktopMigrationCheckpoint(source, key, files)
58 if err != nil {
59 return err
60 }
61 record := source.records[key]
62 if len(record.LegacyHeads) == 0 || record.LegacyHeadsRevision != cp.revision {
63 heads, err := session.LegacyMigrationHeads(ctx, path)
64 if err != nil {
65 return errors.Join(err, updateDesktopMigrationLedger(key, record.TargetSessionID, "failed", "source_heads"))
66 }
67 ids := []string{}
68 selected := ""
69 for _, head := range heads {
70 if head.Retired {
71 continue
72 }
73 ids = append(ids, head.ID)
74 if head.Selected {
75 selected = head.ID
76 }
77 }
78 if len(heads) == 0 {
79 ids = []string{""}
80 }
81 if len(ids) == 0 {
82 return errors.New("legacy DAG has no live heads")
83 }
84 primary := selected
85 if len(record.LegacyHeads) > 0 && slices.Contains(ids, record.LegacyHeads[0]) {
86 primary = record.LegacyHeads[0]
87 }
88 if index := slices.Index(ids, primary); index > 0 {
89 ids[0], ids[index] = ids[index], ids[0]
90 }
91 conversions, err := resolveDesktopConversionHeads(ctx, path, source.conversions[canonicalRuntimeRoot(path)])
92 if err != nil {
93 return errors.Join(err, updateDesktopMigrationLedger(key, record.TargetSessionID, "failed", "conversion_heads"))
94 }
95 if revision, err := desktopMigrationSourceRevision(files); err != nil || revision != cp.revision {
96 return errors.Join(errors.New("legacy source changed while listing heads"), err,
97 updateDesktopMigrationLedger(key, record.TargetSessionID, "failed", "source_changed"))
98 }
99 record, err = saveDesktopMigrationHeads(key, ids, selected, cp.revision, conversions)
100 if err != nil {
101 return err
102 }
103 }
104 source.legacyAdoption = record.LegacyAdoption
105 var joined error
106 for _, head := range record.LegacyHeads {
107 if err := ctx.Err(); err != nil {
108 return errors.Join(joined, err)
109 }
110 headKey := key
111 if head != record.LegacyPrimaryHead {
112 headKey = desktopLegacyHeadKey(path, head)
113 }
114 perHead := source
115 for _, converted := range record.LegacyConversions {
116 if converted.HeadID != head {
117 continue
118 }
119 perHead.headConversions = append(perHead.headConversions, converted)
120 if source.handledStores != nil {
121 source.handledStores[canonicalRuntimeRoot(filepath.Join(converted.Root, converted.SessionID))] = true
122 }
123 }
124 if len(perHead.headConversions) > 0 && head == record.LegacySelectedHead && source.pairedRoot != "" {
125 if err := perHead.includePairedConversion(path, head, headKey); err != nil {
126 joined = errors.Join(joined, err)
127 continue
128 }
129 }
130 joined = errors.Join(joined, a.migrateLegacyHead(ctx, path, perHead, "", head, headKey, head == record.LegacySelectedHead))
131 }
132 return joined
133 }
134
135 // The identity cutover can leave a current paired store without a Source field
136 // alongside provenance-linked conversions. Include it before suppressing its scan.
137 func (source *desktopMigrationSource) includePairedConversion(path, head, key string) error {
138 id := agent.BranchID(path)
139 dir := filepath.Join(source.pairedRoot, id)
140 manifest, err := readDesktopMigrationManifest(dir)
141 if os.IsNotExist(err) {
142 return nil
143 }
144 if err != nil {
145 return errors.Join(err, updateDesktopMigrationLedger(key, "", "failed", "paired_read"))
146 }
147 if manifest.SessionID != id {
148 return errors.Join(session.ErrDamagedStore, updateDesktopMigrationLedger(key, "", "failed", "paired_identity"))
149 }
150 for _, candidate := range source.headConversions {
151 if sameDesktopPath(filepath.Join(candidate.Root, candidate.SessionID), dir) {
152 return nil
153 }
154 }
155 source.headConversions = append([]desktopMigrationConversion{{Root: source.pairedRoot, SessionID: id, HeadID: head, Codec: manifest.Codec}}, source.headConversions...)
156 return nil
157 }
158
159 func saveDesktopMigrationHeads(key string, heads []string, selected, revision string, conversions []desktopMigrationConversion) (desktopMigrationRecord, error) {
160 desktopMigrationMu.Lock()
161 defer desktopMigrationMu.Unlock()
162 release, lockErr := lockDesktopMigrationLedger()
163 if lockErr != nil {
164 return desktopMigrationRecord{}, lockErr
165 }
166 defer release()
167 ledger, original, err := readDesktopMigrationLedgerFile()
168 if err != nil {
169 return desktopMigrationRecord{}, err
170 }
171 record := ledger.Records[key]
172 if record.LegacyPrimaryHead == "" && len(heads) > 0 {
173 record.LegacyPrimaryHead = heads[0]
174 }
175 if len(record.LegacyHeads) == 0 && record.LegacyAdoption == nil {
176 if record.Status == "completed" {
177 record.LegacyAdoption = &desktopMigrationReceipt{TargetSessionID: record.TargetSessionID, ContentDigest: record.ContentDigest, SourceRevision: record.SourceRevision}
178 } else if record.PreviousCompletion != nil {
179 receipt := *record.PreviousCompletion
180 record.LegacyAdoption = &receipt
181 }
182 }
183 record.SourceKey = key
184 record.LegacyHeads, record.LegacySelectedHead, record.LegacyHeadsRevision = heads, selected, revision
185 for i := range conversions {
186 for _, previous := range record.LegacyConversions {
187 if previous.Root == conversions[i].Root && previous.SessionID == conversions[i].SessionID && previous.HeadID == conversions[i].HeadID {
188 conversions[i].extra = previous.extra
189 }
190 }
191 }
192 record.LegacyConversions = conversions
193 ledger.Records[key] = record
194 body, err := marshalDesktopMigrationRecord(original, ledger, key)
195 if err != nil {
196 return record, err
197 }
198 if err := os.MkdirAll(filepath.Dir(desktopMigrationLedgerPath()), 0o700); err != nil {
199 return record, err
200 }
201 return record, fileutil.AtomicWriteFileStrict(desktopMigrationLedgerPath(), append(body, '\n'), 0o600)
202 }
203
204 func (a *App) migratePreviewSession(ctx context.Context, source desktopMigrationSource, id string) (retErr error) {
205 key := desktopCanonicalMigrationKey(source.root, id)
206 if source.versionFingerprint != "" {
207 key += ":review:" + source.versionFingerprint
208 }
209 cp, err := newDesktopMigrationCheckpoint(source, key, canonicalMigrationSourceFiles(source.root, id))
210 if err != nil {
211 return err
212 }
213 if handled, err := a.checkAdoptedMigrationSource(ctx, source, cp); handled || err != nil {
214 return err
215 }
216 if cp.unchanged() {
217 return a.completeRegisteredMigration(ctx, source, cp, cp.record.TargetSessionID, cp.record.ContentDigest)
218 }
219 stage, ref, cleanup, err := stageDesktopStoredPreview(ctx, filepath.Join(source.root, id))
220 if err != nil {
221 return errors.Join(err, updateDesktopMigrationLedger(key, id, "failed", "preview_import"))
222 }
223 defer cleanup()
224 return a.publishStagedMigration(ctx, source, cp, stage, ref, "", session.SessionOriginCanonicalImport)
225 }
226
226 lines GO