返回 DeepSeek-Reasonix
topic_archive_ownership.go
根目录 / desktop / topic_archive_ownership.go
1 package main
2
3 import (
4 "errors"
5 "log/slog"
6 "slices"
7
8 "reasonix/internal/agent"
9 "reasonix/internal/control"
10 )
11
12 type topicArchiveRemovalOwnership struct {
13 guard *agent.SessionRemovalGuard
14 restoreTab *WorkspaceTab
15 }
16
17 type topicArchiveOwnershipBatch struct {
18 entries []*topicArchiveRemovalOwnership
19 byPath map[string]*topicArchiveRemovalOwnership
20 }
21
22 func takeTopicArchiveSessionLease(tab *WorkspaceTab, sessionPath string) *agent.SessionLease {
23 if tab == nil {
24 return nil
25 }
26 key := sessionRuntimeKey(sessionPath)
27 tab.sessionLeaseMu.Lock()
28 defer tab.sessionLeaseMu.Unlock()
29 lease := tab.sessionLease
30 if lease == nil || sessionRuntimeKey(lease.Path()) != key {
31 return nil
32 }
33 tab.sessionLease = nil
34 tab.storeSessionLeaseRuntimeKey("")
35 return lease
36 }
37
38 func topicArchiveLeaseOwners(removed []removedSessionRuntime) map[string]*WorkspaceTab {
39 owners := make(map[string]*WorkspaceTab, len(removed))
40 for _, item := range removed {
41 if item.tab == nil || item.readOnly {
42 continue
43 }
44 // A migrated runtime publishes a SessionID and clears its legacy path,
45 // but can still hold the imported file's compatibility lease. Ownership
46 // follows the actual lease, not the controller's current display path.
47 if key := item.tab.sessionLeaseRuntimeKey(); key != "" {
48 owners[key] = item.tab
49 }
50 }
51 return owners
52 }
53
54 func acquireTopicArchiveOwnership(targets []topicTrashTarget, removed []removedSessionRuntime) (*topicArchiveOwnershipBatch, error) {
55 batch := &topicArchiveOwnershipBatch{byPath: make(map[string]*topicArchiveRemovalOwnership, len(targets))}
56 localOwners := topicArchiveLeaseOwners(removed)
57 // Acquire historical/non-runtime sessions first. If another process owns
58 // any target, the archive aborts before a local runtime lease is touched.
59 for _, target := range targets {
60 key := sessionRuntimeKey(target.sessionPath)
61 if localOwners[key] != nil {
62 continue
63 }
64 guard, err := acquireSessionRemovalGuard(target.sessionPath)
65 if err != nil {
66 batch.release()
67 return nil, err
68 }
69 batch.add(target, guard, nil)
70 }
71 // Convert local runtime leases without releasing the lease lock. Converted
72 // guards can be restored if marker publication or generation validation
73 // fails before detach commits.
74 for _, target := range targets {
75 key := sessionRuntimeKey(target.sessionPath)
76 tab := localOwners[key]
77 if tab == nil {
78 continue
79 }
80 lease := takeTopicArchiveSessionLease(tab, target.sessionPath)
81 if lease == nil {
82 batch.rollback()
83 return nil, errTopicArchiveBusy
84 }
85 guard, err := lease.TryConvertToRemovalGuard()
86 if err != nil {
87 tab.adoptSessionLease(lease)
88 batch.rollback()
89 if errors.Is(err, agent.ErrSessionLeaseHeld) {
90 return nil, errSessionBusyElsewhere
91 }
92 return nil, err
93 }
94 batch.add(target, guard, tab)
95 }
96 return batch, nil
97 }
98
99 func (b *topicArchiveOwnershipBatch) add(target topicTrashTarget, guard *agent.SessionRemovalGuard, tab *WorkspaceTab) {
100 entry := &topicArchiveRemovalOwnership{guard: guard, restoreTab: tab}
101 b.entries = append(b.entries, entry)
102 b.byPath[sessionRuntimeKey(target.sessionPath)] = entry
103 }
104
105 func (b *topicArchiveOwnershipBatch) take(sessionPath string) *agent.SessionRemovalGuard {
106 if b == nil {
107 return nil
108 }
109 entry := b.byPath[sessionRuntimeKey(sessionPath)]
110 if entry == nil {
111 return nil
112 }
113 guard := entry.guard
114 entry.guard = nil
115 return guard
116 }
117
118 func (b *topicArchiveOwnershipBatch) rollback() {
119 if b == nil {
120 return
121 }
122 for _, entry := range slices.Backward(b.entries) {
123 if entry.guard == nil {
124 continue
125 }
126 if entry.restoreTab != nil {
127 lease, err := entry.guard.RestoreSessionLease()
128 if err != nil {
129 slog.Warn("desktop: restoring topic archive session ownership failed")
130 entry.guard.Release()
131 } else {
132 entry.restoreTab.adoptSessionLease(lease)
133 }
134 } else {
135 entry.guard.Release()
136 }
137 entry.guard = nil
138 }
139 }
140
141 func (b *topicArchiveOwnershipBatch) release() {
142 if b == nil {
143 return
144 }
145 for _, entry := range b.entries {
146 if entry.guard != nil {
147 entry.guard.Release()
148 entry.guard = nil
149 }
150 }
151 }
152
153 func delayedDesktopTopicTrash(dir, sessionPath, key string, guard *agent.SessionRemovalGuard, destroys []control.SessionDestroyHandle) {
154 waitAllDestroyHandles(destroys)
155 if err := trashSessionArtifactsWithGuard(dir, sessionPath, key, guard); err != nil {
156 slog.Warn("desktop: delayed topic archive cleanup remains pending")
157 }
158 finishDestroyHandles(destroys)
159 }
160
160 lines GO