返回 DeepSeek-Reasonix
session_resume.go
根目录 / desktop / session_resume.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "strings"
8
9 "reasonix/desktop/internal/workspacestate"
10 "reasonix/internal/control"
11 "reasonix/internal/session"
12 )
13
14 var errSessionNavigationSuperseded = errors.New("session navigation was superseded")
15
16 func (a *App) continueLegacySessionForTranscript(tab *WorkspaceTab, ctrl control.SessionAPI, sourcePath string, limit int, includeHistory, readOnly bool) (HistoryPage, error) {
17 identity, ok := ctrl.(control.IdentityLifecycle)
18 if !ok || !identity.UsesExclusiveSession() {
19 return HistoryPage{}, fmt.Errorf("session identity protocol is unavailable")
20 }
21 navigationCtx, finishNavigation := a.beginSessionNavigationContext()
22 defer finishNavigation()
23 if err := context.Cause(navigationCtx); err != nil {
24 return HistoryPage{}, err
25 }
26 if _, adopted, err := a.legacyCanonicalRef(navigationCtx, sourcePath); err != nil {
27 return HistoryPage{}, err
28 } else if !adopted {
29 // Explicit navigation imports before taking any controller swap gate.
30 if _, err := a.ImportHistoricalSession(desktopSourceKey(sourcePath, "")); err != nil {
31 return HistoryPage{}, err
32 }
33 }
34
35 a.runtimeRebuildMu.Lock()
36 defer a.runtimeRebuildMu.Unlock()
37 tab.turnStartMu.Lock()
38 defer tab.turnStartMu.Unlock()
39 if err := context.Cause(navigationCtx); err != nil {
40 return HistoryPage{}, err
41 }
42
43 current := a.controllerForTab(tab)
44 if current != ctrl || current == nil {
45 return HistoryPage{}, fmt.Errorf("tab runtime changed while continuing legacy session")
46 }
47 if current.RuntimeStatus().Running || current.RuntimeStatus().PendingPrompt {
48 return HistoryPage{}, control.ErrTurnRunning
49 }
50 if err := current.Snapshot(); err != nil {
51 return HistoryPage{}, err
52 }
53 a.mu.RLock()
54 createOptions := desktopLegacyImportOptions(snapshotTabRuntimeLocked(tab).workspaceRoot)
55 a.mu.RUnlock()
56 _, err := a.openOrImportDesktopLegacySession(navigationCtx, identity, sourcePath, createOptions)
57 if err != nil {
58 return HistoryPage{}, err
59 }
60 a.syncTabSessionIdentity(tab, current)
61 a.setTabReadOnly(tab.ID, readOnly)
62 a.invalidatePromptHistoryCache()
63 a.notifyTabRuntimeRebuilt(tab)
64 if !includeHistory {
65 return HistoryPage{Messages: []HistoryMessage{}}, nil
66 }
67 return historyPageFromMessagesForTab(tab, current, current.History(), 0, limit), nil
68 }
69
70 // canonicalOpenIdentity validates the identity protocol of a resolved runtime.
71 // A nil runtime is a dormant tab, not a protocol failure.
72 func canonicalOpenIdentity(ctrl control.SessionAPI) (control.IdentityLifecycle, error) {
73 if ctrl == nil {
74 return nil, nil
75 }
76 identity, ok := ctrl.(control.IdentityLifecycle)
77 if !ok || !identity.UsesExclusiveSession() {
78 return nil, fmt.Errorf("session identity protocol is unavailable")
79 }
80 return identity, nil
81 }
82
83 func (a *App) resumeCanonicalSessionForTranscript(tab *WorkspaceTab, ctrl control.SessionAPI, route string, limit int, includeHistory bool, navigationSequence ...uint64) (HistoryPage, error) {
84 identity, err := canonicalOpenIdentity(ctrl)
85 if err != nil {
86 return HistoryPage{}, err
87 }
88 service := a.desktopSessionService("")
89 ref, ok := sessionRefForRoute(service, route)
90 if !ok {
91 return HistoryPage{}, fmt.Errorf("invalid session identity")
92 }
93 navigationCtx, finishNavigation := a.beginSessionNavigationContext(navigationSequence...)
94 defer finishNavigation()
95 if err := context.Cause(navigationCtx); err != nil {
96 return HistoryPage{}, err
97 }
98 workspace, err := a.canonicalSessionWorkspace(navigationCtx, ref)
99 if err != nil {
100 return HistoryPage{}, err
101 }
102
103 a.runtimeRebuildMu.Lock()
104 defer a.runtimeRebuildMu.Unlock()
105 tab.turnStartMu.Lock()
106 defer tab.turnStartMu.Unlock()
107 wantedNavigation := uint64(0)
108 if len(navigationSequence) > 0 {
109 wantedNavigation = navigationSequence[0]
110 if a.desktopSessions.navigationSeq.Load() != wantedNavigation {
111 return HistoryPage{}, errSessionNavigationSuperseded
112 }
113 }
114 if err := context.Cause(navigationCtx); err != nil {
115 return HistoryPage{}, err
116 }
117
118 current := a.controllerForTab(tab)
119 if ctrl == nil {
120 // The caller resolved a dormant tab before it had a runtime. Adopting a
121 // concurrently built one here, under the rebuild lock, is the
122 // authoritative read; the fences below still reject a later build.
123 ctrl = current
124 if identity, err = canonicalOpenIdentity(ctrl); err != nil {
125 return HistoryPage{}, err
126 }
127 }
128 if current != ctrl {
129 return HistoryPage{}, fmt.Errorf("tab runtime changed while opening session")
130 }
131 var currentRef session.SessionRef
132 if identity != nil {
133 currentRef, _ = identity.SessionRef()
134 }
135 workspaceChanged := canonicalWorkspaceChanged(a.tabRuntimeSnapshot(tab), workspace)
136 if current == nil || currentRef != ref || workspaceChanged {
137 if current != nil && !controllerHasActiveRuntimeWork(current) {
138 if err := current.Snapshot(); err != nil {
139 return HistoryPage{}, err
140 }
141 }
142 adopted, err := a.reattachCanonicalSessionRuntime(tab, current, ref, workspace, wantedNavigation)
143 if err != nil {
144 return HistoryPage{}, err
145 }
146 if adopted != nil {
147 current = adopted
148 } else {
149 binding, err := service.EnsureExecution(navigationCtx, ref)
150 if err != nil {
151 return HistoryPage{}, err
152 }
153 defer func() { _ = binding.Release(a.bootContext()) }()
154 targetModel := strings.TrimSpace(binding.Runtime().StateSnapshot().Session.Projection.ModelRef)
155 current, err = a.replaceControllerForSessionOpenLocked(navigationCtx, tab, current, service, ref, targetModel, workspace, wantedNavigation)
156 if err != nil {
157 return HistoryPage{}, err
158 }
159 }
160 }
161 if err := a.commitCanonicalSessionBinding(tab, current, ref, workspace, wantedNavigation); err != nil {
162 return HistoryPage{}, err
163 }
164 a.setTabReadOnly(tab.ID, false)
165 a.invalidatePromptHistoryCache()
166 a.notifyTabRuntimeRebuilt(tab)
167 if !includeHistory {
168 return HistoryPage{Messages: []HistoryMessage{}}, nil
169 }
170 return historyPageFromMessagesForTab(tab, current, current.History(), 0, limit), nil
171 }
172
173 // replaceControllerForSessionOpenLocked prepares an Agent for the target session's
174 // recorded model before publishing it to the tab. The caller holds
175 // runtimeRebuildMu and tab.turnStartMu, so the source remains usable until the
176 // target model, writer, and event projection have all been validated.
177 func (a *App) replaceControllerForSessionOpenLocked(ctx context.Context, tab *WorkspaceTab, current control.SessionAPI, service *session.Service, ref session.SessionRef, targetModel string, workspace workspacestate.Workspace, navigationSequence ...uint64) (control.SessionAPI, error) {
178 if tab == nil || service == nil {
179 return nil, fmt.Errorf("session runtime changed while opening session")
180 }
181 transition, err := a.reserveSessionRuntimePath(tab, sessionRoute(ref.SessionID))
182 if err != nil {
183 return nil, userFacingSessionLeaseError("", err)
184 }
185 committed := false
186 // boot retains its context for MCP and other controller-owned work. Relay
187 // navigation cancellation only until publication, then keep the app lifetime.
188 controllerCtx, cancelController := context.WithCancel(a.bootContext())
189 stopNavigationCancellation := context.AfterFunc(ctx, cancelController)
190 defer func() {
191 stopNavigationCancellation()
192 if !committed {
193 cancelController()
194 a.rollbackSessionRuntimePath(transition)
195 }
196 }()
197 prepared, err := a.prepareSessionOpenEnvironment(tab, workspace)
198 if err != nil {
199 return nil, err
200 }
201 defer func() { a.finishSessionOpenEnvironment(prepared, committed) }()
202 snap, cfg, root, sharedHost := prepared.snapshot, prepared.config, workspace.Root, prepared.host
203 if targetModel == "" {
204 targetModel, _, _ = cfg.ResolveDesktopNewSessionModel()
205 }
206 extensionGeneration := a.currentExtensionGeneration()
207 buildOptions := a.sessionOpenBootOptions(tab, snap, cfg, service, sharedHost, root, targetModel)
208 requestedModel := targetModel
209 candidate, targetModel, fallbackUsed, err := a.buildSessionOpenControllerCandidate(controllerCtx, extensionGeneration, cfg, buildOptions)
210 if err != nil {
211 return nil, err
212 }
213 discard := true
214 defer func() {
215 if discard {
216 candidate.Close()
217 }
218 }()
219 candidateIdentity, ok := candidate.(control.IdentityLifecycle)
220 if !ok || !candidateIdentity.UsesExclusiveSession() {
221 return nil, fmt.Errorf("replacement session identity protocol is unavailable")
222 }
223 if _, err := candidateIdentity.OpenSession(ctx, ref); err != nil {
224 return nil, err
225 }
226 if fallbackUsed {
227 if err := service.SetModel(ctx, ref, targetModel, cfg.ModelSelectionIdentity(targetModel)); err != nil {
228 return nil, err
229 }
230 a.noticeForTab(tab.ID, fmt.Sprintf("model %q is no longer available; switched to %s", requestedModel, targetModel))
231 }
232 a.bindControllerDisplayRecorder(candidate)
233 runtime := prepareCanonicalControllerRuntime(candidate, snap)
234
235 confirmed, err := a.canonicalSessionWorkspace(ctx, ref)
236 if err != nil {
237 return nil, err
238 }
239 if confirmed.ID != workspace.ID || !sameDesktopPath(confirmed.Root, root) {
240 return nil, errSessionWorkspaceConflict
241 }
242 var terminalSessions []*terminalSession
243 a.mu.Lock()
244 if err := a.authorizeSessionOpenPublicationLocked(tab, current, candidate, navigationSequence); err != nil {
245 a.mu.Unlock()
246 return nil, err
247 }
248 if !stopNavigationCancellation() || ctx.Err() != nil {
249 a.mu.Unlock()
250 return nil, context.Canceled
251 }
252 oldSink := tab.sink
253 if !a.commitCanonicalRuntimeTransitionLocked(tab, transition, prepared.preserveSource) {
254 a.mu.Unlock()
255 return nil, fmt.Errorf("tab runtime changed while opening session")
256 }
257 if prepared.workspaceChanged && a.terminals != nil {
258 terminalSessions = a.terminals.detachForTab(tab.ID)
259 }
260 applyCanonicalWorkspaceLocked(tab, workspace, prepared.workspaceChanged)
261 tab.SharedHostKey = snap.sharedHostKey
262 tab.Ctrl = candidate
263 tab.sink = snap.sink
264 tab.adoptDisplayState(&tabDisplayState{})
265 tab.ActivityStatus = ""
266 tab.replaceTelemetry(tabTelemetrySnapshot{}, sessionRuntimeKey(sessionRoute(ref.SessionID)))
267 setTabSessionIdentity(tab, sessionRoute(ref.SessionID))
268 tab.model = targetModel
269 tab.Label = candidate.Label()
270 applyNormalizedRuntimeToTabLocked(tab, runtime)
271 tab.Ready = true
272 clearTabStartupError(tab)
273 if prepared.preserveSource {
274 a.newSessionRuntimeLocked(tab, transition.targetKey)
275 }
276 tab.sink.setBinding(tab.ID, a, tab.SessionGeneration)
277 tab.sink.setContext(a.ctx)
278 a.bindSessionRuntimeKeyLocked(tab, tab.currentSessionIdentity())
279 a.supersedeTabBuildLocked(tab)
280 a.saveTabsLocked()
281 epoch := a.advanceSessionRuntimeEpochLocked(tab)
282 committed = true
283 a.mu.Unlock()
284
285 if !prepared.preserveSource {
286 fenceCanonicalNavigationSink(oldSink)
287 retireReplacedController(current, candidate)
288 }
289 if prepared.workspaceChanged {
290 a.finishCanonicalWorkspaceMove(tab.ID, terminalSessions)
291 }
292 discard = false
293 a.notifyTabRuntimeRebuiltAtEpoch(tab, epoch)
294 return candidate, nil
295 }
296
297 // The caller holds App.mu so intent, surface identity, and replacement guards
298 // are checked against the same state immediately before controller publication.
299 func (a *App) authorizeSessionOpenPublicationLocked(tab *WorkspaceTab, current, candidate control.SessionAPI, navigation []uint64) error {
300 if len(navigation) > 0 && navigation[0] != 0 && a.desktopSessions.navigationSeq.Load() != navigation[0] {
301 return errSessionNavigationSuperseded
302 }
303 if tab.removed || a.tabs[tab.ID] != tab || tab.Ctrl != current {
304 return fmt.Errorf("tab runtime changed while opening session")
305 }
306 return a.authorizeTabReplacementLocked(tab, candidate, "opening session", "session-open")
307 }
308
308 lines GO