返回 DeepSeek-Reasonix
legacy_empty_session_cleanup_batch.go
根目录 / desktop / legacy_empty_session_cleanup_batch.go
1 package main
2
3 import (
4 "context"
5 "path/filepath"
6 "slices"
7 "strings"
8
9 "reasonix/desktop/internal/legacycleanup"
10 "reasonix/desktop/internal/workspacestate"
11 "reasonix/internal/agent"
12 "reasonix/internal/session"
13 )
14
15 type legacyCleanupBatchBuilder struct {
16 app *App
17 state workspacestate.State
18 items map[string]legacycleanup.Candidate
19 mappedSources map[string]bool
20 seenLegacy map[string]bool
21 }
22
23 func newLegacyCleanupBatchBuilder(app *App, state workspacestate.State) *legacyCleanupBatchBuilder {
24 mapped := make(map[string]bool, len(state.SourceMappings))
25 for _, mapping := range state.SourceMappings {
26 mapped[sessionRuntimeKey(mapping.Path)+"\x00"+mapping.HeadID] = true
27 }
28 return &legacyCleanupBatchBuilder{
29 app: app, state: state, items: map[string]legacycleanup.Candidate{},
30 mappedSources: mapped, seenLegacy: map[string]bool{},
31 }
32 }
33
34 func (b *legacyCleanupBatchBuilder) registerCanonicalSessions(ctx context.Context) {
35 query := b.app.desktopSessionService("").Query()
36 for workspaceID, workspace := range b.state.Workspaces {
37 for _, sessionID := range workspace.SessionIDs {
38 status := b.state.SessionStates[sessionID]
39 if status.Lifecycle != "" && status.Lifecycle != workspacestate.Active {
40 continue
41 }
42 info, statErr := query.Stat(ctx, session.SessionRef{HostID: localDesktopHostID, SessionID: sessionID})
43 title := b.state.Presentation[sessionID].Title
44 if statErr == nil && (info.TitleSequence > 0 || strings.TrimSpace(info.Title) != "") {
45 title = info.Title
46 }
47 if statErr == nil && !isDefaultTopicTitle(title) {
48 continue
49 }
50 candidate := legacycleanup.Candidate{
51 ID: "session:" + sessionID, Kind: "session", WorkspaceID: workspaceID, SessionID: sessionID,
52 Title: title, TitleSequence: info.TitleSequence, EventSequence: info.EventSequence,
53 LifecycleGeneration: status.Generation, OperationID: "legacy-empty-" + sessionID, Phase: "registered",
54 }
55 candidate.Sources = b.sourcesForSession(sessionID)
56 b.items[candidate.ID] = candidate
57 }
58 }
59 }
60
61 func (b *legacyCleanupBatchBuilder) sourcesForSession(sessionID string) []legacycleanup.SourceSnapshot {
62 var sources []legacycleanup.SourceSnapshot
63 for _, mapping := range b.state.SourceMappings {
64 if mapping.SessionID != sessionID || mapping.Format != "legacy" {
65 continue
66 }
67 fingerprint, _ := legacyCleanupSourceFingerprint(mapping.Path)
68 sources = append(sources, legacycleanup.SourceSnapshot{Path: mapping.Path, HeadID: mapping.HeadID, Fingerprint: fingerprint})
69 }
70 slices.SortFunc(sources, func(left, right legacycleanup.SourceSnapshot) int {
71 return strings.Compare(sessionRuntimeKey(left.Path)+"\x00"+left.HeadID, sessionRuntimeKey(right.Path)+"\x00"+right.HeadID)
72 })
73 return sources
74 }
75
76 func (b *legacyCleanupBatchBuilder) registerLegacy(scope, root, workspaceID, topicID, title string) bool {
77 registered := false
78 for _, dir := range b.app.knownSessionDirs() {
79 for _, match := range topicSessionMatches(dir, topicID) {
80 if !topicSessionMatchMatchesTarget(match, scope, root) {
81 continue
82 }
83 path := filepath.Clean(match.path)
84 heads, err := session.LegacyMigrationHeads(b.app.bootContext(), path)
85 if err != nil || len(heads) == 0 {
86 heads = []agent.SessionHead{{}}
87 }
88 fingerprint, _ := legacyCleanupSourceFingerprint(path)
89 for _, head := range heads {
90 if head.Retired || !b.registerLegacyHead(workspaceID, topicID, title, path, fingerprint, head.ID) {
91 continue
92 }
93 registered = true
94 }
95 }
96 }
97 return registered
98 }
99
100 func (b *legacyCleanupBatchBuilder) registerLegacyHead(workspaceID, topicID, title, path, fingerprint, headID string) bool {
101 identity := sessionRuntimeKey(path) + "\x00" + headID
102 if b.seenLegacy[identity] || b.mappedSources[identity] {
103 return false
104 }
105 b.seenLegacy[identity] = true
106 key := desktopSourceKey(path, headID)
107 id := "legacy:" + key
108 b.items[id] = legacycleanup.Candidate{
109 ID: id, Kind: "legacy", WorkspaceID: workspaceID, TopicID: topicID,
110 SourcePath: path, SourceHeadID: headID, SourceFingerprint: fingerprint,
111 Title: title, OperationID: "legacy-empty-source-" + key, Phase: "registered",
112 }
113 return true
114 }
115
116 func (b *legacyCleanupBatchBuilder) registerTopics(scope, root, workspaceID string, ids, pinned []string, groups []desktopGroup) {
117 topicRoot := topicTitleRoot(scope, root)
118 titles, sources, created, rowRevisions := legacyCleanupTopicMetadata(topicRoot)
119 for order, topicID := range ids {
120 title := titles[topicID]
121 if !isDefaultTopicTitle(title) {
122 continue
123 }
124 legacyRegistered := b.registerLegacy(scope, root, workspaceID, topicID, title)
125 if b.topicHasCanonicalSession(topicID) || legacyRegistered {
126 continue
127 }
128 snapshot := legacyCleanupTopicSnapshot(scope, root, topicID, sources[topicID], created[topicID], rowRevisions[topicID], order, pinned, groups)
129 id := "topic:" + topicID
130 b.items[id] = legacycleanup.Candidate{ID: id, Kind: "topic", WorkspaceID: workspaceID, TopicID: topicID, Title: title, OperationID: "legacy-empty-topic-" + topicID, Phase: "registered", Topic: snapshot}
131 }
132 }
133
134 func legacyCleanupTopicMetadata(root string) (map[string]string, map[string]string, map[string]int64, map[string]int64) {
135 titles, sources := map[string]string{}, map[string]string{}
136 created, revisions := map[string]int64{}, map[string]int64{}
137 snapshot, err := desktopTopicState.snapshot(root)
138 if err != nil {
139 return loadTopicTitles(root), loadTopicTitleSources(root), loadTopicCreatedAts(root), revisions
140 }
141 for topicID, record := range snapshot.Records {
142 titles[topicID] = agent.UserPreviewText(record.Title)
143 sources[topicID] = record.TitleSource
144 created[topicID] = record.CreatedAtMS
145 revisions[topicID] = record.RowRevision
146 }
147 return titles, sources, created, revisions
148 }
149
150 func (b *legacyCleanupBatchBuilder) topicHasCanonicalSession(topicID string) bool {
151 for _, presentation := range b.state.Presentation {
152 if presentation.TopicID == topicID {
153 return true
154 }
155 }
156 return false
157 }
158
159 func (b *legacyCleanupBatchBuilder) workspaceIDForRoot(root string) string {
160 for id, workspace := range b.state.Workspaces {
161 if sameDesktopPath(workspace.Root, root) {
162 return id
163 }
164 }
165 return ""
166 }
167
168 func legacyCleanupTopicSnapshot(scope, root, topicID, source string, createdAt, rowRevision int64, order int, pinned []string, groups []desktopGroup) *legacycleanup.TopicSnapshot {
169 snapshot := &legacycleanup.TopicSnapshot{
170 Scope: scope, WorkspaceRoot: root, TitleSource: source, CreatedAt: createdAt,
171 RowRevision: rowRevision, Order: order, Pinned: slices.Contains(pinned, topicID), GroupOrder: -1,
172 }
173 for _, group := range groups {
174 if index := slices.Index(group.TopicIDs, topicID); index >= 0 {
175 snapshot.GroupID, snapshot.GroupOrder = group.ID, index
176 break
177 }
178 }
179 return snapshot
180 }
181
181 lines GO