返回 DeepSeek-Reasonix
session_migration_registry.go
根目录 / desktop / session_migration_registry.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "log/slog"
7 "os"
8 "path/filepath"
9
10 "reasonix/desktop/internal/legacycleanup"
11 "reasonix/desktop/internal/workspacestate"
12 filelock "reasonix/internal/identitylock"
13 "reasonix/internal/session"
14 )
15
16 func migrationCheckpointPath(cp desktopMigrationCheckpoint) (string, string) {
17 path := cp.files[0]
18 if filepath.Base(path) == "manifest.json" {
19 return filepath.Dir(path), "canonical"
20 }
21 return path, "legacy"
22 }
23
24 // Reconcile adoption independently of content comparison: the destination may
25 // have been continued, archived or purged since this receipt was written.
26 func (a *App) completeRegisteredMigration(ctx context.Context, source desktopMigrationSource, cp desktopMigrationCheckpoint, id, digest string) error {
27 path, format := migrationCheckpointPath(cp)
28 state, err := a.workspaceRegistry().Load(ctx)
29 if err != nil {
30 return err
31 }
32 if state.SessionStates[id].Lifecycle == workspacestate.Deleted {
33 return cp.complete(id, digest)
34 }
35 ref := session.SessionRef{HostID: localDesktopHostID, SessionID: id}
36 if _, err := a.desktopSessionService("").Query().Stat(ctx, ref); err != nil {
37 if errors.Is(err, session.ErrSessionNotFound) {
38 return a.sourceRecovery(ctx, path, format, "adopted_target_missing", source.scope, source.workspaceRoot, source.headID)
39 }
40 return err
41 }
42 workspace, err := a.ensureDesktopMigrationWorkspace(ctx, source)
43 if err != nil {
44 return err
45 }
46 fingerprint, err := desktopSourceFingerprint(path)
47 if err != nil {
48 return err
49 }
50 key := source.mappingKey(path)
51 if mapping, exists := state.SourceMappings[key]; exists && source.operationID == "" {
52 if mapping.SessionID != id {
53 return workspacestate.ErrMutationConflict
54 }
55 if _, err := a.canonicalSessionWorkspace(ctx, ref); err == nil {
56 return cp.complete(id, digest)
57 }
58 }
59 if hook := a.desktopSessions.beforeMigrationRegistryCommit; hook != nil {
60 if err := hook(); err != nil {
61 return errors.Join(err, updateDesktopMigrationLedger(cp.key, id, "failed", "registry", digest))
62 }
63 }
64 if err := a.commitDesktopImport(ctx, source, path, format, fingerprint, id, workspace); err != nil {
65 return err
66 }
67 if format == "legacy" {
68 if err := a.bindLegacyCleanupMigration(ctx, path, source.headID, id, workspace); err != nil &&
69 !errors.Is(err, legacycleanup.ErrNotInitialized) && !errors.Is(err, errLegacyCleanupStateChanged) {
70 slog.Warn("desktop: legacy cleanup migration binding unavailable")
71 }
72 }
73 return cp.complete(id, digest)
74 }
75
76 func (a *App) prepareRegisteredMigration(ctx context.Context, source desktopMigrationSource, cp desktopMigrationCheckpoint, id, workspace string) error {
77 state, err := a.workspaceRegistry().Load(ctx)
78 if err != nil {
79 return err
80 }
81 if _, exists := state.Workspaces[workspace]; !exists {
82 return errors.Join(workspacestate.ErrWorkspaceNotFound, updateDesktopMigrationLedger(cp.key, id, "failed", "registry"))
83 }
84 path, _ := migrationCheckpointPath(cp)
85 fingerprint, err := desktopSourceFingerprint(path)
86 if err != nil {
87 return err
88 }
89 _, err = a.prepareDesktopImport(ctx, source, path, fingerprint, id, workspace)
90 return err
91 }
92
93 func (a *App) resolveRegisteredMigrationTarget(ctx context.Context, source desktopMigrationSource, cp desktopMigrationCheckpoint, preferredID, digest string) (string, bool, error) {
94 path, _ := migrationCheckpointPath(cp)
95 fingerprint, err := desktopSourceFingerprint(path)
96 if err != nil {
97 return "", false, err
98 }
99 return a.resolveDesktopImportTarget(ctx, a.desktopSessionService("").Query(), preferredID, cp.key, source.mappingKey(path), digest, path, fingerprint, source.headID)
100 }
101
102 func (a *App) quarantineChangedMigration(ctx context.Context, source desktopMigrationSource, cp desktopMigrationCheckpoint, digest string) (bool, error) {
103 if source.operationID != "" || !cp.completed() || cp.matchesCompletedContent(digest) {
104 return false, nil
105 }
106 path, format := migrationCheckpointPath(cp)
107 // The old path receipt adopted the selected head, not every head. Adding
108 // an independently identified head is discovery, not a changed adoption.
109 if source.headID != "" && source.legacyAdoption != nil && cp.record.TargetSessionID == source.legacyAdoption.TargetSessionID {
110 state, err := a.workspaceRegistry().Load(ctx)
111 if err != nil {
112 return true, err
113 }
114 if _, exists := state.SourceMappings[desktopSourceKey(path, source.headID)]; !exists {
115 return false, nil
116 }
117 }
118 return true, a.sourceRecovery(ctx, path, format, "source_changed_after_adoption", source.scope, source.workspaceRoot, source.headID)
119 }
120
121 func lockDesktopMigrationLedger() (func(), error) {
122 path := desktopMigrationLedgerPath()
123 if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
124 return nil, err
125 }
126 return filelock.TryAcquire(path + ".lock")
127 }
128
129 // Registry fingerprints distinguish a metadata-only stat change from a new
130 // historical version without converting or publishing the source again.
131 func (a *App) checkAdoptedMigrationSource(ctx context.Context, source desktopMigrationSource, cp desktopMigrationCheckpoint) (bool, error) {
132 if !cp.completed() || source.operationID != "" {
133 return false, nil
134 }
135 path, format := migrationCheckpointPath(cp)
136 state, err := a.workspaceRegistry().Load(ctx)
137 if err != nil {
138 return true, err
139 }
140 if state.SessionStates[cp.record.TargetSessionID].Lifecycle == workspacestate.Deleted {
141 return true, nil
142 }
143 mapping, exists := state.SourceMappings[source.mappingKey(path)]
144 if !exists {
145 return false, nil
146 }
147 fingerprint, err := desktopSourceFingerprint(path)
148 if err != nil {
149 return true, err
150 }
151 if mapping.Fingerprint == fingerprint {
152 return true, a.completeRegisteredMigration(ctx, source, cp, mapping.SessionID, cp.record.ContentDigest)
153 }
154 return true, a.sourceRecovery(ctx, path, format, "source_changed_after_adoption", source.scope, source.workspaceRoot, source.headID)
155 }
156
156 lines GO