返回 DeepSeek-Reasonix
saved_tab_route_reconcile.go
根目录 / desktop / saved_tab_route_reconcile.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "os"
7 "path/filepath"
8 "runtime"
9 "slices"
10 "strings"
11
12 "reasonix/internal/session"
13 "reasonix/internal/store"
14 )
15
16 type savedTabRouteCandidate struct {
17 sessionID string
18 kind string
19 }
20
21 func savedTabRouteCandidateForPath(raw string) savedTabRouteCandidate {
22 value := strings.TrimSpace(raw)
23 if value == "" {
24 return savedTabRouteCandidate{}
25 }
26 if strings.HasPrefix(value, remoteSessionIDRoutePrefix) {
27 locator := classifySessionLocator(value)
28 if locator.kind != sessionLocatorCanonical {
29 return savedTabRouteCandidate{kind: "invalid_route"}
30 }
31 return savedTabRouteCandidate{sessionID: locator.ref.SessionID, kind: "canonical_route"}
32 }
33 if !persistedPathLooksAbsolute(value) {
34 return savedTabRouteCandidate{}
35 }
36 normalized := strings.ReplaceAll(value, `\`, "/")
37 base := normalized[strings.LastIndex(normalized, "/")+1:]
38 if !strings.HasPrefix(base, remoteSessionIDRoutePrefix) {
39 return savedTabRouteCandidate{}
40 }
41 // A real legacy transcript or sidecar can legally contain a colon on
42 // POSIX. Those names remain paths and are never upgraded by this repair.
43 if strings.HasSuffix(strings.ToLower(base), ".jsonl") || strings.HasSuffix(strings.ToLower(base), ".meta") {
44 return savedTabRouteCandidate{}
45 }
46 locator := classifySessionLocator(base)
47 if locator.kind != sessionLocatorCanonical {
48 return savedTabRouteCandidate{kind: "invalid_route"}
49 }
50 return savedTabRouteCandidate{sessionID: locator.ref.SessionID, kind: "pseudo_route_path"}
51 }
52
53 func persistedPathLooksAbsolute(path string) bool {
54 if strings.HasPrefix(path, "/") || strings.HasPrefix(path, `\\`) || strings.HasPrefix(path, "//") {
55 return true
56 }
57 return len(path) >= 3 && ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z')) && path[1] == ':' && (path[2] == '\\' || path[2] == '/')
58 }
59
60 func (a *App) normalizeSavedTabRoute(ctx context.Context, entry desktopTabEntry, evidence savedTabReconcileEvidence) (desktopTabEntry, bool, *savedTabReconcileDecision) {
61 candidate := savedTabRouteCandidateForPath(entry.SessionPath)
62 failed := func(reason string, pending, recovery bool) (desktopTabEntry, bool, *savedTabReconcileDecision) {
63 return entry, false, &savedTabReconcileDecision{
64 outcome: preserveError, reason: reason, identityKind: candidate.kind,
65 hadPending: pending, hadRecoveryOwner: recovery,
66 }
67 }
68 if candidate.kind == "" {
69 return entry, false, nil
70 }
71 if candidate.kind == "invalid_route" || candidate.sessionID == "" {
72 return failed("invalid_route", false, false)
73 }
74 if id := strings.TrimSpace(entry.SessionID); id != "" && id != candidate.sessionID {
75 return failed("identity_conflict", false, false)
76 }
77 if evidence.registryErr != nil || evidence.draftErr != nil {
78 return failed("persistence_state_unavailable", false, false)
79 }
80
81 pendingID, pendingFound, pendingConflict := savedTabPendingSessionIdentity(desktopTabEntry{
82 CreateOperationID: entry.CreateOperationID,
83 }, evidence)
84 if pendingConflict || (pendingFound && pendingID != candidate.sessionID) {
85 return failed("pending_identity_conflict", true, false)
86 }
87 if candidate.kind == "canonical_route" {
88 entry.SessionID = candidate.sessionID
89 entry.SessionPath = ""
90 return entry, true, nil
91 }
92
93 present, artifactErr := savedTabPseudoRouteArtifactsPresent(entry.SessionPath)
94 if artifactErr != nil {
95 return failed("legacy_artifacts_unreadable", pendingFound, false)
96 }
97 if present {
98 return failed("legacy_artifacts_present", pendingFound, false)
99 }
100 confirmed, conflict, recoveryOwner := a.savedTabPseudoRouteIdentityConfirmed(ctx, entry, candidate.sessionID, evidence, pendingID, pendingFound)
101 if conflict {
102 return failed("identity_conflict", pendingFound, recoveryOwner)
103 }
104 if !confirmed {
105 return failed("identity_evidence_absent", pendingFound, recoveryOwner)
106 }
107 entry.SessionID = candidate.sessionID
108 entry.SessionPath = ""
109 return entry, true, nil
110 }
111
112 func savedTabPseudoRouteArtifactsPresent(path string) (bool, error) {
113 return savedTabPseudoRouteArtifactsPresentWith(path, os.Lstat)
114 }
115
116 func savedTabPseudoRouteArtifactsPresentWith(path string, lstat func(string) (os.FileInfo, error)) (bool, error) {
117 path = strings.TrimSpace(path)
118 if path == "" {
119 return false, nil
120 }
121 if runtime.GOOS == "windows" && savedTabRouteCandidateForPath(path).kind == "pseudo_route_path" {
122 // The route colon is illegal in a Windows file name, so this spelling
123 // cannot identify a real transcript or sidecar on the native filesystem.
124 return false, nil
125 }
126 // Foreign Windows paths on POSIX cannot name a local artifact. Native
127 // paths are probed without cleaning so the persisted spelling is preserved.
128 if runtime.GOOS != "windows" && !filepath.IsAbs(path) {
129 return false, nil
130 }
131 artifacts := append([]string{path}, store.SessionSidecarFiles(path)...)
132 artifacts = append(artifacts,
133 store.SessionLockFile(path), store.SessionLeaseLock(path), store.SessionLeaseInfo(path),
134 store.SessionCheckpointDir(path), store.SessionJobsDir(path), store.SessionInboxDir(path),
135 store.SessionCleanupPending(path),
136 )
137 seen := make(map[string]bool, len(artifacts))
138 for _, artifact := range artifacts {
139 if artifact == "" || seen[artifact] {
140 continue
141 }
142 seen[artifact] = true
143 if _, err := lstat(artifact); err == nil {
144 return true, nil
145 } else if !errors.Is(err, os.ErrNotExist) {
146 return false, err
147 }
148 }
149 return false, nil
150 }
151
152 func (a *App) savedTabPseudoRouteIdentityConfirmed(ctx context.Context, entry desktopTabEntry, sessionID string, evidence savedTabReconcileEvidence, pendingID string, pendingFound bool) (bool, bool, bool) {
153 confirmed := pendingFound && pendingID == sessionID
154 owners := savedTabDurableRouteOwnerIDs(entry, sessionID, evidence)
155 if len(owners) > 1 || (len(owners) == 1 && !owners[sessionID]) {
156 return false, true, len(owners) > 0
157 }
158 if owners[sessionID] {
159 confirmed = true
160 }
161
162 info, err := a.desktopSessionService("").Query().Stat(ctx, session.SessionRef{HostID: localDesktopHostID, SessionID: sessionID})
163 if err == nil {
164 workspace, consistent := savedTabCanonicalWorkspace(evidence.registry, info, sessionID)
165 if consistent && savedTabMatchesWorkspace(entry, workspace) {
166 confirmed = true
167 }
168 }
169 return confirmed, false, len(owners) > 0
170 }
171
172 func savedTabDurableRouteOwnerIDs(entry desktopTabEntry, candidateID string, evidence savedTabReconcileEvidence) map[string]bool {
173 owners := map[string]bool{}
174 operationID := strings.TrimSpace(entry.CreateOperationID)
175 rawPath := strings.TrimSpace(entry.SessionPath)
176 workspaceID := restoredWorkspaceID(entry)
177 for sessionID, pending := range evidence.registry.PendingCreates {
178 if (operationID != "" && pending.OperationID == operationID) || (sessionID == candidateID && pending.WorkspaceID == workspaceID) {
179 owners[strings.TrimSpace(sessionID)] = true
180 }
181 }
182 for _, operation := range evidence.draftOps {
183 if operationID != "" && operation.ID == operationID {
184 owners[strings.TrimSpace(operation.SessionID)] = true
185 }
186 }
187 for _, operation := range evidence.registry.PendingOperations {
188 if operationID != "" && strings.TrimSpace(operation.ID) == operationID {
189 for _, id := range operation.SessionIDs {
190 owners[strings.TrimSpace(id)] = true
191 }
192 }
193 if operation.Mapping != nil && strings.TrimSpace(operation.Mapping.Path) == rawPath {
194 owners[strings.TrimSpace(operation.Mapping.SessionID)] = true
195 }
196 if operation.WorkspaceID == workspaceID && slices.Contains(operation.SessionIDs, candidateID) {
197 owners[candidateID] = true
198 }
199 }
200 for _, mapping := range evidence.registry.SourceMappings {
201 if strings.TrimSpace(mapping.Path) == rawPath {
202 owners[strings.TrimSpace(mapping.SessionID)] = true
203 }
204 if mapping.WorkspaceID == workspaceID && mapping.SessionID == candidateID {
205 owners[candidateID] = true
206 }
207 }
208 for _, recovery := range evidence.registry.RecoveryEntries {
209 if strings.TrimSpace(recovery.Path) == rawPath {
210 owners[strings.TrimSpace(recovery.SessionID)] = true
211 }
212 if recovery.SessionID == candidateID && (recovery.WorkspaceID == workspaceID || recovery.Scope == entry.Scope && sameDesktopPath(recovery.WorkspaceRoot, entry.WorkspaceRoot)) {
213 owners[candidateID] = true
214 }
215 }
216 delete(owners, "")
217 return owners
218 }
219
220 func blockSavedTabUnsafeRestore(file desktopTabsFile, reason string) desktopTabsFile {
221 file.Tabs = append([]desktopTabEntry(nil), file.Tabs...)
222 for index := range file.Tabs {
223 path := strings.TrimSpace(file.Tabs[index].SessionPath)
224 unsafe := savedTabRouteCandidateForPath(path).kind != ""
225 if !unsafe && path != "" {
226 _, ok, err := legacySessionPathForFileAccess(path)
227 unsafe = err != nil || !ok
228 }
229 if unsafe {
230 file.Tabs[index].restoreBlocked = true
231 file.Tabs[index].restoreBlockReason = reason
232 }
233 }
234 return file
235 }
236
236 lines GO