返回 DeepSeek-Reasonix
session_purge.go
根目录 / desktop / session_purge.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "strings"
8
9 "reasonix/desktop/internal/workspacestate"
10 "reasonix/internal/session"
11 )
12
13 // PurgeCanonicalSession permanently removes only archived sessions. Adoption
14 // receipts survive deletion, including when an exclusive original is removed.
15 func (a *App) PurgeCanonicalSession(ref session.SessionRef) error {
16 if err := validateLocalSessionRef(ref); err != nil {
17 return err
18 }
19 state, err := a.workspaceRegistry().Load(a.bootContext())
20 if err != nil {
21 return err
22 }
23 release := a.lockRuntimeMutation("purge archived session")
24 defer release()
25 if err := a.purgeCanonicalSession(a.bootContext(), ref, state.Generation); err != nil {
26 return err
27 }
28 a.emitProjectTreeChanged()
29 a.emitSessionTargetChange("session_deleted", SessionTargetChangeEvent{
30 TargetKey: (SessionTarget{SessionRef: ref}).key(),
31 })
32 return nil
33 }
34
35 func (a *App) purgeCanonicalSessionWithOperation(ref session.SessionRef, operationID string) (SessionTarget, error) {
36 if err := validateLocalSessionRef(ref); err != nil {
37 return SessionTarget{}, err
38 }
39 target, err := a.resolveCanonicalPurgeTarget(ref)
40 if err != nil {
41 return SessionTarget{}, err
42 }
43 if target.Lifecycle != workspacestate.Archived && target.Lifecycle != workspacestate.Deleted {
44 return SessionTarget{}, newSessionOperationError("archived", "Archive this session before deleting it.")
45 }
46 operationID = strings.TrimSpace(operationID)
47 if operationID == "" {
48 operationID = "delete-" + strings.TrimPrefix(newTabID(), "tab_")
49 }
50 a.cancelAISessionTitle(target.key())
51 state, err := a.workspaceRegistry().Load(a.bootContext())
52 if err != nil {
53 return SessionTarget{}, err
54 }
55 release := a.lockRuntimeMutation("purge archived session")
56 defer release()
57 if err := a.purgeCanonicalSession(a.bootContext(), ref, state.Generation); err != nil {
58 return SessionTarget{}, err
59 }
60 if state, loadErr := a.workspaceRegistry().Load(a.bootContext()); loadErr == nil {
61 target.Lifecycle = state.SessionStates[ref.SessionID].Lifecycle
62 target.LifecycleGeneration = state.SessionStates[ref.SessionID].Generation
63 }
64 a.emitProjectTreeChanged()
65 a.emitSessionTargetChange("session_deleted", SessionTargetChangeEvent{
66 TargetKey: target.key(), OperationID: operationID,
67 LifecycleGeneration: target.LifecycleGeneration, WorkspaceID: target.WorkspaceID,
68 })
69 return target, nil
70 }
71
72 func (a *App) resolveCanonicalPurgeTarget(ref session.SessionRef) (SessionTarget, error) {
73 target, resolveErr := a.resolveCanonicalSessionTargetState(ref, "", true)
74 if resolveErr == nil {
75 return target, nil
76 }
77 state, err := a.workspaceRegistry().Load(a.bootContext())
78 if err != nil {
79 return SessionTarget{}, err
80 }
81 op, pending := state.PendingOperations["purge-"+ref.SessionID]
82 status, known := state.SessionStates[ref.SessionID]
83 if !pending || op.Kind != "purge" || !known || status.Lifecycle != workspacestate.Deleted {
84 return SessionTarget{}, resolveErr
85 }
86 target = a.runtimeSessionTarget("", ref, sessionRoute(ref.SessionID))
87 target.SessionRef = ref
88 target.SessionPath = sessionRoute(ref.SessionID)
89 target.TopicID = state.Presentation[ref.SessionID].TopicID
90 target.Lifecycle = status.Lifecycle
91 target.LifecycleGeneration = status.Generation
92 for id, workspace := range state.Workspaces {
93 if containsDesktopString(workspace.SessionIDs, ref.SessionID) {
94 target.WorkspaceID = id
95 target.WorkspaceRoot = workspace.Root
96 target.Scope = "project"
97 if id == workspacestate.GlobalWorkspaceID {
98 target.Scope, target.WorkspaceRoot = "global", ""
99 }
100 break
101 }
102 }
103 return target, nil
104 }
105
106 func (a *App) purgeCanonicalSession(ctx context.Context, ref session.SessionRef, expected uint64) error {
107 return a.executeCanonicalPurge(ctx, ref, expected, nil)
108 }
109
110 func (a *App) resumeCanonicalPurge(ctx context.Context, ref session.SessionRef, observed workspacestate.Operation) error {
111 return a.executeCanonicalPurge(ctx, ref, observed.ExpectedGeneration, &observed)
112 }
113
114 func (a *App) executeCanonicalPurge(ctx context.Context, ref session.SessionRef, expected uint64, observed *workspacestate.Operation) error {
115 store := a.workspaceRegistry()
116 state, err := store.Load(ctx)
117 if err != nil {
118 return err
119 }
120 existingOnly := observed != nil
121 if observed == nil {
122 if op, exists := state.PendingOperations["purge-"+ref.SessionID]; exists {
123 observed = &op
124 }
125 }
126 prepare := func() error {
127 if observed != nil {
128 return store.ResumePurgeForRequest(ctx, ref.SessionID, expected, *observed)
129 }
130 return store.BeginPurgeWithSources(ctx, ref.SessionID, expected)
131 }
132 switch workspacestate.ClassifyPurge(state, ref.SessionID) {
133 case workspacestate.PurgeCommitted:
134 return prepare()
135 case workspacestate.PurgePreparedStale:
136 // A restore or later lifecycle mutation already superseded this legacy
137 // prepare. Clean only the observed registry record; do not touch the
138 // runtime or filesystem for an obsolete deletion intent.
139 return prepare()
140 case workspacestate.PurgeInvalid:
141 return workspacestate.ErrMutationConflict
142 case workspacestate.PurgeAbsent:
143 if existingOnly {
144 return prepare()
145 }
146 if state.SessionStates[ref.SessionID].Generation > expected {
147 return workspacestate.ErrMutationConflict
148 }
149 if state.SessionStates[ref.SessionID].Lifecycle != workspacestate.Archived {
150 return errors.New("only archived sessions can be permanently deleted")
151 }
152 }
153 if err := a.retireArchivedSessionRuntime(ctx, ref); err != nil {
154 return fmt.Errorf("session runtime is still in use: %w", err)
155 }
156 filesystem := session.NewFilesystemPersistence(a.desktopSessions.root)
157 if err := filesystem.PurgeWithTombstone(ctx, ref.SessionID, func() error {
158 a.lifecycleCheckpoint("before-tombstone")
159 if err := prepare(); err != nil {
160 return err
161 }
162 a.lifecycleCheckpoint("after-tombstone")
163 return nil
164 }); err != nil {
165 return fmt.Errorf("purge cleanup pending: %w", err)
166 }
167 a.lifecycleCheckpoint("after-file-cleanup")
168 if err := a.purgeMigratedSources(ctx, ref.SessionID); err != nil {
169 return fmt.Errorf("source cleanup pending: %w", err)
170 }
171 a.lifecycleCheckpoint("after-source-cleanup")
172 if err := a.sessionUIStore().PurgeComposer(ctx, composerRecordKey(ref)); err != nil {
173 return err
174 }
175 a.sessionPresets.forget(ref.SessionID)
176 if err := store.AdvancePurge(ctx, ref.SessionID, "content_removed"); err != nil {
177 return err
178 }
179 a.lifecycleCheckpoint("after-content-removed")
180 if err := store.CompletePurge(ctx, ref.SessionID); err != nil {
181 return err
182 }
183 a.lifecycleCheckpoint("after-purge-committed")
184 return nil
185 }
186
187 func (a *App) lifecycleCheckpoint(phase string) {
188 if a.lifecycleCheckpointHook != nil {
189 a.lifecycleCheckpointHook(phase)
190 }
191 }
192
193 // Client release may retain an idle runtime for fast navigation. Archive and
194 // purge must retire that cache entry; Service.Close still refuses bound clients
195 // and executing turns, so this cannot close a live user's session underneath it.
196 func (a *App) retireArchivedSessionRuntime(ctx context.Context, ref session.SessionRef) error {
197 service := a.desktopSessionService("")
198 if _, live := service.Runtime(ref); !live {
199 return nil
200 }
201 err := service.Close(ctx, ref)
202 if errors.Is(err, session.ErrSessionNotRunning) {
203 return nil
204 }
205 return err
206 }
207
207 lines GO